mirror of
https://github.com/alexta69/metube.git
synced 2026-06-13 16:40:05 +00:00
feat: add per-download yt-dlp presets and overrides
Agent-Logs-Url: https://github.com/alexta69/metube/sessions/8a3119fc-63d1-4508-a196-8c50ff248812 Co-authored-by: alexta69 <7450369+alexta69@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b4d497f53d
commit
565a715037
@@ -447,6 +447,35 @@
|
||||
ngbTooltip="How often to poll subscriptions for new videos">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Option Preset</span>
|
||||
<select class="form-select"
|
||||
name="ytdlOptionsPreset"
|
||||
[(ngModel)]="ytdlOptionsPreset"
|
||||
(change)="ytdlOptionsPresetChanged()"
|
||||
[disabled]="addInProgress || subscribeInProgress || downloads.loading"
|
||||
ngbTooltip="Choose a named yt-dlp option preset configured on the server">
|
||||
<option value="">Default</option>
|
||||
@for (preset of ytdlOptionPresetNames; track preset) {
|
||||
<option [value]="preset">{{ preset }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Custom yt-dlp Options</span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
placeholder='e.g. {"writesubtitles": true}'
|
||||
name="ytdlOptionsOverrides"
|
||||
[(ngModel)]="ytdlOptionsOverrides"
|
||||
(change)="ytdlOptionsOverridesChanged()"
|
||||
[disabled]="addInProgress || subscribeInProgress || downloads.loading"
|
||||
ngbTooltip="Optional per-download yt-dlp overrides as a JSON object">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-auto">
|
||||
|
||||
@@ -83,6 +83,9 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
chapterTemplate: string;
|
||||
subtitleLanguage: string;
|
||||
subtitleMode: string;
|
||||
ytdlOptionsPreset: string;
|
||||
ytdlOptionsOverrides: string;
|
||||
ytdlOptionPresetNames: string[] = [];
|
||||
addInProgress = false;
|
||||
cancelRequested = false;
|
||||
subscribeInProgress = false;
|
||||
@@ -231,6 +234,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
this.chapterTemplate = this.cookieService.get('metube_chapter_template') || '';
|
||||
this.subtitleLanguage = this.cookieService.get('metube_subtitle_language') || 'en';
|
||||
this.subtitleMode = this.cookieService.get('metube_subtitle_mode') || 'prefer_manual';
|
||||
this.ytdlOptionsPreset = this.cookieService.get('metube_ytdl_options_preset') || '';
|
||||
this.ytdlOptionsOverrides = this.cookieService.get('metube_ytdl_options_overrides') || '';
|
||||
const allowedDownloadTypes = new Set(this.downloadTypes.map(t => t.id));
|
||||
const allowedVideoCodecs = new Set(this.videoCodecs.map(c => c.id));
|
||||
if (!allowedDownloadTypes.has(this.downloadType)) {
|
||||
@@ -287,6 +292,7 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
});
|
||||
this.getConfiguration();
|
||||
this.getYtdlOptionsUpdateTime();
|
||||
this.getYtdlOptionPresets();
|
||||
this.customDirs$ = this.getMatchingCustomDir();
|
||||
this.setTheme(this.activeTheme!);
|
||||
|
||||
@@ -415,6 +421,39 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
getYtdlOptionPresets() {
|
||||
this.downloads.getPresets().pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: (data) => {
|
||||
this.ytdlOptionPresetNames = Array.isArray(data?.presets)
|
||||
? data.presets.filter((preset): preset is string => typeof preset === 'string')
|
||||
: [];
|
||||
if (this.ytdlOptionsPreset && !this.ytdlOptionPresetNames.includes(this.ytdlOptionsPreset)) {
|
||||
this.ytdlOptionsPreset = '';
|
||||
this.ytdlOptionsPresetChanged();
|
||||
}
|
||||
this.cdr.markForCheck();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private validateYtdlOptionsOverrides(value: string): boolean {
|
||||
const trimmed = value?.trim() || '';
|
||||
if (!trimmed) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
||||
alert('Custom yt-dlp options must be a JSON object');
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
alert('Custom yt-dlp options must be valid JSON');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private rebuildCachedSubs() {
|
||||
this.cachedSubs = Array.from(this.subscriptionsSvc.subscriptions.entries());
|
||||
const validIds = new Set(this.cachedSubs.map(([id]) => id));
|
||||
@@ -491,6 +530,9 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
alert('Chapter template must include %(section_number)');
|
||||
return;
|
||||
}
|
||||
if (!this.validateYtdlOptionsOverrides(payload.ytdlOptionsOverrides)) {
|
||||
return;
|
||||
}
|
||||
this.subscribeInProgress = true;
|
||||
this.subscriptionsSvc
|
||||
.subscribe({
|
||||
@@ -695,6 +737,14 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
this.saveSelection(this.downloadType);
|
||||
}
|
||||
|
||||
ytdlOptionsPresetChanged() {
|
||||
this.cookieService.set('metube_ytdl_options_preset', this.ytdlOptionsPreset, { expires: this.settingsCookieExpiryDays });
|
||||
}
|
||||
|
||||
ytdlOptionsOverridesChanged() {
|
||||
this.cookieService.set('metube_ytdl_options_overrides', this.ytdlOptionsOverrides, { expires: this.settingsCookieExpiryDays });
|
||||
}
|
||||
|
||||
isVideoType() {
|
||||
return this.downloadType === 'video';
|
||||
}
|
||||
@@ -894,6 +944,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
chapterTemplate: overrides.chapterTemplate ?? this.chapterTemplate,
|
||||
subtitleLanguage: overrides.subtitleLanguage ?? this.subtitleLanguage,
|
||||
subtitleMode: overrides.subtitleMode ?? this.subtitleMode,
|
||||
ytdlOptionsPreset: overrides.ytdlOptionsPreset ?? this.ytdlOptionsPreset,
|
||||
ytdlOptionsOverrides: overrides.ytdlOptionsOverrides ?? this.ytdlOptionsOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -905,6 +957,9 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
alert('Chapter template must include %(section_number)');
|
||||
return;
|
||||
}
|
||||
if (!this.validateYtdlOptionsOverrides(payload.ytdlOptionsOverrides)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('Downloading:', payload);
|
||||
this.addInProgress = true;
|
||||
@@ -960,6 +1015,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
chapterTemplate: download.chapter_template,
|
||||
subtitleLanguage: download.subtitle_language,
|
||||
subtitleMode: download.subtitle_mode,
|
||||
ytdlOptionsPreset: download.ytdl_options_preset || '',
|
||||
ytdlOptionsOverrides: download.ytdl_options_overrides ? JSON.stringify(download.ytdl_options_overrides) : '',
|
||||
});
|
||||
this.downloads.delById('done', [key]).subscribe();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface Download {
|
||||
chapter_template?: string;
|
||||
subtitle_language?: string;
|
||||
subtitle_mode?: string;
|
||||
ytdl_options_preset?: string;
|
||||
ytdl_options_overrides?: Record<string, unknown>;
|
||||
status: string;
|
||||
msg: string;
|
||||
percent: number;
|
||||
|
||||
@@ -39,6 +39,8 @@ function basePayload(): AddDownloadPayload {
|
||||
chapterTemplate: '',
|
||||
subtitleLanguage: 'en',
|
||||
subtitleMode: 'prefer_manual',
|
||||
ytdlOptionsPreset: '',
|
||||
ytdlOptionsOverrides: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,11 +81,22 @@ describe('DownloadsService', () => {
|
||||
chapter_template: '',
|
||||
subtitle_language: 'en',
|
||||
subtitle_mode: 'prefer_manual',
|
||||
ytdl_options_preset: '',
|
||||
ytdl_options_overrides: '',
|
||||
}),
|
||||
);
|
||||
req.flush({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('getPresets() fetches configured preset names', () => {
|
||||
service.getPresets().subscribe((result) => {
|
||||
expect(result).toEqual({ presets: ['Preset A'] });
|
||||
});
|
||||
const req = httpMock.expectOne('presets');
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush({ presets: ['Preset A'] });
|
||||
});
|
||||
|
||||
it('cancelAdd posts to cancel-add', () => {
|
||||
service.cancelAdd().subscribe();
|
||||
const req = httpMock.expectOne('cancel-add');
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface AddDownloadPayload {
|
||||
chapterTemplate: string;
|
||||
subtitleLanguage: string;
|
||||
subtitleMode: string;
|
||||
ytdlOptionsPreset: string;
|
||||
ytdlOptionsOverrides: string;
|
||||
}
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -141,11 +143,19 @@ export class DownloadsService {
|
||||
chapter_template: payload.chapterTemplate,
|
||||
subtitle_language: payload.subtitleLanguage,
|
||||
subtitle_mode: payload.subtitleMode,
|
||||
ytdl_options_preset: payload.ytdlOptionsPreset,
|
||||
ytdl_options_overrides: payload.ytdlOptionsOverrides,
|
||||
}).pipe(
|
||||
catchError(this.handleHTTPError)
|
||||
);
|
||||
}
|
||||
|
||||
public getPresets() {
|
||||
return this.http.get<{ presets: string[] }>('presets').pipe(
|
||||
catchError(() => of({ presets: [] }))
|
||||
);
|
||||
}
|
||||
|
||||
public startById(ids: string[]) {
|
||||
return this.http.post('start', {ids: ids});
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ export class SubscriptionsService {
|
||||
chapter_template: payload.chapterTemplate,
|
||||
subtitle_language: payload.subtitleLanguage,
|
||||
subtitle_mode: payload.subtitleMode,
|
||||
ytdl_options_preset: payload.ytdlOptionsPreset,
|
||||
ytdl_options_overrides: payload.ytdlOptionsOverrides,
|
||||
check_interval_minutes: payload.checkIntervalMinutes,
|
||||
})
|
||||
.pipe(catchError((err) => this.handleHTTPError(err)));
|
||||
|
||||
Reference in New Issue
Block a user