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 <noreply@anthropic.com>
This commit is contained in:
ddmoney420 2026-03-03 14:56:11 -07:00
parent c2d35afc3e
commit feca39689e
6 changed files with 37 additions and 6 deletions

View file

@ -253,6 +253,7 @@ async def add(request):
subtitle_format = post.get('subtitle_format') subtitle_format = post.get('subtitle_format')
subtitle_language = post.get('subtitle_language') subtitle_language = post.get('subtitle_language')
subtitle_mode = post.get('subtitle_mode') subtitle_mode = post.get('subtitle_mode')
custom_filename = post.get('custom_filename', '')
if custom_name_prefix is None: if custom_name_prefix is None:
custom_name_prefix = '' custom_name_prefix = ''
@ -299,6 +300,7 @@ async def add(request):
subtitle_format, subtitle_format,
subtitle_language, subtitle_language,
subtitle_mode, subtitle_mode,
custom_filename=custom_filename,
) )
return web.Response(text=serializer.encode(status)) return web.Response(text=serializer.encode(status))

View file

@ -142,6 +142,7 @@ class DownloadInfo:
subtitle_format="srt", subtitle_format="srt",
subtitle_language="en", subtitle_language="en",
subtitle_mode="prefer_manual", subtitle_mode="prefer_manual",
custom_filename='',
): ):
self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' 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}' 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_language = subtitle_language
self.subtitle_mode = subtitle_mode self.subtitle_mode = subtitle_mode
self.subtitle_files = [] self.subtitle_files = []
self.custom_filename = custom_filename
class Download: class Download:
manager = None manager = None
@ -624,6 +626,9 @@ class DownloadQueue:
for property, value in entry.items(): for property, value in entry.items():
if property.startswith("channel"): if property.startswith("channel"):
output = _outtmpl_substitute_field(output, property, value) 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) ytdl_options = dict(self.config.YTDL_OPTIONS)
playlist_item_limit = getattr(dl, 'playlist_item_limit', 0) playlist_item_limit = getattr(dl, 'playlist_item_limit', 0)
if playlist_item_limit > 0: if playlist_item_limit > 0:
@ -652,6 +657,7 @@ class DownloadQueue:
subtitle_language, subtitle_language,
subtitle_mode, subtitle_mode,
already, already,
custom_filename='',
_add_gen=None, _add_gen=None,
): ):
if not entry: if not entry:
@ -683,6 +689,7 @@ class DownloadQueue:
subtitle_language, subtitle_language,
subtitle_mode, subtitle_mode,
already, already,
custom_filename,
_add_gen, _add_gen,
) )
elif etype == 'playlist' or etype == 'channel': elif etype == 'playlist' or etype == 'channel':
@ -722,7 +729,8 @@ class DownloadQueue:
subtitle_language, subtitle_language,
subtitle_mode, subtitle_mode,
already, already,
_add_gen, custom_filename='',
_add_gen=_add_gen,
) )
) )
if any(res['status'] == 'error' for res in results): if any(res['status'] == 'error' for res in results):
@ -751,6 +759,7 @@ class DownloadQueue:
subtitle_format, subtitle_format,
subtitle_language, subtitle_language,
subtitle_mode, subtitle_mode,
custom_filename=custom_filename,
) )
await self.__add_download(dl, auto_start) await self.__add_download(dl, auto_start)
return {'status': 'ok'} return {'status': 'ok'}
@ -771,12 +780,13 @@ class DownloadQueue:
subtitle_language="en", subtitle_language="en",
subtitle_mode="prefer_manual", subtitle_mode="prefer_manual",
already=None, already=None,
custom_filename='',
_add_gen=None, _add_gen=None,
): ):
log.info( log.info(
f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} ' f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} '
f'{playlist_item_limit=} {auto_start=} {split_by_chapters=} {chapter_template=} ' 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: if already is None:
_add_gen = self._add_generation _add_gen = self._add_generation
@ -805,6 +815,7 @@ class DownloadQueue:
subtitle_language, subtitle_language,
subtitle_mode, subtitle_mode,
already, already,
custom_filename,
_add_gen, _add_gen,
) )

View file

@ -211,6 +211,14 @@
ngbTooltip="Add a prefix to downloaded filenames"> ngbTooltip="Add a prefix to downloaded filenames">
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text" ngbTooltip="Override the output filename (without extension)">Filename</span>
<input type="text" class="form-control" placeholder="Leave empty for default"
name="customFilename" [(ngModel)]="customFilename"
[disabled]="addInProgress || downloads.loading">
</div>
</div>
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
<span class="input-group-text">Items Limit</span> <span class="input-group-text">Items Limit</span>

View file

@ -45,6 +45,7 @@ export class App implements AfterViewInit, OnInit {
format: string; format: string;
folder!: string; folder!: string;
customNamePrefix!: string; customNamePrefix!: string;
customFilename = '';
autoStart: boolean; autoStart: boolean;
playlistItemLimit!: number; playlistItemLimit!: number;
splitByChapters: boolean; splitByChapters: boolean;
@ -405,6 +406,7 @@ export class App implements AfterViewInit, OnInit {
subtitleFormat?: string, subtitleFormat?: string,
subtitleLanguage?: string, subtitleLanguage?: string,
subtitleMode?: string, subtitleMode?: string,
customFilename?: string,
) { ) {
url = url ?? this.addUrl url = url ?? this.addUrl
quality = quality ?? this.quality quality = quality ?? this.quality
@ -418,6 +420,7 @@ export class App implements AfterViewInit, OnInit {
subtitleFormat = subtitleFormat ?? this.subtitleFormat subtitleFormat = subtitleFormat ?? this.subtitleFormat
subtitleLanguage = subtitleLanguage ?? this.subtitleLanguage subtitleLanguage = subtitleLanguage ?? this.subtitleLanguage
subtitleMode = subtitleMode ?? this.subtitleMode subtitleMode = subtitleMode ?? this.subtitleMode
customFilename = customFilename ?? this.customFilename
// Validate chapter template if chapter splitting is enabled // Validate chapter template if chapter splitting is enabled
if (splitByChapters && !chapterTemplate.includes('%(section_number)')) { if (splitByChapters && !chapterTemplate.includes('%(section_number)')) {
@ -425,14 +428,15 @@ export class App implements AfterViewInit, OnInit {
return; 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.addInProgress = true;
this.cancelRequested = false; 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) { if (status.status === 'error' && !this.cancelRequested) {
alert(`Error adding URL: ${status.msg}`); alert(`Error adding URL: ${status.msg}`);
} else if (status.status !== 'error') { } else if (status.status !== 'error') {
this.addUrl = ''; this.addUrl = '';
this.customFilename = '';
} }
this.addInProgress = false; this.addInProgress = false;
this.cancelRequested = false; this.cancelRequested = false;
@ -466,6 +470,7 @@ export class App implements AfterViewInit, OnInit {
download.subtitle_format, download.subtitle_format,
download.subtitle_language, download.subtitle_language,
download.subtitle_mode, download.subtitle_mode,
download.custom_filename,
); );
this.downloads.delById('done', [key]).subscribe(); 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 // 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.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix,
this.playlistItemLimit, this.autoStart, this.splitByChapters, this.chapterTemplate, this.playlistItemLimit, this.autoStart, this.splitByChapters, this.chapterTemplate,
this.subtitleFormat, this.subtitleLanguage, this.subtitleMode) this.subtitleFormat, this.subtitleLanguage, this.subtitleMode, this.customFilename)
.subscribe({ .subscribe({
next: (status: Status) => { next: (status: Status) => {
if (status.status === 'error') { if (status.status === 'error') {

View file

@ -13,6 +13,7 @@ export interface Download {
subtitle_format?: string; subtitle_format?: string;
subtitle_language?: string; subtitle_language?: string;
subtitle_mode?: string; subtitle_mode?: string;
custom_filename?: string;
status: string; status: string;
msg: string; msg: string;
percent: number; percent: number;

View file

@ -120,6 +120,7 @@ export class DownloadsService {
subtitleFormat: string, subtitleFormat: string,
subtitleLanguage: string, subtitleLanguage: string,
subtitleMode: string, subtitleMode: string,
customFilename: string = '',
) { ) {
return this.http.post<Status>('add', { return this.http.post<Status>('add', {
url: url, url: url,
@ -133,7 +134,8 @@ export class DownloadsService {
chapter_template: chapterTemplate, chapter_template: chapterTemplate,
subtitle_format: subtitleFormat, subtitle_format: subtitleFormat,
subtitle_language: subtitleLanguage, subtitle_language: subtitleLanguage,
subtitle_mode: subtitleMode subtitle_mode: subtitleMode,
custom_filename: customFilename
}).pipe( }).pipe(
catchError(this.handleHTTPError) catchError(this.handleHTTPError)
); );
@ -183,6 +185,7 @@ export class DownloadsService {
const defaultSubtitleFormat = 'srt'; const defaultSubtitleFormat = 'srt';
const defaultSubtitleLanguage = 'en'; const defaultSubtitleLanguage = 'en';
const defaultSubtitleMode = 'prefer_manual'; const defaultSubtitleMode = 'prefer_manual';
const defaultCustomFilename = '';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.add( this.add(
@ -198,6 +201,7 @@ export class DownloadsService {
defaultSubtitleFormat, defaultSubtitleFormat,
defaultSubtitleLanguage, defaultSubtitleLanguage,
defaultSubtitleMode, defaultSubtitleMode,
defaultCustomFilename,
) )
.subscribe({ .subscribe({
next: (response) => resolve(response), next: (response) => resolve(response),