fix(captions): add language variant fallback + error on no files
Two fixes for captions downloads: 1. Language variant fallback: When user requests 'en' but the video only has 'en-GB' (or other regional variants), yt-dlp silently skips subtitle extraction because it uses exact language matching. Now the subtitleslangs list includes common regional variants (e.g. en-US, en-GB, en-AU, etc.) so yt-dlp can match the first available variant. 2. Error on no files: When yt-dlp completes successfully (ret=0) but produces no subtitle files (e.g. no matching language at all), the download was incorrectly shown as 'completed'. Now it's marked as 'error' with a descriptive message like 'No subtitles found for language "en"'. Same treatment for thumbnail downloads.
This commit is contained in:
parent
e639caa21d
commit
36b397ae93
2 changed files with 72 additions and 6 deletions
|
|
@ -10,6 +10,39 @@ CODEC_FILTER_MAP = {
|
||||||
'vp9': "[vcodec~='^vp0?9']",
|
'vp9': "[vcodec~='^vp0?9']",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Common language variant suffixes that yt-dlp may encounter on YouTube and
|
||||||
|
# other platforms. When a user requests "en" we also try "en-GB", "en-US",
|
||||||
|
# etc. so that a request isn't silently skipped when only a regional variant
|
||||||
|
# exists. yt-dlp processes the list in order and picks the first match.
|
||||||
|
_LANGUAGE_REGION_VARIANTS = {
|
||||||
|
"en": ("US", "GB", "AU", "CA", "IN", "NZ", "IE", "ZA"),
|
||||||
|
"zh": ("CN", "TW", "HK", "SG"),
|
||||||
|
"pt": ("BR", "PT"),
|
||||||
|
"es": ("ES", "MX", "AR", "CO", "CL", "PE", "VE"),
|
||||||
|
"fr": ("FR", "CA", "BE", "CH"),
|
||||||
|
"de": ("DE", "AT", "CH"),
|
||||||
|
"ar": ("SA", "AE", "EG", "MA", "DZ"),
|
||||||
|
"nl": ("NL", "BE"),
|
||||||
|
"ro": ("RO", "MD"),
|
||||||
|
"ms": ("MY", "BN", "SG"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _subtitle_lang_list(language: str, include_orig: bool = False) -> list[str]:
|
||||||
|
"""Build a prioritised list of subtitle language codes for yt-dlp.
|
||||||
|
|
||||||
|
Starts with the exact language, then appends common regional variants
|
||||||
|
(e.g. ``en`` → ``en-US, en-GB, en-AU, …``). ``include_orig`` adds the
|
||||||
|
``<lang>-orig`` tag that YouTube uses for original-language auto-captions.
|
||||||
|
"""
|
||||||
|
langs = [language]
|
||||||
|
variants = _LANGUAGE_REGION_VARIANTS.get(language)
|
||||||
|
if variants:
|
||||||
|
langs.extend(f"{language}-{region}" for region in variants)
|
||||||
|
if include_orig:
|
||||||
|
langs.append(f"{language}-orig")
|
||||||
|
return langs
|
||||||
|
|
||||||
|
|
||||||
def _normalize_caption_mode(mode: str) -> str:
|
def _normalize_caption_mode(mode: str) -> str:
|
||||||
mode = (mode or "").strip()
|
mode = (mode or "").strip()
|
||||||
|
|
@ -141,21 +174,20 @@ def get_opts(
|
||||||
if mode == "manual_only":
|
if mode == "manual_only":
|
||||||
opts["writesubtitles"] = True
|
opts["writesubtitles"] = True
|
||||||
opts["writeautomaticsub"] = False
|
opts["writeautomaticsub"] = False
|
||||||
opts["subtitleslangs"] = [language]
|
opts["subtitleslangs"] = _subtitle_lang_list(language)
|
||||||
elif mode == "auto_only":
|
elif mode == "auto_only":
|
||||||
opts["writesubtitles"] = False
|
opts["writesubtitles"] = False
|
||||||
opts["writeautomaticsub"] = True
|
opts["writeautomaticsub"] = True
|
||||||
# `-orig` captures common YouTube auto-sub tags. The plain language
|
opts["subtitleslangs"] = _subtitle_lang_list(language, include_orig=True)
|
||||||
# fallback keeps behavior useful across other extractors.
|
|
||||||
opts["subtitleslangs"] = [f"{language}-orig", language]
|
|
||||||
elif mode == "prefer_auto":
|
elif mode == "prefer_auto":
|
||||||
opts["writesubtitles"] = True
|
opts["writesubtitles"] = True
|
||||||
opts["writeautomaticsub"] = True
|
opts["writeautomaticsub"] = True
|
||||||
opts["subtitleslangs"] = [f"{language}-orig", language]
|
opts["subtitleslangs"] = _subtitle_lang_list(language, include_orig=True)
|
||||||
else:
|
else:
|
||||||
|
# prefer_manual (default)
|
||||||
opts["writesubtitles"] = True
|
opts["writesubtitles"] = True
|
||||||
opts["writeautomaticsub"] = True
|
opts["writeautomaticsub"] = True
|
||||||
opts["subtitleslangs"] = [language, f"{language}-orig"]
|
opts["subtitleslangs"] = _subtitle_lang_list(language, include_orig=True)
|
||||||
|
|
||||||
opts["postprocessors"] = postprocessors + (
|
opts["postprocessors"] = postprocessors + (
|
||||||
opts["postprocessors"] if "postprocessors" in opts else []
|
opts["postprocessors"] if "postprocessors" in opts else []
|
||||||
|
|
|
||||||
34
app/ytdl.py
34
app/ytdl.py
|
|
@ -704,6 +704,7 @@ class Download:
|
||||||
status = await self.loop.run_in_executor(None, self.status_queue.get)
|
status = await self.loop.run_in_executor(None, self.status_queue.get)
|
||||||
if status is None:
|
if status is None:
|
||||||
log.info(f"Status update finished for: {self.info.title}")
|
log.info(f"Status update finished for: {self.info.title}")
|
||||||
|
|
||||||
return
|
return
|
||||||
if self.paused:
|
if self.paused:
|
||||||
self.info.status = 'paused'
|
self.info.status = 'paused'
|
||||||
|
|
@ -775,6 +776,7 @@ class Download:
|
||||||
str(getattr(self.info, 'format', '')).lower() == 'txt'
|
str(getattr(self.info, 'format', '')).lower() == 'txt'
|
||||||
):
|
):
|
||||||
self.info.filename = rel_path
|
self.info.filename = rel_path
|
||||||
|
continue
|
||||||
|
|
||||||
if 'thumbnail_file' in status:
|
if 'thumbnail_file' in status:
|
||||||
thumbnail_file = status.get('thumbnail_file')
|
thumbnail_file = status.get('thumbnail_file')
|
||||||
|
|
@ -784,6 +786,11 @@ class Download:
|
||||||
self.info.size = os.path.getsize(thumbnail_file)
|
self.info.size = os.path.getsize(thumbnail_file)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# All remaining messages must have a 'status' key
|
||||||
|
if 'status' not in status:
|
||||||
|
log.warning(f"Skipping status update without 'status' key for {self.info.title}: {list(status.keys())}")
|
||||||
|
continue
|
||||||
|
|
||||||
self.info.status = status['status']
|
self.info.status = status['status']
|
||||||
self.info.msg = status.get('msg')
|
self.info.msg = status.get('msg')
|
||||||
if 'download_phase' in status:
|
if 'download_phase' in status:
|
||||||
|
|
@ -1006,6 +1013,33 @@ class DownloadQueue:
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
download.info.status = 'error'
|
download.info.status = 'error'
|
||||||
|
else:
|
||||||
|
# Captions-only downloads that produced no subtitle files should be
|
||||||
|
# reported as an error rather than a silent "success". This can
|
||||||
|
# happen when the requested language (e.g. "en") is unavailable and
|
||||||
|
# yt-dlp silently skips subtitle extraction without raising an
|
||||||
|
# error.
|
||||||
|
if getattr(download.info, 'download_type', '') == 'captions' and not download.info.filename:
|
||||||
|
subtitle_files = getattr(download.info, 'subtitle_files', [])
|
||||||
|
if not subtitle_files:
|
||||||
|
log.warning(
|
||||||
|
f"Captions download for \"{download.info.title}\" produced no files. "
|
||||||
|
f"Requested language: {getattr(download.info, 'subtitle_language', 'en')}, "
|
||||||
|
f"mode: {getattr(download.info, 'subtitle_mode', 'prefer_manual')}"
|
||||||
|
)
|
||||||
|
download.info.status = 'error'
|
||||||
|
download.info.error = (
|
||||||
|
f"No subtitles found for language \"{getattr(download.info, 'subtitle_language', 'en')}\". "
|
||||||
|
f"The video may not have subtitles in the requested language."
|
||||||
|
)
|
||||||
|
# Thumbnail-only downloads that produced no image file should also
|
||||||
|
# be reported as an error for the same reason.
|
||||||
|
elif getattr(download.info, 'download_type', '') == 'thumbnail' and not download.info.filename:
|
||||||
|
log.warning(
|
||||||
|
f"Thumbnail download for \"{download.info.title}\" produced no file."
|
||||||
|
)
|
||||||
|
download.info.status = 'error'
|
||||||
|
download.info.error = "No thumbnail found for this video."
|
||||||
download.close()
|
download.close()
|
||||||
if self.queue.exists(key):
|
if self.queue.exists(key):
|
||||||
self.queue.delete(key)
|
self.queue.delete(key)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue