From cd36e3cee726dea22590509d5af65f2a93450df4 Mon Sep 17 00:00:00 2001 From: ddmoney420 Date: Tue, 3 Mar 2026 15:03:54 -0700 Subject: [PATCH] Add track numbering toggle for playlist downloads (closes #727) Adds a Track Numbering switch in Advanced Options that prepends the playlist index to each track's filename (e.g. "01 - Song.mp3"). Preference saved via cookie. Custom filename still takes priority when both are set. Co-Authored-By: Claude Opus 4.6 --- app/main.py | 2 ++ app/ytdl.py | 12 +++++++++++- ui/src/app/app.html | 9 +++++++++ ui/src/app/app.ts | 12 +++++++++--- ui/src/app/interfaces/download.ts | 1 + ui/src/app/services/downloads.service.ts | 6 +++++- 6 files changed, 37 insertions(+), 5 deletions(-) diff --git a/app/main.py b/app/main.py index 0678544..5fe4bab 100644 --- a/app/main.py +++ b/app/main.py @@ -254,6 +254,7 @@ async def add(request): subtitle_language = post.get('subtitle_language') subtitle_mode = post.get('subtitle_mode') custom_filename = post.get('custom_filename', '') + track_numbering = post.get('track_numbering', False) if custom_name_prefix is None: custom_name_prefix = '' @@ -301,6 +302,7 @@ async def add(request): subtitle_language, subtitle_mode, custom_filename=custom_filename, + track_numbering=track_numbering, ) return web.Response(text=serializer.encode(status)) diff --git a/app/ytdl.py b/app/ytdl.py index 6bc7cd2..8567827 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -143,6 +143,7 @@ class DownloadInfo: subtitle_language="en", subtitle_mode="prefer_manual", custom_filename='', + track_numbering=False, ): 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}' @@ -166,6 +167,7 @@ class DownloadInfo: self.subtitle_mode = subtitle_mode self.subtitle_files = [] self.custom_filename = custom_filename + self.track_numbering = track_numbering class Download: manager = None @@ -620,6 +622,8 @@ class DownloadQueue: for property, value in entry.items(): if property.startswith("playlist"): output = _outtmpl_substitute_field(output, property, value) + if getattr(dl, 'track_numbering', False): + output = f'%(playlist_index)s - {output}' if entry is not None and entry.get('channel_index') is not None: if len(self.config.OUTPUT_TEMPLATE_CHANNEL): output = self.config.OUTPUT_TEMPLATE_CHANNEL @@ -658,6 +662,7 @@ class DownloadQueue: subtitle_mode, already, custom_filename='', + track_numbering=False, _add_gen=None, ): if not entry: @@ -690,6 +695,7 @@ class DownloadQueue: subtitle_mode, already, custom_filename, + track_numbering, _add_gen, ) elif etype == 'playlist' or etype == 'channel': @@ -730,6 +736,7 @@ class DownloadQueue: subtitle_mode, already, custom_filename='', + track_numbering=track_numbering, _add_gen=_add_gen, ) ) @@ -760,6 +767,7 @@ class DownloadQueue: subtitle_language, subtitle_mode, custom_filename=custom_filename, + track_numbering=track_numbering, ) await self.__add_download(dl, auto_start) return {'status': 'ok'} @@ -781,12 +789,13 @@ class DownloadQueue: subtitle_mode="prefer_manual", already=None, custom_filename='', + track_numbering=False, _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=} {custom_filename=}' + f'{subtitle_format=} {subtitle_language=} {subtitle_mode=} {custom_filename=} {track_numbering=}' ) if already is None: _add_gen = self._add_generation @@ -816,6 +825,7 @@ class DownloadQueue: subtitle_mode, already, custom_filename, + track_numbering, _add_gen, ) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index eaa8385..348c0c7 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -219,6 +219,15 @@ [disabled]="addInProgress || downloads.loading"> +
+
+ + +
+
Items Limit diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 26fffb0..18e62a0 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -46,6 +46,7 @@ export class App implements AfterViewInit, OnInit { folder!: string; customNamePrefix!: string; customFilename = ''; + trackNumbering = false; autoStart: boolean; playlistItemLimit!: number; splitByChapters: boolean; @@ -179,6 +180,7 @@ export class App implements AfterViewInit, OnInit { this.subtitleFormat = this.cookieService.get('metube_subtitle_format') || 'srt'; this.subtitleLanguage = this.cookieService.get('metube_subtitle_language') || 'en'; this.subtitleMode = this.cookieService.get('metube_subtitle_mode') || 'prefer_manual'; + this.trackNumbering = this.cookieService.get('metube_track_numbering') === 'true'; 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)) { @@ -373,6 +375,10 @@ export class App implements AfterViewInit, OnInit { this.cookieService.set('metube_subtitle_mode', this.subtitleMode, { expires: 3650 }); } + trackNumberingChanged() { + this.cookieService.set('metube_track_numbering', this.trackNumbering ? 'true' : 'false', { expires: 3650 }); + } + queueSelectionChanged(checked: number) { this.queueDelSelected().nativeElement.disabled = checked == 0; this.queueDownloadSelected().nativeElement.disabled = checked == 0; @@ -428,10 +434,10 @@ 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 + ' customFilename=' + customFilename); + 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 + ' trackNumbering=' + this.trackNumbering); this.addInProgress = true; this.cancelRequested = false; - this.downloads.add(url, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleFormat, subtitleLanguage, subtitleMode, customFilename).subscribe((status: Status) => { + this.downloads.add(url, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleFormat, subtitleLanguage, subtitleMode, customFilename, this.trackNumbering).subscribe((status: Status) => { if (status.status === 'error' && !this.cancelRequested) { alert(`Error adding URL: ${status.msg}`); } else if (status.status !== 'error') { @@ -618,7 +624,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.customFilename) + this.subtitleFormat, this.subtitleLanguage, this.subtitleMode, this.customFilename, this.trackNumbering) .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 dbcce49..d0fc544 100644 --- a/ui/src/app/interfaces/download.ts +++ b/ui/src/app/interfaces/download.ts @@ -14,6 +14,7 @@ export interface Download { subtitle_language?: string; subtitle_mode?: string; custom_filename?: string; + track_numbering?: boolean; 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 0bd83d5..2c7689c 100644 --- a/ui/src/app/services/downloads.service.ts +++ b/ui/src/app/services/downloads.service.ts @@ -121,6 +121,7 @@ export class DownloadsService { subtitleLanguage: string, subtitleMode: string, customFilename: string = '', + trackNumbering: boolean = false, ) { return this.http.post('add', { url: url, @@ -135,7 +136,8 @@ export class DownloadsService { subtitle_format: subtitleFormat, subtitle_language: subtitleLanguage, subtitle_mode: subtitleMode, - custom_filename: customFilename + custom_filename: customFilename, + track_numbering: trackNumbering }).pipe( catchError(this.handleHTTPError) ); @@ -186,6 +188,7 @@ export class DownloadsService { const defaultSubtitleLanguage = 'en'; const defaultSubtitleMode = 'prefer_manual'; const defaultCustomFilename = ''; + const defaultTrackNumbering = false; return new Promise((resolve, reject) => { this.add( @@ -202,6 +205,7 @@ export class DownloadsService { defaultSubtitleLanguage, defaultSubtitleMode, defaultCustomFilename, + defaultTrackNumbering, ) .subscribe({ next: (response) => resolve(response),