code review fixes

This commit is contained in:
Alex Shnitman
2026-02-27 12:58:50 +02:00
parent 77da359234
commit 053e41cf52
5 changed files with 100 additions and 51 deletions

View File

@@ -59,7 +59,6 @@ Certain values can be set via environment variables, using the `-e` parameter on
* __OUTPUT_TEMPLATE_CHANNEL__: The template for the filenames of the downloaded videos when downloaded as a channel. Defaults to `%(channel)s/%(title)s.%(ext)s`. When empty, then `OUTPUT_TEMPLATE` is used.
* __YTDL_OPTIONS__: Additional options to pass to yt-dlp in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L222). They roughly correspond to command-line options, though some do not have exact equivalents here. For example, `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. You may find [this script](https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py) helpful for converting from command-line options to `YTDL_OPTIONS`.
* __YTDL_OPTIONS_FILE__: A path to a JSON file that will be loaded and used for populating `YTDL_OPTIONS` above. Please note that if both `YTDL_OPTIONS_FILE` and `YTDL_OPTIONS` are specified, the options in `YTDL_OPTIONS` take precedence. The file will be monitored for changes and reloaded automatically when changes are detected.
* UI format __Captions__: Downloads subtitles/captions only (no media). Subtitle format, language, and source preference are configurable from Advanced Options (defaults: `srt`, `en`, `prefer_manual`). `txt` is generated from `srt` by stripping timestamps and cue numbers.
### 🌐 Web Server & URLs

View File

@@ -156,6 +156,9 @@ app = web.Application()
sio = socketio.AsyncServer(cors_allowed_origins='*')
sio.attach(app, socketio_path=config.URL_PREFIX + 'socket.io')
routes = web.RouteTableDef()
VALID_SUBTITLE_FORMATS = {'srt', 'txt', 'vtt', 'ttml', 'sbv', 'scc', 'dfxp'}
VALID_SUBTITLE_MODES = {'auto_only', 'manual_only', 'prefer_manual', 'prefer_auto'}
SUBTITLE_LANGUAGE_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9-]{0,34}$')
class Notifier(DownloadQueueNotifier):
async def added(self, dl):
@@ -269,11 +272,17 @@ async def add(request):
subtitle_language = 'en'
if subtitle_mode is None:
subtitle_mode = 'prefer_manual'
subtitle_format = str(subtitle_format).strip().lower()
subtitle_language = str(subtitle_language).strip()
subtitle_mode = str(subtitle_mode).strip()
if chapter_template and ('..' in chapter_template or chapter_template.startswith('/') or chapter_template.startswith('\\')):
raise web.HTTPBadRequest(reason='chapter_template must not contain ".." or start with a path separator')
valid_subtitle_modes = {'auto_only', 'manual_only', 'prefer_manual', 'prefer_auto'}
if subtitle_mode not in valid_subtitle_modes:
raise web.HTTPBadRequest(reason=f'subtitle_mode must be one of {sorted(valid_subtitle_modes)}')
if subtitle_format not in VALID_SUBTITLE_FORMATS:
raise web.HTTPBadRequest(reason=f'subtitle_format must be one of {sorted(VALID_SUBTITLE_FORMATS)}')
if not SUBTITLE_LANGUAGE_RE.fullmatch(subtitle_language):
raise web.HTTPBadRequest(reason='subtitle_language must match pattern [A-Za-z0-9-] and be at most 35 characters')
if subtitle_mode not in VALID_SUBTITLE_MODES:
raise web.HTTPBadRequest(reason=f'subtitle_mode must be one of {sorted(VALID_SUBTITLE_MODES)}')
playlist_item_limit = int(playlist_item_limit)

View File

@@ -163,6 +163,7 @@ class DownloadInfo:
self.subtitle_format = subtitle_format
self.subtitle_language = subtitle_language
self.subtitle_mode = subtitle_mode
self.subtitle_files = []
class Download:
manager = None
@@ -379,8 +380,6 @@ class Download:
except OSError as exc:
log.debug(f"Could not remove temporary SRT file {subtitle_file}: {exc}")
if not hasattr(self.info, 'subtitle_files'):
self.info.subtitle_files = []
rel_path = os.path.relpath(subtitle_output_file, self.download_dir)
file_size = os.path.getsize(subtitle_output_file) if os.path.exists(subtitle_output_file) else None
existing = next((sf for sf in self.info.subtitle_files if sf['filename'] == rel_path), None)

View File

@@ -218,54 +218,60 @@
ngbTooltip="Maximum number of items to download from a playlist or channel (0 = no limit)">
</div>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">Subtitles</span>
<select class="form-select"
name="subtitleFormat"
[(ngModel)]="subtitleFormat"
(change)="subtitleFormatChanged()"
[disabled]="addInProgress || downloads.loading"
ngbTooltip="Subtitle output format for captions mode">
@for (fmt of subtitleFormats; track fmt.id) {
<option [ngValue]="fmt.id">{{ fmt.text }}</option>
}
</select>
@if (format === 'captions') {
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">Subtitles</span>
<select class="form-select"
name="subtitleFormat"
[(ngModel)]="subtitleFormat"
(change)="subtitleFormatChanged()"
[disabled]="addInProgress || downloads.loading"
ngbTooltip="Subtitle output format for captions mode">
@for (fmt of subtitleFormats; track fmt.id) {
<option [ngValue]="fmt.id">{{ fmt.text }}</option>
}
</select>
</div>
@if (subtitleFormat === 'txt') {
<div class="form-text">TXT is generated from SRT by stripping timestamps and cue numbers.</div>
}
</div>
@if (subtitleFormat === 'txt') {
<div class="form-text">TXT is generated from SRT by stripping timestamps and cue numbers.</div>
}
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">Language</span>
<select class="form-select"
name="subtitleLanguage"
[(ngModel)]="subtitleLanguage"
(change)="subtitleLanguageChanged()"
[disabled]="addInProgress || downloads.loading"
ngbTooltip="Preferred subtitle language for captions mode">
@for (lang of subtitleLanguages; track lang.id) {
<option [ngValue]="lang.id">{{ lang.text }}</option>
}
</select>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">Language</span>
<input class="form-control"
type="text"
list="subtitleLanguageOptions"
name="subtitleLanguage"
[(ngModel)]="subtitleLanguage"
(change)="subtitleLanguageChanged()"
[disabled]="addInProgress || downloads.loading"
placeholder="e.g. en, es, zh-Hans"
ngbTooltip="Subtitle language (you can type any language code)">
<datalist id="subtitleLanguageOptions">
@for (lang of subtitleLanguages; track lang.id) {
<option [value]="lang.id">{{ lang.text }}</option>
}
</datalist>
</div>
</div>
</div>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">Subtitle Source</span>
<select class="form-select"
name="subtitleMode"
[(ngModel)]="subtitleMode"
(change)="subtitleModeChanged()"
[disabled]="addInProgress || downloads.loading"
ngbTooltip="Choose manual, auto, or fallback preference for captions mode">
@for (mode of subtitleModes; track mode.id) {
<option [ngValue]="mode.id">{{ mode.text }}</option>
}
</select>
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text">Subtitle Source</span>
<select class="form-select"
name="subtitleMode"
[(ngModel)]="subtitleMode"
(change)="subtitleModeChanged()"
[disabled]="addInProgress || downloads.loading"
ngbTooltip="Choose manual, auto, or fallback preference for captions mode">
@for (mode of subtitleModes; track mode.id) {
<option [ngValue]="mode.id">{{ mode.text }}</option>
}
</select>
</div>
</div>
</div>
}
<div class="col-12">
<div class="row g-2 align-items-center">
<div class="col-auto">

View File

@@ -108,16 +108,48 @@ export class App implements AfterViewInit, OnInit {
];
subtitleLanguages = [
{ id: 'en', text: 'English' },
{ id: 'ar', text: 'Arabic' },
{ id: 'bn', text: 'Bengali' },
{ id: 'bg', text: 'Bulgarian' },
{ id: 'ca', text: 'Catalan' },
{ id: 'cs', text: 'Czech' },
{ id: 'da', text: 'Danish' },
{ id: 'nl', text: 'Dutch' },
{ id: 'es', text: 'Spanish' },
{ id: 'et', text: 'Estonian' },
{ id: 'fi', text: 'Finnish' },
{ id: 'fr', text: 'French' },
{ id: 'de', text: 'German' },
{ id: 'el', text: 'Greek' },
{ id: 'he', text: 'Hebrew' },
{ id: 'hi', text: 'Hindi' },
{ id: 'hu', text: 'Hungarian' },
{ id: 'id', text: 'Indonesian' },
{ id: 'it', text: 'Italian' },
{ id: 'lt', text: 'Lithuanian' },
{ id: 'lv', text: 'Latvian' },
{ id: 'ms', text: 'Malay' },
{ id: 'no', text: 'Norwegian' },
{ id: 'pl', text: 'Polish' },
{ id: 'pt', text: 'Portuguese' },
{ id: 'pt-BR', text: 'Portuguese (Brazil)' },
{ id: 'ro', text: 'Romanian' },
{ id: 'ru', text: 'Russian' },
{ id: 'sk', text: 'Slovak' },
{ id: 'sl', text: 'Slovenian' },
{ id: 'sr', text: 'Serbian' },
{ id: 'sv', text: 'Swedish' },
{ id: 'ta', text: 'Tamil' },
{ id: 'te', text: 'Telugu' },
{ id: 'th', text: 'Thai' },
{ id: 'tr', text: 'Turkish' },
{ id: 'uk', text: 'Ukrainian' },
{ id: 'ur', text: 'Urdu' },
{ id: 'vi', text: 'Vietnamese' },
{ id: 'ja', text: 'Japanese' },
{ id: 'ko', text: 'Korean' },
{ id: 'zh-Hans', text: 'Chinese (Simplified)' },
{ id: 'zh-Hant', text: 'Chinese (Traditional)' },
];
subtitleModes = [
{ id: 'prefer_manual', text: 'Prefer Manual' },
@@ -139,9 +171,13 @@ export class App implements AfterViewInit, OnInit {
this.subtitleLanguage = this.cookieService.get('metube_subtitle_language') || 'en';
this.subtitleMode = this.cookieService.get('metube_subtitle_mode') || 'prefer_manual';
const allowedSubtitleFormats = new Set(this.subtitleFormats.map(fmt => fmt.id));
const allowedSubtitleModes = new Set(this.subtitleModes.map(mode => mode.id));
if (!allowedSubtitleFormats.has(this.subtitleFormat)) {
this.subtitleFormat = 'srt';
}
if (!allowedSubtitleModes.has(this.subtitleMode)) {
this.subtitleMode = 'prefer_manual';
}
this.activeTheme = this.getPreferredTheme(this.cookieService);