From feca39689eb105d05d83c258b40f066565c784be Mon Sep 17 00:00:00 2001 From: ddmoney420 Date: Tue, 3 Mar 2026 14:56:11 -0700 Subject: [PATCH] Add custom filename field for downloads (closes #56) Adds a Filename text input in Advanced Options that overrides the output template with a user-specified name (extension auto-appended). Cleared after each download. Ignored for playlist entries so each keeps its own title. Co-Authored-By: Claude Opus 4.6 --- app/main.py | 2 ++ app/ytdl.py | 15 +++++++++++++-- ui/src/app/app.html | 8 ++++++++ ui/src/app/app.ts | 11 ++++++++--- ui/src/app/interfaces/download.ts | 1 + ui/src/app/services/downloads.service.ts | 6 +++++- 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index 98879cc..0678544 100644 --- a/app/main.py +++ b/app/main.py @@ -253,6 +253,7 @@ async def add(request): subtitle_format = post.get('subtitle_format') subtitle_language = post.get('subtitle_language') subtitle_mode = post.get('subtitle_mode') + custom_filename = post.get('custom_filename', '') if custom_name_prefix is None: custom_name_prefix = '' @@ -299,6 +300,7 @@ async def add(request): subtitle_format, subtitle_language, subtitle_mode, + custom_filename=custom_filename, ) return web.Response(text=serializer.encode(status)) diff --git a/app/ytdl.py b/app/ytdl.py index bc84b74..6bc7cd2 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -142,6 +142,7 @@ class DownloadInfo: subtitle_format="srt", subtitle_language="en", subtitle_mode="prefer_manual", + custom_filename='', ): self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}' @@ -164,6 +165,7 @@ class DownloadInfo: self.subtitle_language = subtitle_language self.subtitle_mode = subtitle_mode self.subtitle_files = [] + self.custom_filename = custom_filename class Download: manager = None @@ -624,6 +626,9 @@ class DownloadQueue: for property, value in entry.items(): if property.startswith("channel"): output = _outtmpl_substitute_field(output, property, value) + # Custom filename override: replaces entire output template + if getattr(dl, 'custom_filename', ''): + output = f'{dl.custom_filename}.%(ext)s' ytdl_options = dict(self.config.YTDL_OPTIONS) playlist_item_limit = getattr(dl, 'playlist_item_limit', 0) if playlist_item_limit > 0: @@ -652,6 +657,7 @@ class DownloadQueue: subtitle_language, subtitle_mode, already, + custom_filename='', _add_gen=None, ): if not entry: @@ -683,6 +689,7 @@ class DownloadQueue: subtitle_language, subtitle_mode, already, + custom_filename, _add_gen, ) elif etype == 'playlist' or etype == 'channel': @@ -722,7 +729,8 @@ class DownloadQueue: subtitle_language, subtitle_mode, already, - _add_gen, + custom_filename='', + _add_gen=_add_gen, ) ) if any(res['status'] == 'error' for res in results): @@ -751,6 +759,7 @@ class DownloadQueue: subtitle_format, subtitle_language, subtitle_mode, + custom_filename=custom_filename, ) await self.__add_download(dl, auto_start) return {'status': 'ok'} @@ -771,12 +780,13 @@ class DownloadQueue: subtitle_language="en", subtitle_mode="prefer_manual", already=None, + custom_filename='', _add_gen=None, ): log.info( f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} ' f'{playlist_item_limit=} {auto_start=} {split_by_chapters=} {chapter_template=} ' - f'{subtitle_format=} {subtitle_language=} {subtitle_mode=}' + f'{subtitle_format=} {subtitle_language=} {subtitle_mode=} {custom_filename=}' ) if already is None: _add_gen = self._add_generation @@ -805,6 +815,7 @@ class DownloadQueue: subtitle_language, subtitle_mode, already, + custom_filename, _add_gen, ) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 42adcc9..eaa8385 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -211,6 +211,14 @@ ngbTooltip="Add a prefix to downloaded filenames"> +
+
+ Filename + +
+
Items Limit diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 26cca3a..26fffb0 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -45,6 +45,7 @@ export class App implements AfterViewInit, OnInit { format: string; folder!: string; customNamePrefix!: string; + customFilename = ''; autoStart: boolean; playlistItemLimit!: number; splitByChapters: boolean; @@ -405,6 +406,7 @@ export class App implements AfterViewInit, OnInit { subtitleFormat?: string, subtitleLanguage?: string, subtitleMode?: string, + customFilename?: string, ) { url = url ?? this.addUrl quality = quality ?? this.quality @@ -418,6 +420,7 @@ export class App implements AfterViewInit, OnInit { subtitleFormat = subtitleFormat ?? this.subtitleFormat subtitleLanguage = subtitleLanguage ?? this.subtitleLanguage subtitleMode = subtitleMode ?? this.subtitleMode + customFilename = customFilename ?? this.customFilename // Validate chapter template if chapter splitting is enabled if (splitByChapters && !chapterTemplate.includes('%(section_number)')) { @@ -425,14 +428,15 @@ export class App implements AfterViewInit, OnInit { return; } - console.debug('Downloading: url=' + url + ' quality=' + quality + ' format=' + format + ' folder=' + folder + ' customNamePrefix=' + customNamePrefix + ' playlistItemLimit=' + playlistItemLimit + ' autoStart=' + autoStart + ' splitByChapters=' + splitByChapters + ' chapterTemplate=' + chapterTemplate + ' subtitleFormat=' + subtitleFormat + ' subtitleLanguage=' + subtitleLanguage + ' subtitleMode=' + subtitleMode); + console.debug('Downloading: url=' + url + ' quality=' + quality + ' format=' + format + ' folder=' + folder + ' customNamePrefix=' + customNamePrefix + ' playlistItemLimit=' + playlistItemLimit + ' autoStart=' + autoStart + ' splitByChapters=' + splitByChapters + ' chapterTemplate=' + chapterTemplate + ' subtitleFormat=' + subtitleFormat + ' subtitleLanguage=' + subtitleLanguage + ' subtitleMode=' + subtitleMode + ' customFilename=' + customFilename); this.addInProgress = true; this.cancelRequested = false; - this.downloads.add(url, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleFormat, subtitleLanguage, subtitleMode).subscribe((status: Status) => { + this.downloads.add(url, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleFormat, subtitleLanguage, subtitleMode, customFilename).subscribe((status: Status) => { if (status.status === 'error' && !this.cancelRequested) { alert(`Error adding URL: ${status.msg}`); } else if (status.status !== 'error') { this.addUrl = ''; + this.customFilename = ''; } this.addInProgress = false; this.cancelRequested = false; @@ -466,6 +470,7 @@ export class App implements AfterViewInit, OnInit { download.subtitle_format, download.subtitle_language, download.subtitle_mode, + download.custom_filename, ); this.downloads.delById('done', [key]).subscribe(); } @@ -613,7 +618,7 @@ export class App implements AfterViewInit, OnInit { // Now pass the selected quality, format, folder, etc. to the add() method this.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix, this.playlistItemLimit, this.autoStart, this.splitByChapters, this.chapterTemplate, - this.subtitleFormat, this.subtitleLanguage, this.subtitleMode) + this.subtitleFormat, this.subtitleLanguage, this.subtitleMode, this.customFilename) .subscribe({ next: (status: Status) => { if (status.status === 'error') { diff --git a/ui/src/app/interfaces/download.ts b/ui/src/app/interfaces/download.ts index 6b41625..dbcce49 100644 --- a/ui/src/app/interfaces/download.ts +++ b/ui/src/app/interfaces/download.ts @@ -13,6 +13,7 @@ export interface Download { subtitle_format?: string; subtitle_language?: string; subtitle_mode?: string; + custom_filename?: string; status: string; msg: string; percent: number; diff --git a/ui/src/app/services/downloads.service.ts b/ui/src/app/services/downloads.service.ts index e94ca9b..0bd83d5 100644 --- a/ui/src/app/services/downloads.service.ts +++ b/ui/src/app/services/downloads.service.ts @@ -120,6 +120,7 @@ export class DownloadsService { subtitleFormat: string, subtitleLanguage: string, subtitleMode: string, + customFilename: string = '', ) { return this.http.post('add', { url: url, @@ -133,7 +134,8 @@ export class DownloadsService { chapter_template: chapterTemplate, subtitle_format: subtitleFormat, subtitle_language: subtitleLanguage, - subtitle_mode: subtitleMode + subtitle_mode: subtitleMode, + custom_filename: customFilename }).pipe( catchError(this.handleHTTPError) ); @@ -183,6 +185,7 @@ export class DownloadsService { const defaultSubtitleFormat = 'srt'; const defaultSubtitleLanguage = 'en'; const defaultSubtitleMode = 'prefer_manual'; + const defaultCustomFilename = ''; return new Promise((resolve, reject) => { this.add( @@ -198,6 +201,7 @@ export class DownloadsService { defaultSubtitleFormat, defaultSubtitleLanguage, defaultSubtitleMode, + defaultCustomFilename, ) .subscribe({ next: (response) => resolve(response),