From 36b397ae93eace9caf4084b43ffadf14c1581ccc Mon Sep 17 00:00:00 2001 From: TonyBlu Date: Mon, 25 May 2026 21:16:10 +0800 Subject: [PATCH] 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. --- app/dl_formats.py | 44 ++++++++++++++++++++++++++++++++++++++------ app/ytdl.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/app/dl_formats.py b/app/dl_formats.py index 7933451..2e66c8e 100644 --- a/app/dl_formats.py +++ b/app/dl_formats.py @@ -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 + ``-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 [] diff --git a/app/ytdl.py b/app/ytdl.py index 4eb5526..65656f3 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -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)