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. * __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__: 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. * __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 ### 🌐 Web Server & URLs

View File

@@ -156,6 +156,9 @@ app = web.Application()
sio = socketio.AsyncServer(cors_allowed_origins='*') sio = socketio.AsyncServer(cors_allowed_origins='*')
sio.attach(app, socketio_path=config.URL_PREFIX + 'socket.io') sio.attach(app, socketio_path=config.URL_PREFIX + 'socket.io')
routes = web.RouteTableDef() 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): class Notifier(DownloadQueueNotifier):
async def added(self, dl): async def added(self, dl):
@@ -269,11 +272,17 @@ async def add(request):
subtitle_language = 'en' subtitle_language = 'en'
if subtitle_mode is None: if subtitle_mode is None:
subtitle_mode = 'prefer_manual' 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('\\')): 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') 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_format not in VALID_SUBTITLE_FORMATS:
if subtitle_mode not in valid_subtitle_modes: raise web.HTTPBadRequest(reason=f'subtitle_format must be one of {sorted(VALID_SUBTITLE_FORMATS)}')
raise web.HTTPBadRequest(reason=f'subtitle_mode must be one of {sorted(valid_subtitle_modes)}') 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) playlist_item_limit = int(playlist_item_limit)

View File

@@ -163,6 +163,7 @@ class DownloadInfo:
self.subtitle_format = subtitle_format self.subtitle_format = subtitle_format
self.subtitle_language = subtitle_language self.subtitle_language = subtitle_language
self.subtitle_mode = subtitle_mode self.subtitle_mode = subtitle_mode
self.subtitle_files = []
class Download: class Download:
manager = None manager = None
@@ -379,8 +380,6 @@ class Download:
except OSError as exc: except OSError as exc:
log.debug(f"Could not remove temporary SRT file {subtitle_file}: {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) 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 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) 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)"> ngbTooltip="Maximum number of items to download from a playlist or channel (0 = no limit)">
</div> </div>
</div> </div>
<div class="col-md-4"> @if (format === 'captions') {
<div class="input-group"> <div class="col-md-4">
<span class="input-group-text">Subtitles</span> <div class="input-group">
<select class="form-select" <span class="input-group-text">Subtitles</span>
name="subtitleFormat" <select class="form-select"
[(ngModel)]="subtitleFormat" name="subtitleFormat"
(change)="subtitleFormatChanged()" [(ngModel)]="subtitleFormat"
[disabled]="addInProgress || downloads.loading" (change)="subtitleFormatChanged()"
ngbTooltip="Subtitle output format for captions mode"> [disabled]="addInProgress || downloads.loading"
@for (fmt of subtitleFormats; track fmt.id) { ngbTooltip="Subtitle output format for captions mode">
<option [ngValue]="fmt.id">{{ fmt.text }}</option> @for (fmt of subtitleFormats; track fmt.id) {
} <option [ngValue]="fmt.id">{{ fmt.text }}</option>
</select> }
</select>
</div>
@if (subtitleFormat === 'txt') {
<div class="form-text">TXT is generated from SRT by stripping timestamps and cue numbers.</div>
}
</div> </div>
@if (subtitleFormat === 'txt') { <div class="col-md-4">
<div class="form-text">TXT is generated from SRT by stripping timestamps and cue numbers.</div> <div class="input-group">
} <span class="input-group-text">Language</span>
</div> <input class="form-control"
<div class="col-md-4"> type="text"
<div class="input-group"> list="subtitleLanguageOptions"
<span class="input-group-text">Language</span> name="subtitleLanguage"
<select class="form-select" [(ngModel)]="subtitleLanguage"
name="subtitleLanguage" (change)="subtitleLanguageChanged()"
[(ngModel)]="subtitleLanguage" [disabled]="addInProgress || downloads.loading"
(change)="subtitleLanguageChanged()" placeholder="e.g. en, es, zh-Hans"
[disabled]="addInProgress || downloads.loading" ngbTooltip="Subtitle language (you can type any language code)">
ngbTooltip="Preferred subtitle language for captions mode"> <datalist id="subtitleLanguageOptions">
@for (lang of subtitleLanguages; track lang.id) { @for (lang of subtitleLanguages; track lang.id) {
<option [ngValue]="lang.id">{{ lang.text }}</option> <option [value]="lang.id">{{ lang.text }}</option>
} }
</select> </datalist>
</div>
</div> </div>
</div> <div class="col-md-4">
<div class="col-md-4"> <div class="input-group">
<div class="input-group"> <span class="input-group-text">Subtitle Source</span>
<span class="input-group-text">Subtitle Source</span> <select class="form-select"
<select class="form-select" name="subtitleMode"
name="subtitleMode" [(ngModel)]="subtitleMode"
[(ngModel)]="subtitleMode" (change)="subtitleModeChanged()"
(change)="subtitleModeChanged()" [disabled]="addInProgress || downloads.loading"
[disabled]="addInProgress || downloads.loading" ngbTooltip="Choose manual, auto, or fallback preference for captions mode">
ngbTooltip="Choose manual, auto, or fallback preference for captions mode"> @for (mode of subtitleModes; track mode.id) {
@for (mode of subtitleModes; track mode.id) { <option [ngValue]="mode.id">{{ mode.text }}</option>
<option [ngValue]="mode.id">{{ mode.text }}</option> }
} </select>
</select> </div>
</div> </div>
</div> }
<div class="col-12"> <div class="col-12">
<div class="row g-2 align-items-center"> <div class="row g-2 align-items-center">
<div class="col-auto"> <div class="col-auto">

View File

@@ -108,16 +108,48 @@ export class App implements AfterViewInit, OnInit {
]; ];
subtitleLanguages = [ subtitleLanguages = [
{ id: 'en', text: 'English' }, { 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: 'es', text: 'Spanish' },
{ id: 'et', text: 'Estonian' },
{ id: 'fi', text: 'Finnish' },
{ id: 'fr', text: 'French' }, { id: 'fr', text: 'French' },
{ id: 'de', text: 'German' }, { 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: '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', text: 'Portuguese' },
{ id: 'pt-BR', text: 'Portuguese (Brazil)' },
{ id: 'ro', text: 'Romanian' },
{ id: 'ru', text: 'Russian' }, { 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: 'uk', text: 'Ukrainian' },
{ id: 'ur', text: 'Urdu' },
{ id: 'vi', text: 'Vietnamese' },
{ id: 'ja', text: 'Japanese' }, { id: 'ja', text: 'Japanese' },
{ id: 'ko', text: 'Korean' }, { id: 'ko', text: 'Korean' },
{ id: 'zh-Hans', text: 'Chinese (Simplified)' }, { id: 'zh-Hans', text: 'Chinese (Simplified)' },
{ id: 'zh-Hant', text: 'Chinese (Traditional)' },
]; ];
subtitleModes = [ subtitleModes = [
{ id: 'prefer_manual', text: 'Prefer Manual' }, { 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.subtitleLanguage = this.cookieService.get('metube_subtitle_language') || 'en';
this.subtitleMode = this.cookieService.get('metube_subtitle_mode') || 'prefer_manual'; this.subtitleMode = this.cookieService.get('metube_subtitle_mode') || 'prefer_manual';
const allowedSubtitleFormats = new Set(this.subtitleFormats.map(fmt => fmt.id)); 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)) { if (!allowedSubtitleFormats.has(this.subtitleFormat)) {
this.subtitleFormat = 'srt'; this.subtitleFormat = 'srt';
} }
if (!allowedSubtitleModes.has(this.subtitleMode)) {
this.subtitleMode = 'prefer_manual';
}
this.activeTheme = this.getPreferredTheme(this.cookieService); this.activeTheme = this.getPreferredTheme(this.cookieService);