From 6d890021a63fb9955d288df5cee03194239022fe Mon Sep 17 00:00:00 2001 From: TonyBlu Date: Mon, 25 May 2026 21:48:27 +0800 Subject: [PATCH] feat(captions): P1/P2 improvements - region variant fallback, DownloadInfo init, subtitle_files UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 fixes: - _subtitle_lang_list now extracts base language from qualified tags (e.g. zh-Hans → zh base) so regional variants are still added as fallbacks. Original input is preserved as first priority. - DownloadInfo.__init__ now explicitly initializes filename=None and chapter_files=[] to avoid AttributeError on dynamic attribute access. - __setstate__ adds filename=None migration for old persistent data. - Frontend Download interface gains subtitle_files field; completed downloads show each subtitle file with download link and CC icon. P2 improvements: - _LANGUAGE_REGION_VARIANTS expanded from 10 to 30 languages covering it/ja/ko/hi/th/vi/id/pl/uk/ru/cs/sv/da/no/fi/tr/el/he/hu/bn/ta. - Languages with no regional variants use empty tuple (they still get -orig suffix when include_orig=True). --- app/dl_formats.py | 41 ++++++++++++++++++++++++++++--- app/tests/test_dl_formats.py | 2 +- app/ytdl.py | 4 +++ ui/src/app/app.html | 31 ++++++++++++++++++++++- ui/src/app/app.ts | 3 ++- ui/src/app/interfaces/download.ts | 1 + 6 files changed, 76 insertions(+), 6 deletions(-) diff --git a/app/dl_formats.py b/app/dl_formats.py index 2e66c8e..7ae2181 100644 --- a/app/dl_formats.py +++ b/app/dl_formats.py @@ -25,6 +25,27 @@ _LANGUAGE_REGION_VARIANTS = { "nl": ("NL", "BE"), "ro": ("RO", "MD"), "ms": ("MY", "BN", "SG"), + "it": ("IT", "SM"), + "ja": (), + "ko": (), + "hi": (), + "th": (), + "vi": (), + "id": (), + "pl": (), + "uk": (), + "ru": ("RU", "BY", "KZ", "UA"), + "cs": (), + "sv": ("SE", "FI"), + "da": ("DK",), + "no": ("NO", "NB", "NN"), + "fi": (), + "tr": (), + "el": ("GR", "CY"), + "he": (), + "hu": (), + "bn": ("BD", "IN"), + "ta": ("IN", "SG", "LK", "MY"), } @@ -34,13 +55,27 @@ def _subtitle_lang_list(language: str, include_orig: bool = False) -> list[str]: 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. + + Handles user input that already contains a region or script subtag (e.g. + ``zh-Hans``, ``pt-BR``): extracts the base language (``zh``, ``pt``) so + regional variants for that base language are still included as fallbacks. """ langs = [language] - variants = _LANGUAGE_REGION_VARIANTS.get(language) + # Extract base language (e.g. "zh-Hans" → "zh", "en-GB" → "en") + base = language.split('-')[0] + # If user provided a qualified tag, also include the base form as fallback + if base != language: + langs.append(base) + variants = _LANGUAGE_REGION_VARIANTS.get(base) if variants: - langs.extend(f"{language}-{region}" for region in variants) + for region in variants: + variant = f"{base}-{region}" + if variant not in langs: + langs.append(variant) if include_orig: - langs.append(f"{language}-orig") + orig_tag = f"{base}-orig" + if orig_tag not in langs: + langs.append(orig_tag) return langs diff --git a/app/tests/test_dl_formats.py b/app/tests/test_dl_formats.py index 01af602..c424832 100644 --- a/app/tests/test_dl_formats.py +++ b/app/tests/test_dl_formats.py @@ -114,7 +114,7 @@ class DlFormatsTests(unittest.TestCase): opts = get_opts( "captions", "auto", "srt", "best", {}, subtitle_language="it", subtitle_mode="prefer_manual" ) - self.assertEqual(opts["subtitleslangs"], ["it", "it-orig"]) + self.assertEqual(opts["subtitleslangs"], ["it", "it-IT", "it-SM", "it-orig"]) def test_get_opts_captions_txt_maps_to_srt_format(self): opts = get_opts("captions", "auto", "txt", "best", {}) diff --git a/app/ytdl.py b/app/ytdl.py index 257d581..6552c25 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -210,6 +210,7 @@ class DownloadInfo: self.size = None self.timestamp = time.time_ns() self.error = error + self.filename = None # Strip non-pickleable values (generators, iterators, locks, etc.) for shelve self.entry = _sanitize_entry_for_pickle(entry) if entry is not None else None self.playlist_item_limit = playlist_item_limit @@ -221,6 +222,7 @@ class DownloadInfo: self.ytdl_options_overrides = dict(ytdl_options_overrides or {}) self.clip_start = clip_start self.clip_end = clip_end + self.chapter_files = [] self.subtitle_files = [] def __setstate__(self, state): @@ -289,6 +291,8 @@ class DownloadInfo: self.subtitle_files = [] if not hasattr(self, "chapter_files"): self.chapter_files = [] + if not hasattr(self, "filename"): + self.filename = None if not hasattr(self, "clip_start"): self.clip_start = None if not hasattr(self, "clip_end"): diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 86c3dd0..8e2a0e9 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -904,7 +904,36 @@ } - } + } + @if (entry[1].subtitle_files && entry[1].subtitle_files.length > 0) { + @for (subtitleFile of entry[1].subtitle_files; track subtitleFile.filename) { + + + +
+ + {{ + getChapterFileName(subtitleFile.filename) }} +
+ + + + + + @if (subtitleFile.size) { + {{ subtitleFile.size | fileSize }} + } + + + +
+ +
+ + + } + } } diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 8c5398a..a08d247 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -7,7 +7,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgSelectModule } from '@ng-select/ng-select'; -import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload, faPause, faPlay } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload, faPause, faPlay, faClosedCaptioning } from '@fortawesome/free-solid-svg-icons'; import { faGithub } from '@fortawesome/free-brands-svg-icons'; import { CookieService } from 'ngx-cookie-service'; import { AddDownloadPayload, DownloadsService } from './services/downloads.service'; @@ -188,6 +188,7 @@ export class App implements AfterViewInit, OnInit, OnDestroy { faUpload = faUpload; faPause = faPause; faPlay = faPlay; + faClosedCaptioning = faClosedCaptioning; subtitleLanguages = [ { id: 'en', text: 'English' }, { id: 'ar', text: 'Arabic' }, diff --git a/ui/src/app/interfaces/download.ts b/ui/src/app/interfaces/download.ts index 9402982..75ec874 100644 --- a/ui/src/app/interfaces/download.ts +++ b/ui/src/app/interfaces/download.ts @@ -31,4 +31,5 @@ export interface Download { error?: string; deleting?: boolean; chapter_files?: { filename: string, size: number }[]; + subtitle_files?: { filename: string, size: number }[]; }