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_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))

View file

@ -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,
)

View file

@ -219,6 +219,15 @@
[disabled]="addInProgress || downloads.loading">
</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="input-group">
<span class="input-group-text">Items Limit</span>

View file

@ -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') {

View file

@ -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;

View file

@ -121,6 +121,7 @@ export class DownloadsService {
subtitleLanguage: string,
subtitleMode: string,
customFilename: string = '',
trackNumbering: boolean = false,
) {
return this.http.post<Status>('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),