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:
parent
c2d35afc3e
commit
feca39689e
6 changed files with 37 additions and 6 deletions
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
15
app/ytdl.py
15
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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,14 @@
|
|||
ngbTooltip="Add a prefix to downloaded filenames">
|
||||
</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="input-group">
|
||||
<span class="input-group-text">Items Limit</span>
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export class DownloadsService {
|
|||
subtitleFormat: string,
|
||||
subtitleLanguage: string,
|
||||
subtitleMode: string,
|
||||
customFilename: string = '',
|
||||
) {
|
||||
return this.http.post<Status>('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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue