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:
parent
feca39689e
commit
cd36e3cee7
6 changed files with 37 additions and 5 deletions
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
||||||
12
app/ytdl.py
12
app/ytdl.py
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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') {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue