mirror of
https://github.com/alexta69/metube.git
synced 2026-03-18 22:43:51 +00:00
reoganize quality and codec selections
This commit is contained in:
@@ -120,39 +120,205 @@
|
||||
|
||||
<!-- Options Row -->
|
||||
<div class="row mb-3 g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Quality</span>
|
||||
<select class="form-select"
|
||||
name="quality"
|
||||
[(ngModel)]="quality"
|
||||
(change)="qualityChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (q of qualities; track q) {
|
||||
<option [ngValue]="q.id">{{ q.text }}</option>
|
||||
}
|
||||
</select>
|
||||
@if (downloadType === 'video') {
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Type</span>
|
||||
<select class="form-select"
|
||||
name="downloadType"
|
||||
[(ngModel)]="downloadType"
|
||||
(change)="downloadTypeChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (type of downloadTypes; track type.id) {
|
||||
<option [ngValue]="type.id">{{ type.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Format</span>
|
||||
<select class="form-select"
|
||||
name="format"
|
||||
[(ngModel)]="format"
|
||||
(change)="formatChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (f of formats; track f) {
|
||||
<option [ngValue]="f.id">{{ f.text }}</option>
|
||||
}
|
||||
</select>
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Codec</span>
|
||||
<select class="form-select"
|
||||
name="codec"
|
||||
[(ngModel)]="codec"
|
||||
(change)="codecChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (vc of videoCodecs; track vc.id) {
|
||||
<option [ngValue]="vc.id">{{ vc.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Format</span>
|
||||
<select class="form-select"
|
||||
name="format"
|
||||
[(ngModel)]="format"
|
||||
(change)="formatChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (f of getFormatOptions(); track f.id) {
|
||||
<option [ngValue]="f.id">{{ f.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Quality</span>
|
||||
<select class="form-select"
|
||||
name="quality"
|
||||
[(ngModel)]="quality"
|
||||
(change)="qualityChanged()"
|
||||
[disabled]="addInProgress || downloads.loading || !showQualitySelector()">
|
||||
@for (q of qualities; track q.id) {
|
||||
<option [ngValue]="q.id">{{ q.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (downloadType === 'audio') {
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Type</span>
|
||||
<select class="form-select"
|
||||
name="downloadType"
|
||||
[(ngModel)]="downloadType"
|
||||
(change)="downloadTypeChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (type of downloadTypes; track type.id) {
|
||||
<option [ngValue]="type.id">{{ type.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Format</span>
|
||||
<select class="form-select"
|
||||
name="format"
|
||||
[(ngModel)]="format"
|
||||
(change)="formatChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (f of getFormatOptions(); track f.id) {
|
||||
<option [ngValue]="f.id">{{ f.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Quality</span>
|
||||
<select class="form-select"
|
||||
name="quality"
|
||||
[(ngModel)]="quality"
|
||||
(change)="qualityChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (q of qualities; track q.id) {
|
||||
<option [ngValue]="q.id">{{ q.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (downloadType === 'captions') {
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Type</span>
|
||||
<select class="form-select"
|
||||
name="downloadType"
|
||||
[(ngModel)]="downloadType"
|
||||
(change)="downloadTypeChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (type of downloadTypes; track type.id) {
|
||||
<option [ngValue]="type.id">{{ type.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Format</span>
|
||||
<select class="form-select"
|
||||
name="format"
|
||||
[(ngModel)]="format"
|
||||
(change)="formatChanged()"
|
||||
[disabled]="addInProgress || downloads.loading"
|
||||
ngbTooltip="Subtitle output format for captions mode">
|
||||
@for (f of getFormatOptions(); track f.id) {
|
||||
<option [ngValue]="f.id">{{ f.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Language</span>
|
||||
<input class="form-control"
|
||||
type="text"
|
||||
list="subtitleLanguageOptions"
|
||||
name="subtitleLanguage"
|
||||
[(ngModel)]="subtitleLanguage"
|
||||
(change)="subtitleLanguageChanged()"
|
||||
[disabled]="addInProgress || downloads.loading"
|
||||
placeholder="e.g. en, es, zh-Hans"
|
||||
ngbTooltip="Subtitle language (you can type any language code)">
|
||||
<datalist id="subtitleLanguageOptions">
|
||||
@for (lang of subtitleLanguages; track lang.id) {
|
||||
<option [value]="lang.id">{{ lang.text }}</option>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Subtitle Source</span>
|
||||
<select class="form-select"
|
||||
name="subtitleMode"
|
||||
[(ngModel)]="subtitleMode"
|
||||
(change)="subtitleModeChanged()"
|
||||
[disabled]="addInProgress || downloads.loading"
|
||||
ngbTooltip="Choose manual, auto, or fallback preference for captions mode">
|
||||
@for (mode of subtitleModes; track mode.id) {
|
||||
<option [ngValue]="mode.id">{{ mode.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Type</span>
|
||||
<select class="form-select"
|
||||
name="downloadType"
|
||||
[(ngModel)]="downloadType"
|
||||
(change)="downloadTypeChanged()"
|
||||
[disabled]="addInProgress || downloads.loading">
|
||||
@for (type of downloadTypes; track type.id) {
|
||||
<option [ngValue]="type.id">{{ type.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Format</span>
|
||||
<input class="form-control" value="JPG" disabled>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 g-3">
|
||||
<div class="col-12 text-start">
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary w-100 h-100"
|
||||
(click)="toggleAdvanced()">
|
||||
class="btn btn-link p-0 text-decoration-none"
|
||||
(click)="toggleAdvanced()"
|
||||
[attr.aria-expanded]="isAdvancedOpen"
|
||||
aria-controls="advancedOptions">
|
||||
Advanced Options
|
||||
<fa-icon
|
||||
[icon]="isAdvancedOpen ? faChevronDown : faChevronRight"
|
||||
class="ms-1" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,7 +327,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="collapse show" id="advancedOptions" [ngbCollapse]="!isAdvancedOpen">
|
||||
<div class="card card-body">
|
||||
<div class="py-2">
|
||||
<!-- Advanced Settings -->
|
||||
<div class="row g-3 mb-2">
|
||||
<div class="col-md-6">
|
||||
@@ -225,77 +391,6 @@
|
||||
ngbTooltip="Maximum number of items to download from a playlist or channel (0 = no limit)">
|
||||
</div>
|
||||
</div>
|
||||
@if (format === 'captions') {
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Subtitles</span>
|
||||
<select class="form-select"
|
||||
name="subtitleFormat"
|
||||
[(ngModel)]="subtitleFormat"
|
||||
(change)="subtitleFormatChanged()"
|
||||
[disabled]="addInProgress || downloads.loading"
|
||||
ngbTooltip="Subtitle output format for captions mode">
|
||||
@for (fmt of subtitleFormats; track fmt.id) {
|
||||
<option [ngValue]="fmt.id">{{ fmt.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
@if (subtitleFormat === 'txt') {
|
||||
<div class="form-text">TXT is generated from SRT by stripping timestamps and cue numbers.</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Language</span>
|
||||
<input class="form-control"
|
||||
type="text"
|
||||
list="subtitleLanguageOptions"
|
||||
name="subtitleLanguage"
|
||||
[(ngModel)]="subtitleLanguage"
|
||||
(change)="subtitleLanguageChanged()"
|
||||
[disabled]="addInProgress || downloads.loading"
|
||||
placeholder="e.g. en, es, zh-Hans"
|
||||
ngbTooltip="Subtitle language (you can type any language code)">
|
||||
<datalist id="subtitleLanguageOptions">
|
||||
@for (lang of subtitleLanguages; track lang.id) {
|
||||
<option [value]="lang.id">{{ lang.text }}</option>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Subtitle Source</span>
|
||||
<select class="form-select"
|
||||
name="subtitleMode"
|
||||
[(ngModel)]="subtitleMode"
|
||||
(change)="subtitleModeChanged()"
|
||||
[disabled]="addInProgress || downloads.loading"
|
||||
ngbTooltip="Choose manual, auto, or fallback preference for captions mode">
|
||||
@for (mode of subtitleModes; track mode.id) {
|
||||
<option [ngValue]="mode.id">{{ mode.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (isVideoType()) {
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Video Codec</span>
|
||||
<select class="form-select"
|
||||
name="videoCodec"
|
||||
[(ngModel)]="videoCodec"
|
||||
(change)="videoCodecChanged()"
|
||||
[disabled]="addInProgress || downloads.loading"
|
||||
ngbTooltip="Prefer a specific video codec. Falls back to best available if not found.">
|
||||
@for (vc of videoCodecs; track vc.id) {
|
||||
<option [ngValue]="vc.id">{{ vc.text }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="col-12">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-auto">
|
||||
@@ -506,8 +601,9 @@
|
||||
<app-master-checkbox #doneMasterCheckboxRef [id]="'done'" [list]="downloads.done" (changed)="doneSelectionChanged($event)" />
|
||||
</th>
|
||||
<th scope="col">Video</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Quality</th>
|
||||
<th scope="col">Codec</th>
|
||||
<th scope="col">Codec / Format</th>
|
||||
<th scope="col">File Size</th>
|
||||
<th scope="col">Downloaded</th>
|
||||
<th scope="col" style="width: 8rem;"></th>
|
||||
@@ -536,15 +632,18 @@
|
||||
<span ngbTooltip="{{buildResultItemTooltip(entry[1])}}">@if (!!entry[1].filename) {
|
||||
<a href="{{buildDownloadLink(entry[1])}}" target="_blank">{{ entry[1].title }}</a>
|
||||
} @else {
|
||||
<span [style.cursor]="entry[1].status === 'error' ? 'pointer' : 'default'"
|
||||
(click)="entry[1].status === 'error' ? toggleErrorDetail(entry[0]) : null">
|
||||
{{entry[1].title}}
|
||||
@if (entry[1].status === 'error' && !isErrorExpanded(entry[0])) {
|
||||
<small class="text-danger ms-2">
|
||||
<fa-icon [icon]="faChevronRight" size="xs" class="me-1" />Click for details
|
||||
</small>
|
||||
}
|
||||
</span>
|
||||
@if (entry[1].status === 'error') {
|
||||
<button type="button" class="btn btn-link p-0 text-start align-baseline" (click)="toggleErrorDetail(entry[0])">
|
||||
{{entry[1].title}}
|
||||
@if (!isErrorExpanded(entry[0])) {
|
||||
<small class="text-danger ms-2">
|
||||
<fa-icon [icon]="faChevronRight" size="xs" class="me-1" />Click for details
|
||||
</small>
|
||||
}
|
||||
</button>
|
||||
} @else {
|
||||
<span>{{entry[1].title}}</span>
|
||||
}
|
||||
}</span>
|
||||
@if (entry[1].status === 'error' && isErrorExpanded(entry[0])) {
|
||||
<div class="alert alert-danger py-2 px-3 mt-2 mb-0 small" style="border-left: 4px solid var(--bs-danger);">
|
||||
@@ -571,6 +670,9 @@
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{{ downloadTypeLabel(entry[1]) }}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{{ formatQualityLabel(entry[1]) }}
|
||||
</td>
|
||||
@@ -613,6 +715,7 @@
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>
|
||||
@if (chapterFile.size) {
|
||||
<span>{{ chapterFile.size | fileSize }}</span>
|
||||
|
||||
@@ -6,12 +6,27 @@ import { FormsModule } from '@angular/forms';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import { CookieService } from 'ngx-cookie-service';
|
||||
import { DownloadsService } from './services/downloads.service';
|
||||
import { Themes } from './theme';
|
||||
import { Download, Status, Theme , Quality, Format, Formats, State } from './interfaces';
|
||||
import {
|
||||
Download,
|
||||
Status,
|
||||
Theme,
|
||||
Quality,
|
||||
Option,
|
||||
AudioFormatOption,
|
||||
DOWNLOAD_TYPES,
|
||||
VIDEO_CODECS,
|
||||
VIDEO_FORMATS,
|
||||
VIDEO_QUALITIES,
|
||||
AUDIO_FORMATS,
|
||||
CAPTION_FORMATS,
|
||||
THUMBNAIL_FORMATS,
|
||||
State,
|
||||
} from './interfaces';
|
||||
import { EtaPipe, SpeedPipe, FileSizePipe } from './pipes';
|
||||
import { MasterCheckboxComponent , SlaveCheckboxComponent} from './components/';
|
||||
|
||||
@@ -40,8 +55,15 @@ export class App implements AfterViewInit, OnInit {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
addUrl!: string;
|
||||
formats: Format[] = Formats;
|
||||
downloadTypes: Option[] = DOWNLOAD_TYPES;
|
||||
videoCodecs: Option[] = VIDEO_CODECS;
|
||||
videoFormats: Option[] = VIDEO_FORMATS;
|
||||
audioFormats: AudioFormatOption[] = AUDIO_FORMATS;
|
||||
captionFormats: Option[] = CAPTION_FORMATS;
|
||||
thumbnailFormats: Option[] = THUMBNAIL_FORMATS;
|
||||
qualities!: Quality[];
|
||||
downloadType: string;
|
||||
codec: string;
|
||||
quality: string;
|
||||
format: string;
|
||||
folder!: string;
|
||||
@@ -50,10 +72,8 @@ export class App implements AfterViewInit, OnInit {
|
||||
playlistItemLimit!: number;
|
||||
splitByChapters: boolean;
|
||||
chapterTemplate: string;
|
||||
subtitleFormat: string;
|
||||
subtitleLanguage: string;
|
||||
subtitleMode: string;
|
||||
videoCodec: string;
|
||||
addInProgress = false;
|
||||
cancelRequested = false;
|
||||
hasCookies = false;
|
||||
@@ -72,9 +92,18 @@ export class App implements AfterViewInit, OnInit {
|
||||
metubeVersion: string | null = null;
|
||||
isAdvancedOpen = false;
|
||||
sortAscending = false;
|
||||
expandedErrors: Set<string> = new Set();
|
||||
expandedErrors: Set<string> = new Set<string>();
|
||||
cachedSortedDone: [string, Download][] = [];
|
||||
lastCopiedErrorId: string | null = null;
|
||||
private previousDownloadType = 'video';
|
||||
private selectionsByType: Record<string, {
|
||||
codec: string;
|
||||
format: string;
|
||||
quality: string;
|
||||
subtitleLanguage: string;
|
||||
subtitleMode: string;
|
||||
}> = {};
|
||||
private readonly selectionCookiePrefix = 'metube_selection_';
|
||||
|
||||
// Download metrics
|
||||
activeDownloads = 0;
|
||||
@@ -112,13 +141,8 @@ export class App implements AfterViewInit, OnInit {
|
||||
faSortAmountDown = faSortAmountDown;
|
||||
faSortAmountUp = faSortAmountUp;
|
||||
faChevronRight = faChevronRight;
|
||||
faChevronDown = faChevronDown;
|
||||
faUpload = faUpload;
|
||||
subtitleFormats = [
|
||||
{ id: 'srt', text: 'SRT' },
|
||||
{ id: 'txt', text: 'TXT (Text only)' },
|
||||
{ id: 'vtt', text: 'VTT' },
|
||||
{ id: 'ttml', text: 'TTML' }
|
||||
];
|
||||
subtitleLanguages = [
|
||||
{ id: 'en', text: 'English' },
|
||||
{ id: 'ar', text: 'Arabic' },
|
||||
@@ -170,39 +194,35 @@ export class App implements AfterViewInit, OnInit {
|
||||
{ id: 'manual_only', text: 'Manual Only' },
|
||||
{ id: 'auto_only', text: 'Auto Only' },
|
||||
];
|
||||
videoCodecs = [
|
||||
{ id: 'auto', text: 'Auto' },
|
||||
{ id: 'h264', text: 'H.264' },
|
||||
{ id: 'h265', text: 'H.265 (HEVC)' },
|
||||
{ id: 'av1', text: 'AV1' },
|
||||
{ id: 'vp9', text: 'VP9' },
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.downloadType = this.cookieService.get('metube_download_type') || 'video';
|
||||
this.codec = this.cookieService.get('metube_codec') || 'auto';
|
||||
this.format = this.cookieService.get('metube_format') || 'any';
|
||||
// Needs to be set or qualities won't automatically be set
|
||||
this.setQualities()
|
||||
this.quality = this.cookieService.get('metube_quality') || 'best';
|
||||
this.autoStart = this.cookieService.get('metube_auto_start') !== 'false';
|
||||
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.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.videoCodec = this.cookieService.get('metube_video_codec') || 'auto';
|
||||
const allowedDownloadTypes = new Set(this.downloadTypes.map(t => t.id));
|
||||
const allowedVideoCodecs = new Set(this.videoCodecs.map(c => c.id));
|
||||
if (!allowedVideoCodecs.has(this.videoCodec)) {
|
||||
this.videoCodec = 'auto';
|
||||
if (!allowedDownloadTypes.has(this.downloadType)) {
|
||||
this.downloadType = 'video';
|
||||
}
|
||||
if (!allowedVideoCodecs.has(this.codec)) {
|
||||
this.codec = 'auto';
|
||||
}
|
||||
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)) {
|
||||
this.subtitleFormat = 'srt';
|
||||
}
|
||||
if (!allowedSubtitleModes.has(this.subtitleMode)) {
|
||||
this.subtitleMode = 'prefer_manual';
|
||||
}
|
||||
this.loadSavedSelections();
|
||||
this.restoreSelection(this.downloadType);
|
||||
this.normalizeSelectionsForType();
|
||||
this.setQualities();
|
||||
this.previousDownloadType = this.downloadType;
|
||||
this.saveSelection(this.downloadType);
|
||||
this.sortAscending = this.cookieService.get('metube_sort_ascending') === 'true';
|
||||
|
||||
this.activeTheme = this.getPreferredTheme(this.cookieService);
|
||||
@@ -223,7 +243,7 @@ export class App implements AfterViewInit, OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.downloads.getCookieStatus().subscribe(data => {
|
||||
this.hasCookies = data?.has_cookies || false;
|
||||
this.hasCookies = !!(data && typeof data === 'object' && 'has_cookies' in data && data.has_cookies);
|
||||
});
|
||||
this.getConfiguration();
|
||||
this.getYtdlOptionsUpdateTime();
|
||||
@@ -268,10 +288,27 @@ export class App implements AfterViewInit, OnInit {
|
||||
|
||||
qualityChanged() {
|
||||
this.cookieService.set('metube_quality', this.quality, { expires: 3650 });
|
||||
this.saveSelection(this.downloadType);
|
||||
// Re-trigger custom directory change
|
||||
this.downloads.customDirsChanged.next(this.downloads.customDirs);
|
||||
}
|
||||
|
||||
downloadTypeChanged() {
|
||||
this.saveSelection(this.previousDownloadType);
|
||||
this.restoreSelection(this.downloadType);
|
||||
this.cookieService.set('metube_download_type', this.downloadType, { expires: 3650 });
|
||||
this.normalizeSelectionsForType(false);
|
||||
this.setQualities();
|
||||
this.saveSelection(this.downloadType);
|
||||
this.previousDownloadType = this.downloadType;
|
||||
this.downloads.customDirsChanged.next(this.downloads.customDirs);
|
||||
}
|
||||
|
||||
codecChanged() {
|
||||
this.cookieService.set('metube_codec', this.codec, { expires: 3650 });
|
||||
this.saveSelection(this.downloadType);
|
||||
}
|
||||
|
||||
showAdvanced() {
|
||||
return this.downloads.configuration['CUSTOM_DIRS'];
|
||||
}
|
||||
@@ -284,7 +321,7 @@ export class App implements AfterViewInit, OnInit {
|
||||
}
|
||||
|
||||
isAudioType() {
|
||||
return this.quality == 'audio' || this.format == 'mp3' || this.format == 'm4a' || this.format == 'opus' || this.format == 'wav' || this.format == 'flac';
|
||||
return this.downloadType === 'audio';
|
||||
}
|
||||
|
||||
getMatchingCustomDir() : Observable<string[]> {
|
||||
@@ -358,8 +395,8 @@ export class App implements AfterViewInit, OnInit {
|
||||
|
||||
formatChanged() {
|
||||
this.cookieService.set('metube_format', this.format, { expires: 3650 });
|
||||
// Updates to use qualities available
|
||||
this.setQualities()
|
||||
this.setQualities();
|
||||
this.saveSelection(this.downloadType);
|
||||
// Re-trigger custom directory change
|
||||
this.downloads.customDirsChanged.next(this.downloads.customDirs);
|
||||
}
|
||||
@@ -380,36 +417,42 @@ export class App implements AfterViewInit, OnInit {
|
||||
this.cookieService.set('metube_chapter_template', this.chapterTemplate, { expires: 3650 });
|
||||
}
|
||||
|
||||
subtitleFormatChanged() {
|
||||
this.cookieService.set('metube_subtitle_format', this.subtitleFormat, { expires: 3650 });
|
||||
}
|
||||
|
||||
subtitleLanguageChanged() {
|
||||
this.cookieService.set('metube_subtitle_language', this.subtitleLanguage, { expires: 3650 });
|
||||
this.saveSelection(this.downloadType);
|
||||
}
|
||||
|
||||
subtitleModeChanged() {
|
||||
this.cookieService.set('metube_subtitle_mode', this.subtitleMode, { expires: 3650 });
|
||||
}
|
||||
|
||||
videoCodecChanged() {
|
||||
this.cookieService.set('metube_video_codec', this.videoCodec, { expires: 3650 });
|
||||
this.saveSelection(this.downloadType);
|
||||
}
|
||||
|
||||
isVideoType() {
|
||||
return (this.format === 'any' || this.format === 'mp4') && this.quality !== 'audio';
|
||||
return this.downloadType === 'video';
|
||||
}
|
||||
|
||||
formatQualityLabel(download: Download): string {
|
||||
if (download.download_type === 'captions' || download.download_type === 'thumbnail') {
|
||||
return '-';
|
||||
}
|
||||
const q = download.quality;
|
||||
if (!q) return '';
|
||||
if (/^\d+$/.test(q) && download.download_type === 'audio') return `${q} kbps`;
|
||||
if (/^\d+$/.test(q)) return `${q}p`;
|
||||
if (q === 'best_ios') return 'Best (iOS)';
|
||||
return q.charAt(0).toUpperCase() + q.slice(1);
|
||||
}
|
||||
|
||||
downloadTypeLabel(download: Download): string {
|
||||
const type = download.download_type || 'video';
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
formatCodecLabel(download: Download): string {
|
||||
const codec = download.video_codec;
|
||||
if (download.download_type !== 'video') {
|
||||
const format = (download.format || '').toUpperCase();
|
||||
return format || '-';
|
||||
}
|
||||
const codec = download.codec;
|
||||
if (!codec || codec === 'auto') return 'Auto';
|
||||
return this.videoCodecs.find(c => c.id === codec)?.text ?? codec;
|
||||
}
|
||||
@@ -425,17 +468,130 @@ export class App implements AfterViewInit, OnInit {
|
||||
}
|
||||
|
||||
setQualities() {
|
||||
// qualities for specific format
|
||||
const format = this.formats.find(el => el.id == this.format)
|
||||
if (format) {
|
||||
this.qualities = format.qualities
|
||||
const exists = this.qualities.find(el => el.id === this.quality)
|
||||
this.quality = exists ? this.quality : 'best'
|
||||
if (this.downloadType === 'video') {
|
||||
this.qualities = this.format === 'ios'
|
||||
? [{ id: 'best', text: 'Best' }]
|
||||
: VIDEO_QUALITIES;
|
||||
} else if (this.downloadType === 'audio') {
|
||||
const selectedFormat = this.audioFormats.find(el => el.id === this.format);
|
||||
this.qualities = selectedFormat ? selectedFormat.qualities : [{ id: 'best', text: 'Best' }];
|
||||
} else {
|
||||
this.qualities = [{ id: 'best', text: 'Best' }];
|
||||
}
|
||||
const exists = this.qualities.find(el => el.id === this.quality);
|
||||
this.quality = exists ? this.quality : 'best';
|
||||
}
|
||||
|
||||
showCodecSelector() {
|
||||
return this.downloadType === 'video';
|
||||
}
|
||||
|
||||
showFormatSelector() {
|
||||
return this.downloadType !== 'thumbnail';
|
||||
}
|
||||
|
||||
showQualitySelector() {
|
||||
if (this.downloadType === 'video') {
|
||||
return this.format !== 'ios';
|
||||
}
|
||||
return this.downloadType === 'audio';
|
||||
}
|
||||
|
||||
getFormatOptions() {
|
||||
if (this.downloadType === 'video') {
|
||||
return this.videoFormats;
|
||||
}
|
||||
if (this.downloadType === 'audio') {
|
||||
return this.audioFormats;
|
||||
}
|
||||
if (this.downloadType === 'captions') {
|
||||
return this.captionFormats;
|
||||
}
|
||||
return this.thumbnailFormats;
|
||||
}
|
||||
|
||||
private normalizeSelectionsForType(resetForTypeChange = false) {
|
||||
if (this.downloadType === 'video') {
|
||||
const allowedFormats = new Set(this.videoFormats.map(f => f.id));
|
||||
if (resetForTypeChange || !allowedFormats.has(this.format)) {
|
||||
this.format = 'any';
|
||||
}
|
||||
const allowedCodecs = new Set(this.videoCodecs.map(c => c.id));
|
||||
if (resetForTypeChange || !allowedCodecs.has(this.codec)) {
|
||||
this.codec = 'auto';
|
||||
}
|
||||
} else if (this.downloadType === 'audio') {
|
||||
const allowedFormats = new Set(this.audioFormats.map(f => f.id));
|
||||
if (resetForTypeChange || !allowedFormats.has(this.format)) {
|
||||
this.format = this.audioFormats[0].id;
|
||||
}
|
||||
} else if (this.downloadType === 'captions') {
|
||||
const allowedFormats = new Set(this.captionFormats.map(f => f.id));
|
||||
if (resetForTypeChange || !allowedFormats.has(this.format)) {
|
||||
this.format = 'srt';
|
||||
}
|
||||
this.quality = 'best';
|
||||
} else {
|
||||
this.format = 'jpg';
|
||||
this.quality = 'best';
|
||||
}
|
||||
this.cookieService.set('metube_format', this.format, { expires: 3650 });
|
||||
this.cookieService.set('metube_codec', this.codec, { expires: 3650 });
|
||||
}
|
||||
|
||||
private saveSelection(type: string) {
|
||||
if (!type) return;
|
||||
const selection = {
|
||||
codec: this.codec,
|
||||
format: this.format,
|
||||
quality: this.quality,
|
||||
subtitleLanguage: this.subtitleLanguage,
|
||||
subtitleMode: this.subtitleMode,
|
||||
};
|
||||
this.selectionsByType[type] = selection;
|
||||
this.cookieService.set(
|
||||
this.selectionCookiePrefix + type,
|
||||
JSON.stringify(selection),
|
||||
{ expires: 3650 }
|
||||
);
|
||||
}
|
||||
|
||||
private restoreSelection(type: string) {
|
||||
const saved = this.selectionsByType[type];
|
||||
if (!saved) return;
|
||||
this.codec = saved.codec;
|
||||
this.format = saved.format;
|
||||
this.quality = saved.quality;
|
||||
this.subtitleLanguage = saved.subtitleLanguage;
|
||||
this.subtitleMode = saved.subtitleMode;
|
||||
}
|
||||
|
||||
private loadSavedSelections() {
|
||||
for (const type of this.downloadTypes.map(t => t.id)) {
|
||||
const key = this.selectionCookiePrefix + type;
|
||||
if (!this.cookieService.check(key)) continue;
|
||||
try {
|
||||
const raw = this.cookieService.get(key);
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
this.selectionsByType[type] = {
|
||||
codec: String(parsed.codec ?? 'auto'),
|
||||
format: String(parsed.format ?? ''),
|
||||
quality: String(parsed.quality ?? 'best'),
|
||||
subtitleLanguage: String(parsed.subtitleLanguage ?? 'en'),
|
||||
subtitleMode: String(parsed.subtitleMode ?? 'prefer_manual'),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed cookie values.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addDownload(
|
||||
url?: string,
|
||||
downloadType?: string,
|
||||
codec?: string,
|
||||
quality?: string,
|
||||
format?: string,
|
||||
folder?: string,
|
||||
@@ -444,12 +600,12 @@ export class App implements AfterViewInit, OnInit {
|
||||
autoStart?: boolean,
|
||||
splitByChapters?: boolean,
|
||||
chapterTemplate?: string,
|
||||
subtitleFormat?: string,
|
||||
subtitleLanguage?: string,
|
||||
subtitleMode?: string,
|
||||
videoCodec?: string,
|
||||
) {
|
||||
url = url ?? this.addUrl
|
||||
downloadType = downloadType ?? this.downloadType
|
||||
codec = codec ?? this.codec
|
||||
quality = quality ?? this.quality
|
||||
format = format ?? this.format
|
||||
folder = folder ?? this.folder
|
||||
@@ -458,10 +614,8 @@ export class App implements AfterViewInit, OnInit {
|
||||
autoStart = autoStart ?? this.autoStart
|
||||
splitByChapters = splitByChapters ?? this.splitByChapters
|
||||
chapterTemplate = chapterTemplate ?? this.chapterTemplate
|
||||
subtitleFormat = subtitleFormat ?? this.subtitleFormat
|
||||
subtitleLanguage = subtitleLanguage ?? this.subtitleLanguage
|
||||
subtitleMode = subtitleMode ?? this.subtitleMode
|
||||
videoCodec = videoCodec ?? this.videoCodec
|
||||
|
||||
// Validate chapter template if chapter splitting is enabled
|
||||
if (splitByChapters && !chapterTemplate.includes('%(section_number)')) {
|
||||
@@ -469,10 +623,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 + ' videoCodec=' + videoCodec);
|
||||
console.debug('Downloading: url=' + url + ' downloadType=' + downloadType + ' codec=' + codec + ' quality=' + quality + ' format=' + format + ' folder=' + folder + ' customNamePrefix=' + customNamePrefix + ' playlistItemLimit=' + playlistItemLimit + ' autoStart=' + autoStart + ' splitByChapters=' + splitByChapters + ' chapterTemplate=' + chapterTemplate + ' subtitleLanguage=' + subtitleLanguage + ' subtitleMode=' + subtitleMode);
|
||||
this.addInProgress = true;
|
||||
this.cancelRequested = false;
|
||||
this.downloads.add(url, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleFormat, subtitleLanguage, subtitleMode, videoCodec).subscribe((status: Status) => {
|
||||
this.downloads.add(url, downloadType, codec, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleLanguage, subtitleMode).subscribe((status: Status) => {
|
||||
if (status.status === 'error' && !this.cancelRequested) {
|
||||
alert(`Error adding URL: ${status.msg}`);
|
||||
} else if (status.status !== 'error') {
|
||||
@@ -499,6 +653,8 @@ export class App implements AfterViewInit, OnInit {
|
||||
retryDownload(key: string, download: Download) {
|
||||
this.addDownload(
|
||||
download.url,
|
||||
download.download_type,
|
||||
download.codec,
|
||||
download.quality,
|
||||
download.format,
|
||||
download.folder,
|
||||
@@ -507,10 +663,8 @@ export class App implements AfterViewInit, OnInit {
|
||||
true,
|
||||
download.split_by_chapters,
|
||||
download.chapter_template,
|
||||
download.subtitle_format,
|
||||
download.subtitle_language,
|
||||
download.subtitle_mode,
|
||||
download.video_codec,
|
||||
);
|
||||
this.downloads.delById('done', [key]).subscribe();
|
||||
}
|
||||
@@ -560,7 +714,7 @@ export class App implements AfterViewInit, OnInit {
|
||||
|
||||
buildDownloadLink(download: Download) {
|
||||
let baseDir = this.downloads.configuration["PUBLIC_HOST_URL"];
|
||||
if (download.quality == 'audio' || download.filename.endsWith('.mp3')) {
|
||||
if (download.download_type === 'audio' || download.filename.endsWith('.mp3')) {
|
||||
baseDir = this.downloads.configuration["PUBLIC_HOST_AUDIO_URL"];
|
||||
}
|
||||
|
||||
@@ -584,7 +738,7 @@ export class App implements AfterViewInit, OnInit {
|
||||
|
||||
buildChapterDownloadLink(download: Download, chapterFilename: string) {
|
||||
let baseDir = this.downloads.configuration["PUBLIC_HOST_URL"];
|
||||
if (download.quality == 'audio' || chapterFilename.endsWith('.mp3')) {
|
||||
if (download.download_type === 'audio' || chapterFilename.endsWith('.mp3')) {
|
||||
baseDir = this.downloads.configuration["PUBLIC_HOST_AUDIO_URL"];
|
||||
}
|
||||
|
||||
@@ -655,10 +809,10 @@ export class App implements AfterViewInit, OnInit {
|
||||
}
|
||||
const url = urls[index];
|
||||
this.batchImportStatus = `Importing URL ${index + 1} of ${urls.length}: ${url}`;
|
||||
// Now pass the selected quality, format, folder, etc. to the add() method
|
||||
this.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix,
|
||||
// Pass current selection options to backend
|
||||
this.downloads.add(url, this.downloadType, this.codec, this.quality, this.format, this.folder, this.customNamePrefix,
|
||||
this.playlistItemLimit, this.autoStart, this.splitByChapters, this.chapterTemplate,
|
||||
this.subtitleFormat, this.subtitleLanguage, this.subtitleMode, this.videoCodec)
|
||||
this.subtitleLanguage, this.subtitleMode)
|
||||
.subscribe({
|
||||
next: (status: Status) => {
|
||||
if (status.status === 'error') {
|
||||
@@ -891,7 +1045,7 @@ export class App implements AfterViewInit, OnInit {
|
||||
|
||||
private refreshCookieStatus() {
|
||||
this.downloads.getCookieStatus().subscribe(data => {
|
||||
this.hasCookies = data?.has_cookies || false;
|
||||
this.hasCookies = !!(data && typeof data === 'object' && 'has_cookies' in data && data.has_cookies);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ export interface Download {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
download_type: string;
|
||||
codec?: string;
|
||||
quality: string;
|
||||
format: string;
|
||||
folder: string;
|
||||
@@ -10,10 +12,8 @@ export interface Download {
|
||||
playlist_item_limit: number;
|
||||
split_by_chapters?: boolean;
|
||||
chapter_template?: string;
|
||||
subtitle_format?: string;
|
||||
subtitle_language?: string;
|
||||
subtitle_mode?: string;
|
||||
video_codec?: string;
|
||||
status: string;
|
||||
msg: string;
|
||||
percent: number;
|
||||
@@ -25,5 +25,5 @@ export interface Download {
|
||||
size?: number;
|
||||
error?: string;
|
||||
deleting?: boolean;
|
||||
chapter_files?: Array<{ filename: string, size: number }>;
|
||||
chapter_files?: { filename: string, size: number }[];
|
||||
}
|
||||
|
||||
@@ -1,81 +1,77 @@
|
||||
import { Format } from "./format";
|
||||
import { Quality } from "./quality";
|
||||
|
||||
export interface Option {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const Formats: Format[] = [
|
||||
{
|
||||
id: 'any',
|
||||
text: 'Any',
|
||||
qualities: [
|
||||
{ id: 'best', text: 'Best' },
|
||||
{ id: '2160', text: '2160p' },
|
||||
{ id: '1440', text: '1440p' },
|
||||
{ id: '1080', text: '1080p' },
|
||||
{ id: '720', text: '720p' },
|
||||
{ id: '480', text: '480p' },
|
||||
{ id: '360', text: '360p' },
|
||||
{ id: '240', text: '240p' },
|
||||
{ id: 'worst', text: 'Worst' },
|
||||
{ id: 'audio', text: 'Audio Only' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mp4',
|
||||
text: 'MP4',
|
||||
qualities: [
|
||||
{ id: 'best', text: 'Best' },
|
||||
{ id: 'best_ios', text: 'Best (iOS)' },
|
||||
{ id: '2160', text: '2160p' },
|
||||
{ id: '1440', text: '1440p' },
|
||||
{ id: '1080', text: '1080p' },
|
||||
{ id: '720', text: '720p' },
|
||||
{ id: '480', text: '480p' },
|
||||
{ id: '360', text: '360p' },
|
||||
{ id: '240', text: '240p' },
|
||||
{ id: 'worst', text: 'Worst' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'm4a',
|
||||
text: 'M4A',
|
||||
qualities: [
|
||||
{ id: 'best', text: 'Best' },
|
||||
{ id: '192', text: '192 kbps' },
|
||||
{ id: '128', text: '128 kbps' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mp3',
|
||||
text: 'MP3',
|
||||
qualities: [
|
||||
{ id: 'best', text: 'Best' },
|
||||
{ id: '320', text: '320 kbps' },
|
||||
{ id: '192', text: '192 kbps' },
|
||||
{ id: '128', text: '128 kbps' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'opus',
|
||||
text: 'OPUS',
|
||||
qualities: [{ id: 'best', text: 'Best' }],
|
||||
},
|
||||
{
|
||||
id: 'wav',
|
||||
text: 'WAV',
|
||||
qualities: [{ id: 'best', text: 'Best' }],
|
||||
},
|
||||
{
|
||||
id: 'flac',
|
||||
text: 'FLAC',
|
||||
qualities: [{ id: 'best', text: 'Best' }],
|
||||
},
|
||||
{
|
||||
id: 'thumbnail',
|
||||
text: 'Thumbnail',
|
||||
qualities: [{ id: 'best', text: 'Best' }],
|
||||
},
|
||||
{
|
||||
id: 'captions',
|
||||
text: 'Captions',
|
||||
qualities: [{ id: 'best', text: 'Best' }],
|
||||
},
|
||||
export interface AudioFormatOption extends Option {
|
||||
qualities: Quality[];
|
||||
}
|
||||
|
||||
export const DOWNLOAD_TYPES: Option[] = [
|
||||
{ id: "video", text: "Video" },
|
||||
{ id: "audio", text: "Audio" },
|
||||
{ id: "captions", text: "Captions" },
|
||||
{ id: "thumbnail", text: "Thumbnail" },
|
||||
];
|
||||
|
||||
export const VIDEO_CODECS: Option[] = [
|
||||
{ id: "auto", text: "Auto" },
|
||||
{ id: "h264", text: "H.264" },
|
||||
{ id: "h265", text: "H.265 (HEVC)" },
|
||||
{ id: "av1", text: "AV1" },
|
||||
{ id: "vp9", text: "VP9" },
|
||||
];
|
||||
|
||||
export const VIDEO_FORMATS: Option[] = [
|
||||
{ id: "any", text: "Auto" },
|
||||
{ id: "mp4", text: "MP4" },
|
||||
{ id: "ios", text: "iOS Compatible" },
|
||||
];
|
||||
|
||||
export const VIDEO_QUALITIES: Quality[] = [
|
||||
{ id: "best", text: "Best" },
|
||||
{ id: "2160", text: "2160p" },
|
||||
{ id: "1440", text: "1440p" },
|
||||
{ id: "1080", text: "1080p" },
|
||||
{ id: "720", text: "720p" },
|
||||
{ id: "480", text: "480p" },
|
||||
{ id: "360", text: "360p" },
|
||||
{ id: "240", text: "240p" },
|
||||
{ id: "worst", text: "Worst" },
|
||||
];
|
||||
|
||||
export const AUDIO_FORMATS: AudioFormatOption[] = [
|
||||
{
|
||||
id: "m4a",
|
||||
text: "M4A",
|
||||
qualities: [
|
||||
{ id: "best", text: "Best" },
|
||||
{ id: "192", text: "192 kbps" },
|
||||
{ id: "128", text: "128 kbps" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "mp3",
|
||||
text: "MP3",
|
||||
qualities: [
|
||||
{ id: "best", text: "Best" },
|
||||
{ id: "320", text: "320 kbps" },
|
||||
{ id: "192", text: "192 kbps" },
|
||||
{ id: "128", text: "128 kbps" },
|
||||
],
|
||||
},
|
||||
{ id: "opus", text: "OPUS", qualities: [{ id: "best", text: "Best" }] },
|
||||
{ id: "wav", text: "WAV", qualities: [{ id: "best", text: "Best" }] },
|
||||
{ id: "flac", text: "FLAC", qualities: [{ id: "best", text: "Best" }] },
|
||||
];
|
||||
|
||||
export const CAPTION_FORMATS: Option[] = [
|
||||
{ id: "srt", text: "SRT" },
|
||||
{ id: "txt", text: "TXT (Text only)" },
|
||||
{ id: "vtt", text: "VTT" },
|
||||
{ id: "ttml", text: "TTML" },
|
||||
];
|
||||
|
||||
export const THUMBNAIL_FORMATS: Option[] = [{ id: "jpg", text: "JPG" }];
|
||||
|
||||
@@ -109,6 +109,8 @@ export class DownloadsService {
|
||||
|
||||
public add(
|
||||
url: string,
|
||||
downloadType: string,
|
||||
codec: string,
|
||||
quality: string,
|
||||
format: string,
|
||||
folder: string,
|
||||
@@ -117,13 +119,13 @@ export class DownloadsService {
|
||||
autoStart: boolean,
|
||||
splitByChapters: boolean,
|
||||
chapterTemplate: string,
|
||||
subtitleFormat: string,
|
||||
subtitleLanguage: string,
|
||||
subtitleMode: string,
|
||||
videoCodec: string,
|
||||
) {
|
||||
return this.http.post<Status>('add', {
|
||||
url: url,
|
||||
download_type: downloadType,
|
||||
codec: codec,
|
||||
quality: quality,
|
||||
format: format,
|
||||
folder: folder,
|
||||
@@ -132,10 +134,8 @@ export class DownloadsService {
|
||||
auto_start: autoStart,
|
||||
split_by_chapters: splitByChapters,
|
||||
chapter_template: chapterTemplate,
|
||||
subtitle_format: subtitleFormat,
|
||||
subtitle_language: subtitleLanguage,
|
||||
subtitle_mode: subtitleMode,
|
||||
video_codec: videoCodec,
|
||||
}).pipe(
|
||||
catchError(this.handleHTTPError)
|
||||
);
|
||||
@@ -174,6 +174,8 @@ export class DownloadsService {
|
||||
status: string;
|
||||
msg?: string;
|
||||
}> {
|
||||
const defaultDownloadType = 'video';
|
||||
const defaultCodec = 'auto';
|
||||
const defaultQuality = 'best';
|
||||
const defaultFormat = 'mp4';
|
||||
const defaultFolder = '';
|
||||
@@ -182,14 +184,14 @@ export class DownloadsService {
|
||||
const defaultAutoStart = true;
|
||||
const defaultSplitByChapters = false;
|
||||
const defaultChapterTemplate = this.configuration['OUTPUT_TEMPLATE_CHAPTER'];
|
||||
const defaultSubtitleFormat = 'srt';
|
||||
const defaultSubtitleLanguage = 'en';
|
||||
const defaultSubtitleMode = 'prefer_manual';
|
||||
const defaultVideoCodec = 'auto';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.add(
|
||||
url,
|
||||
defaultDownloadType,
|
||||
defaultCodec,
|
||||
defaultQuality,
|
||||
defaultFormat,
|
||||
defaultFolder,
|
||||
@@ -198,10 +200,8 @@ export class DownloadsService {
|
||||
defaultAutoStart,
|
||||
defaultSplitByChapters,
|
||||
defaultChapterTemplate,
|
||||
defaultSubtitleFormat,
|
||||
defaultSubtitleLanguage,
|
||||
defaultSubtitleMode,
|
||||
defaultVideoCodec,
|
||||
)
|
||||
.subscribe({
|
||||
next: (response) => resolve(response),
|
||||
@@ -221,19 +221,19 @@ export class DownloadsService {
|
||||
uploadCookies(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('cookies', file);
|
||||
return this.http.post<any>('upload-cookies', formData).pipe(
|
||||
return this.http.post<{ status: string; msg?: string }>('upload-cookies', formData).pipe(
|
||||
catchError(this.handleHTTPError)
|
||||
);
|
||||
}
|
||||
|
||||
deleteCookies() {
|
||||
return this.http.post<any>('delete-cookies', {}).pipe(
|
||||
return this.http.post<{ status: string; msg?: string }>('delete-cookies', {}).pipe(
|
||||
catchError(this.handleHTTPError)
|
||||
);
|
||||
}
|
||||
|
||||
getCookieStatus() {
|
||||
return this.http.get<any>('cookie-status').pipe(
|
||||
return this.http.get<{ status: string; has_cookies: boolean }>('cookie-status').pipe(
|
||||
catchError(this.handleHTTPError)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user