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

View file

@ -254,6 +254,7 @@ async def add(request):
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', '') custom_filename = post.get('custom_filename', '')
track_numbering = post.get('track_numbering', False)
if custom_name_prefix is None: if custom_name_prefix is None:
custom_name_prefix = '' custom_name_prefix = ''
@ -301,6 +302,7 @@ async def add(request):
subtitle_language, subtitle_language,
subtitle_mode, subtitle_mode,
custom_filename=custom_filename, custom_filename=custom_filename,
track_numbering=track_numbering,
) )
return web.Response(text=serializer.encode(status)) return web.Response(text=serializer.encode(status))

View file

@ -143,6 +143,7 @@ class DownloadInfo:
subtitle_language="en", subtitle_language="en",
subtitle_mode="prefer_manual", subtitle_mode="prefer_manual",
custom_filename='', custom_filename='',
track_numbering=False,
): ):
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}'
@ -166,6 +167,7 @@ class DownloadInfo:
self.subtitle_mode = subtitle_mode self.subtitle_mode = subtitle_mode
self.subtitle_files = [] self.subtitle_files = []
self.custom_filename = custom_filename self.custom_filename = custom_filename
self.track_numbering = track_numbering
class Download: class Download:
manager = None manager = None
@ -620,6 +622,8 @@ class DownloadQueue:
for property, value in entry.items(): for property, value in entry.items():
if property.startswith("playlist"): if property.startswith("playlist"):
output = _outtmpl_substitute_field(output, property, value) 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 entry is not None and entry.get('channel_index') is not None:
if len(self.config.OUTPUT_TEMPLATE_CHANNEL): if len(self.config.OUTPUT_TEMPLATE_CHANNEL):
output = self.config.OUTPUT_TEMPLATE_CHANNEL output = self.config.OUTPUT_TEMPLATE_CHANNEL
@ -658,6 +662,7 @@ class DownloadQueue:
subtitle_mode, subtitle_mode,
already, already,
custom_filename='', custom_filename='',
track_numbering=False,
_add_gen=None, _add_gen=None,
): ):
if not entry: if not entry:
@ -690,6 +695,7 @@ class DownloadQueue:
subtitle_mode, subtitle_mode,
already, already,
custom_filename, custom_filename,
track_numbering,
_add_gen, _add_gen,
) )
elif etype == 'playlist' or etype == 'channel': elif etype == 'playlist' or etype == 'channel':
@ -730,6 +736,7 @@ class DownloadQueue:
subtitle_mode, subtitle_mode,
already, already,
custom_filename='', custom_filename='',
track_numbering=track_numbering,
_add_gen=_add_gen, _add_gen=_add_gen,
) )
) )
@ -760,6 +767,7 @@ class DownloadQueue:
subtitle_language, subtitle_language,
subtitle_mode, subtitle_mode,
custom_filename=custom_filename, custom_filename=custom_filename,
track_numbering=track_numbering,
) )
await self.__add_download(dl, auto_start) await self.__add_download(dl, auto_start)
return {'status': 'ok'} return {'status': 'ok'}
@ -781,12 +789,13 @@ class DownloadQueue:
subtitle_mode="prefer_manual", subtitle_mode="prefer_manual",
already=None, already=None,
custom_filename='', custom_filename='',
track_numbering=False,
_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=} {custom_filename=}' f'{subtitle_format=} {subtitle_language=} {subtitle_mode=} {custom_filename=} {track_numbering=}'
) )
if already is None: if already is None:
_add_gen = self._add_generation _add_gen = self._add_generation
@ -816,6 +825,7 @@ class DownloadQueue:
subtitle_mode, subtitle_mode,
already, already,
custom_filename, custom_filename,
track_numbering,
_add_gen, _add_gen,
) )

View file

@ -219,6 +219,15 @@
[disabled]="addInProgress || downloads.loading"> [disabled]="addInProgress || downloads.loading">
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" role="switch" id="checkbox-track-numbering"
name="trackNumbering" [(ngModel)]="trackNumbering" (change)="trackNumberingChanged()"
[disabled]="addInProgress || downloads.loading"
ngbTooltip="Prepend playlist track number to filename">
<label class="form-check-label" for="checkbox-track-numbering">Track Numbering</label>
</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

@ -46,6 +46,7 @@ export class App implements AfterViewInit, OnInit {
folder!: string; folder!: string;
customNamePrefix!: string; customNamePrefix!: string;
customFilename = ''; customFilename = '';
trackNumbering = false;
autoStart: boolean; autoStart: boolean;
playlistItemLimit!: number; playlistItemLimit!: number;
splitByChapters: boolean; splitByChapters: boolean;
@ -179,6 +180,7 @@ export class App implements AfterViewInit, OnInit {
this.subtitleFormat = this.cookieService.get('metube_subtitle_format') || 'srt'; this.subtitleFormat = this.cookieService.get('metube_subtitle_format') || 'srt';
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';
this.trackNumbering = this.cookieService.get('metube_track_numbering') === 'true';
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)); const allowedSubtitleModes = new Set(this.subtitleModes.map(mode => mode.id));
if (!allowedSubtitleFormats.has(this.subtitleFormat)) { 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 }); 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) { queueSelectionChanged(checked: number) {
this.queueDelSelected().nativeElement.disabled = checked == 0; this.queueDelSelected().nativeElement.disabled = checked == 0;
this.queueDownloadSelected().nativeElement.disabled = checked == 0; this.queueDownloadSelected().nativeElement.disabled = checked == 0;
@ -428,10 +434,10 @@ 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 + ' 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.addInProgress = true;
this.cancelRequested = false; 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) { 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') {
@ -618,7 +624,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.customFilename) this.subtitleFormat, this.subtitleLanguage, this.subtitleMode, this.customFilename, this.trackNumbering)
.subscribe({ .subscribe({
next: (status: Status) => { next: (status: Status) => {
if (status.status === 'error') { if (status.status === 'error') {

View file

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

View file

@ -121,6 +121,7 @@ export class DownloadsService {
subtitleLanguage: string, subtitleLanguage: string,
subtitleMode: string, subtitleMode: string,
customFilename: string = '', customFilename: string = '',
trackNumbering: boolean = false,
) { ) {
return this.http.post<Status>('add', { return this.http.post<Status>('add', {
url: url, url: url,
@ -135,7 +136,8 @@ export class DownloadsService {
subtitle_format: subtitleFormat, subtitle_format: subtitleFormat,
subtitle_language: subtitleLanguage, subtitle_language: subtitleLanguage,
subtitle_mode: subtitleMode, subtitle_mode: subtitleMode,
custom_filename: customFilename custom_filename: customFilename,
track_numbering: trackNumbering
}).pipe( }).pipe(
catchError(this.handleHTTPError) catchError(this.handleHTTPError)
); );
@ -186,6 +188,7 @@ export class DownloadsService {
const defaultSubtitleLanguage = 'en'; const defaultSubtitleLanguage = 'en';
const defaultSubtitleMode = 'prefer_manual'; const defaultSubtitleMode = 'prefer_manual';
const defaultCustomFilename = ''; const defaultCustomFilename = '';
const defaultTrackNumbering = false;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.add( this.add(
@ -202,6 +205,7 @@ export class DownloadsService {
defaultSubtitleLanguage, defaultSubtitleLanguage,
defaultSubtitleMode, defaultSubtitleMode,
defaultCustomFilename, defaultCustomFilename,
defaultTrackNumbering,
) )
.subscribe({ .subscribe({
next: (response) => resolve(response), next: (response) => resolve(response),