mirror of
https://github.com/alexta69/metube.git
synced 2026-06-13 16:40:05 +00:00
@@ -428,6 +428,34 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (downloadType === 'video' || downloadType === 'audio') {
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Clip start</span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="clipStart"
|
||||
[(ngModel)]="clipStart"
|
||||
(change)="clipStartChanged()"
|
||||
placeholder="e.g. 2:26"
|
||||
[disabled]="addInProgress || subscribeInProgress || downloads.loading"
|
||||
ngbTooltip="Optional start time (seconds, M:SS, or H:MM:SS). Blank = from start or YouTube &t= in URL.">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Clip end</span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="clipEnd"
|
||||
[(ngModel)]="clipEnd"
|
||||
(change)="clipEndChanged()"
|
||||
placeholder="e.g. 3:24"
|
||||
[disabled]="addInProgress || subscribeInProgress || downloads.loading"
|
||||
ngbTooltip="Optional end time. Blank = until end of media.">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Behavior -->
|
||||
|
||||
@@ -194,6 +194,30 @@ describe('App', () => {
|
||||
expect(payload.titleRegex).toBe('EPISODE');
|
||||
});
|
||||
|
||||
it('omits clip fields from subscribe payload', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const app = fixture.componentInstance;
|
||||
const subs = TestBed.inject(SubscriptionsService) as unknown as SubscriptionsServiceStub;
|
||||
app.addUrl = 'https://example.com/channel';
|
||||
app.clipStart = '1:00';
|
||||
app.clipEnd = '2:00';
|
||||
app.addSubscription();
|
||||
expect(subs.subscribeCalls.length).toBe(1);
|
||||
const payload = subs.subscribeCalls[0] as Record<string, unknown>;
|
||||
expect('clipStart' in payload).toBe(false);
|
||||
expect('clipEnd' in payload).toBe(false);
|
||||
});
|
||||
|
||||
it('buildAddPayload includes clip times', () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const app = fixture.componentInstance;
|
||||
app.clipStart = '0:10';
|
||||
app.clipEnd = '1:20';
|
||||
const payload = app['buildAddPayload']();
|
||||
expect(payload.clipStart).toBe('0:10');
|
||||
expect(payload.clipEnd).toBe('1:20');
|
||||
});
|
||||
|
||||
it('blocks subscribe with invalid title regex', () => {
|
||||
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined);
|
||||
const fixture = TestBed.createComponent(App);
|
||||
|
||||
+21
-1
@@ -81,6 +81,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
playlistItemLimit!: number;
|
||||
splitByChapters: boolean;
|
||||
chapterTemplate: string;
|
||||
clipStart = '';
|
||||
clipEnd = '';
|
||||
subtitleLanguage: string;
|
||||
subtitleMode: string;
|
||||
ytdlOptionsPresets: string[] = [];
|
||||
@@ -242,6 +244,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
this.splitByChapters = this.cookieService.get('metube_split_chapters') === 'true';
|
||||
// Will be set from backend configuration, use empty string as placeholder
|
||||
this.chapterTemplate = this.cookieService.get('metube_chapter_template') || '';
|
||||
this.clipStart = this.cookieService.get('metube_clip_start') || '';
|
||||
this.clipEnd = this.cookieService.get('metube_clip_end') || '';
|
||||
this.subtitleLanguage = this.cookieService.get('metube_subtitle_language') || 'en';
|
||||
this.subtitleMode = this.cookieService.get('metube_subtitle_mode') || 'prefer_manual';
|
||||
this.ytdlOptionsPresets = this.loadYtdlOptionsPresetsFromCookie();
|
||||
@@ -579,10 +583,14 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
if (!this.validateYtdlOptionsOverrides(payload.ytdlOptionsOverrides)) {
|
||||
return;
|
||||
}
|
||||
// Subscriptions do not support clip ranges (backend rejects clip fields).
|
||||
const { clipStart: _clipStart, clipEnd: _clipEnd, ...subscribeBase } = payload;
|
||||
void _clipStart;
|
||||
void _clipEnd;
|
||||
this.subscribeInProgress = true;
|
||||
this.subscriptionsSvc
|
||||
.subscribe({
|
||||
...payload,
|
||||
...subscribeBase,
|
||||
checkIntervalMinutes: this.checkIntervalMinutes,
|
||||
titleRegex: tr,
|
||||
})
|
||||
@@ -807,6 +815,14 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
this.cookieService.set('metube_chapter_template', this.chapterTemplate, { expires: this.settingsCookieExpiryDays });
|
||||
}
|
||||
|
||||
clipStartChanged() {
|
||||
this.cookieService.set('metube_clip_start', this.clipStart, { expires: this.settingsCookieExpiryDays });
|
||||
}
|
||||
|
||||
clipEndChanged() {
|
||||
this.cookieService.set('metube_clip_end', this.clipEnd, { expires: this.settingsCookieExpiryDays });
|
||||
}
|
||||
|
||||
subtitleLanguageChanged() {
|
||||
this.cookieService.set('metube_subtitle_language', this.subtitleLanguage, { expires: this.settingsCookieExpiryDays });
|
||||
this.saveSelection(this.downloadType);
|
||||
@@ -1033,6 +1049,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
ytdlOptionsOverrides: allowYtdlOptionsOverrides
|
||||
? (overrides.ytdlOptionsOverrides ?? this.ytdlOptionsOverrides)
|
||||
: '',
|
||||
clipStart: overrides.clipStart ?? this.clipStart,
|
||||
clipEnd: overrides.clipEnd ?? this.clipEnd,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1106,6 +1124,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
||||
? [...download.ytdl_options_presets]
|
||||
: [],
|
||||
ytdlOptionsOverrides: download.ytdl_options_overrides ? JSON.stringify(download.ytdl_options_overrides) : '',
|
||||
clipStart: download.clip_start != null ? String(download.clip_start) : '',
|
||||
clipEnd: download.clip_end != null ? String(download.clip_end) : '',
|
||||
});
|
||||
this.downloads.delById('done', [key]).subscribe();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface Download {
|
||||
subtitle_mode?: string;
|
||||
ytdl_options_presets?: string[];
|
||||
ytdl_options_overrides?: Record<string, unknown>;
|
||||
clip_start?: number;
|
||||
clip_end?: number;
|
||||
status: string;
|
||||
msg: string;
|
||||
percent: number;
|
||||
|
||||
@@ -41,6 +41,8 @@ function basePayload(): AddDownloadPayload {
|
||||
subtitleMode: 'prefer_manual',
|
||||
ytdlOptionsPresets: [],
|
||||
ytdlOptionsOverrides: '',
|
||||
clipStart: '',
|
||||
clipEnd: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,6 +90,24 @@ describe('DownloadsService', () => {
|
||||
req.flush({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('add() sends clip_start and clip_end when set', () => {
|
||||
service
|
||||
.add({
|
||||
...basePayload(),
|
||||
clipStart: '1:00',
|
||||
clipEnd: '2:00',
|
||||
})
|
||||
.subscribe();
|
||||
const req = httpMock.expectOne('add');
|
||||
expect(req.request.body).toEqual(
|
||||
expect.objectContaining({
|
||||
clip_start: '1:00',
|
||||
clip_end: '2:00',
|
||||
}),
|
||||
);
|
||||
req.flush({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('getPresets() fetches configured preset names', () => {
|
||||
service.getPresets().subscribe((result) => {
|
||||
expect(result).toEqual({ presets: ['Preset A'] });
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface AddDownloadPayload {
|
||||
subtitleMode: string;
|
||||
ytdlOptionsPresets: string[];
|
||||
ytdlOptionsOverrides: string;
|
||||
clipStart?: string;
|
||||
clipEnd?: string;
|
||||
}
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -129,7 +131,7 @@ export class DownloadsService {
|
||||
}
|
||||
|
||||
public add(payload: AddDownloadPayload) {
|
||||
return this.http.post<Status>('add', {
|
||||
const body: Record<string, unknown> = {
|
||||
url: payload.url,
|
||||
download_type: payload.downloadType,
|
||||
codec: payload.codec,
|
||||
@@ -145,7 +147,12 @@ export class DownloadsService {
|
||||
subtitle_mode: payload.subtitleMode,
|
||||
ytdl_options_presets: payload.ytdlOptionsPresets,
|
||||
ytdl_options_overrides: payload.ytdlOptionsOverrides,
|
||||
}).pipe(
|
||||
};
|
||||
const cs = payload.clipStart?.trim();
|
||||
const ce = payload.clipEnd?.trim();
|
||||
if (cs) body['clip_start'] = cs;
|
||||
if (ce) body['clip_end'] = ce;
|
||||
return this.http.post<Status>('add', body).pipe(
|
||||
catchError(this.handleHTTPError)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user