feat(captions): P1/P2 improvements - region variant fallback, DownloadInfo init, subtitle_files UI
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).
This commit is contained in:
parent
b31f4e2fea
commit
6d890021a6
6 changed files with 76 additions and 6 deletions
|
|
@ -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
|
||||
``<lang>-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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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", {})
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -904,7 +904,36 @@
|
|||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if (entry[1].subtitle_files && entry[1].subtitle_files.length > 0) {
|
||||
@for (subtitleFile of entry[1].subtitle_files; track subtitleFile.filename) {
|
||||
<tr [class.disabled]='entry[1].deleting'>
|
||||
<td></td>
|
||||
<td>
|
||||
<div style="padding-left: 2rem;">
|
||||
<fa-icon [icon]="faClosedCaptioning" class="text-info me-2" />
|
||||
<a href="{{buildChapterDownloadLink(entry[1], subtitleFile.filename)}}" target="_blank" [attr.aria-label]="'Open subtitle file ' + getChapterFileName(subtitleFile.filename)">{{
|
||||
getChapterFileName(subtitleFile.filename) }}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>
|
||||
@if (subtitleFile.size) {
|
||||
<span>{{ subtitleFile.size | fileSize }}</span>
|
||||
}
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<div class="d-flex">
|
||||
<a href="{{buildChapterDownloadLink(entry[1], subtitleFile.filename)}}" download [attr.aria-label]="'Download subtitle file ' + getChapterFileName(subtitleFile.filename)"
|
||||
class="btn btn-link"><fa-icon [icon]="faDownload" /></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -31,4 +31,5 @@ export interface Download {
|
|||
error?: string;
|
||||
deleting?: boolean;
|
||||
chapter_files?: { filename: string, size: number }[];
|
||||
subtitle_files?: { filename: string, size: number }[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue