feat: add external subtitle file detection with language parsing and cache invalidation on subtitle changes

Add EXTERNAL_SUBTITLE_EXTENSIONS set with common subtitle formats (.srt, .ass, .ssa, .vtt, .sub, .idx, .sup). Add external_subtitle_files() to glob subtitle files matching media stem and external_subtitle_languages() to parse language codes from filenames (de/deu/ger/german/deutsch, en/eng/english, fr/fra/fre/french/französisch, es/spa/spanish/spanisch, it/ita/italian/italienisch).

Update run_ffprobe() to merge external subtitle
This commit is contained in:
2026-08-14 13:25:37 +02:00
parent b5365170fb
commit 61d65d5988
2 changed files with 36 additions and 7 deletions
+30 -2
View File
@@ -180,6 +180,29 @@ def map_path(path, remote, local):
return str(Path(local) / path[len(remote):].lstrip("/"))
return path
EXTERNAL_SUBTITLE_EXTENSIONS = {".srt", ".ass", ".ssa", ".vtt", ".sub", ".idx", ".sup"}
def external_subtitle_files(path):
media = Path(path)
return sorted(candidate for candidate in media.parent.glob(f"{media.stem}.*") if candidate.suffix.lower() in EXTERNAL_SUBTITLE_EXTENSIONS)
def external_subtitle_languages(path):
media = Path(path)
languages = []
for candidate in external_subtitle_files(path):
tokens = {token for token in re.split(r"[. _()\[\]-]+", candidate.stem.lower()) if token}
if tokens & {"de", "deu", "ger", "german", "deutsch"}:
languages.append(norm_lang("de"))
if tokens & {"en", "eng", "english"}:
languages.append(norm_lang("en"))
if tokens & {"fr", "fra", "fre", "french", "französisch"}:
languages.append(norm_lang("fr"))
if tokens & {"es", "spa", "spanish", "spanisch"}:
languages.append(norm_lang("es"))
if tokens & {"it", "ita", "italian", "italienisch"}:
languages.append(norm_lang("it"))
return unique_langs(languages)
def run_ffprobe(path):
proc = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=size:stream=index,codec_type,codec_name,channels,channel_layout:stream_tags=language,title", "-of", "json", path], capture_output=True, text=True, timeout=90)
if proc.returncode != 0: raise RuntimeError(proc.stderr.strip() or "ffprobe fehlgeschlagen")
@@ -190,13 +213,18 @@ def run_ffprobe(path):
elif kind == "audio":
audios.append(lang); details.append({"language": lang, "codec": stream.get("codec_name"), "channels": stream.get("channels"), "layout": stream.get("channel_layout"), "title": tags.get("title")})
elif kind == "subtitle": subs.append(lang)
return {"audio_languages": unique_langs(audios), "subtitle_languages": unique_langs(subs), "audio_details": details, "video_codec": video or "", "size": int((raw.get("format") or {}).get("size") or 0)}
subtitles = unique_langs(subs + external_subtitle_languages(path))
return {"audio_languages": unique_langs(audios), "subtitle_languages": subtitles, "external_subtitle_files": [str(file) for file in external_subtitle_files(path)], "audio_details": details, "video_codec": video or "", "size": int((raw.get("format") or {}).get("size") or 0)}
def cached(path):
p = Path(path)
if not p.exists(): return {"error": f"Datei nicht gefunden: {p}", "pending": True}
mtime = p.stat().st_mtime; con = db(); row = con.execute("SELECT mtime,data FROM media_cache WHERE path=?", (str(p),)).fetchone(); con.close()
if row and float(row["mtime"]) == float(mtime): return json.loads(row["data"])
if row and float(row["mtime"]) == float(mtime):
data = json.loads(row["data"])
current_subtitles = [str(file) for file in external_subtitle_files(path)]
if "external_subtitle_files" in data and data["external_subtitle_files"] == current_subtitles:
return data
return None
def scan_one(path):