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:
TonyBlu 2026-05-25 21:16:10 +08:00
parent e639caa21d
commit 36b397ae93
2 changed files with 72 additions and 6 deletions

View file

@ -10,6 +10,39 @@ CODEC_FILTER_MAP = {
'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:
mode = (mode or "").strip()
@ -141,21 +174,20 @@ def get_opts(
if mode == "manual_only":
opts["writesubtitles"] = True
opts["writeautomaticsub"] = False
opts["subtitleslangs"] = [language]
opts["subtitleslangs"] = _subtitle_lang_list(language)
elif mode == "auto_only":
opts["writesubtitles"] = False
opts["writeautomaticsub"] = True
# `-orig` captures common YouTube auto-sub tags. The plain language
# fallback keeps behavior useful across other extractors.
opts["subtitleslangs"] = [f"{language}-orig", language]
opts["subtitleslangs"] = _subtitle_lang_list(language, include_orig=True)
elif mode == "prefer_auto":
opts["writesubtitles"] = True
opts["writeautomaticsub"] = True
opts["subtitleslangs"] = [f"{language}-orig", language]
opts["subtitleslangs"] = _subtitle_lang_list(language, include_orig=True)
else:
# prefer_manual (default)
opts["writesubtitles"] = True
opts["writeautomaticsub"] = True
opts["subtitleslangs"] = [language, f"{language}-orig"]
opts["subtitleslangs"] = _subtitle_lang_list(language, include_orig=True)
opts["postprocessors"] = postprocessors + (
opts["postprocessors"] if "postprocessors" in opts else []

View file

@ -704,6 +704,7 @@ class Download:
status = await self.loop.run_in_executor(None, self.status_queue.get)
if status is None:
log.info(f"Status update finished for: {self.info.title}")
return
if self.paused:
self.info.status = 'paused'
@ -775,6 +776,7 @@ class Download:
str(getattr(self.info, 'format', '')).lower() == 'txt'
):
self.info.filename = rel_path
continue
if 'thumbnail_file' in status:
thumbnail_file = status.get('thumbnail_file')
@ -784,6 +786,11 @@ class Download:
self.info.size = os.path.getsize(thumbnail_file)
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.msg = status.get('msg')
if 'download_phase' in status:
@ -1006,6 +1013,33 @@ class DownloadQueue:
except OSError:
pass
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()
if self.queue.exists(key):
self.queue.delete(key)