From ae4ebacc274a94b62962bc27e1f1d9a734c667ef Mon Sep 17 00:00:00 2001 From: xerdream Date: Thu, 17 Jul 2025 13:38:33 +0800 Subject: [PATCH] add ytdlp-option setting in web --- app/main.py | 35 +++++++++++ ui/src/app/app.component.html | 20 ++++++ ui/src/app/app.component.ts | 22 ++++++- ui/src/app/app.module.ts | 2 + ui/src/app/settings.ts | 16 +++++ .../ytdlp-option/ytdlp-option.component.html | 33 ++++++++++ .../ytdlp-option/ytdlp-option.component.sass | 19 ++++++ .../ytdlp-option.component.spec.ts | 23 +++++++ .../ytdlp-option/ytdlp-option.component.ts | 61 +++++++++++++++++++ 9 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 ui/src/app/settings.ts create mode 100644 ui/src/app/settings/ytdlp-option/ytdlp-option.component.html create mode 100644 ui/src/app/settings/ytdlp-option/ytdlp-option.component.sass create mode 100644 ui/src/app/settings/ytdlp-option/ytdlp-option.component.spec.ts create mode 100644 ui/src/app/settings/ytdlp-option/ytdlp-option.component.ts diff --git a/app/main.py b/app/main.py index 90554ee..5854acc 100644 --- a/app/main.py +++ b/app/main.py @@ -92,6 +92,20 @@ class Config: sys.exit(1) self.YTDL_OPTIONS.update(opts) + def update_ytdl_options_file(self): + if not self.YTDL_OPTIONS_FILE: + log.info('YTDL_OPTIONS_FILE not set, writing to default location') + self.YTDL_OPTIONS_FILE = os.path.join(self.DOWNLOAD_DIR,'ytdl_options.json') + log.info('Writing YTDL_OPTIONS to %s', self.YTDL_OPTIONS_FILE) + try: + with open(config.YTDL_OPTIONS_FILE, 'w') as f: + f.write(serializer.encode(config.YTDL_OPTIONS)) + except: + log.error('Failed to write YTDL_OPTIONS to %s', self.YTDL_OPTIONS_FILE) + self.YTDL_OPTIONS_FILE = '' + return False + return True + config = Config() class ObjectSerializer(json.JSONEncoder): @@ -242,6 +256,27 @@ def get_custom_dirs(): "audio_download_dir": audio_download_dir } +@routes.get(config.URL_PREFIX + 'ytdl_options') +async def ytdl_options(request): + log.info("Sending yt-dlp options") + return web.Response(text=serializer.encode(config.YTDL_OPTIONS)) + +@routes.post(config.URL_PREFIX + 'ytdl_options') +async def update_ytdl_options(request): + try: + post = await request.json() + assert isinstance(post, dict) + except (json.decoder.JSONDecodeError, AssertionError): + log.info("Bad request: invalid data") + raise web.HTTPBadRequest(reason="invalid data,need json") + config.YTDL_OPTIONS = post + + msg = 'Options updated' + if not config.update_ytdl_options_file(): + msg += ', but failed to write to file' + + return web.Response(text=serializer.encode({'msg':msg,'data':config.YTDL_OPTIONS})) + @routes.get(config.URL_PREFIX) def index(request): response = web.FileResponse(os.path.join(config.BASE_DIR, 'ui/dist/metube/browser/index.html')) diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 87fb48a..58c2ae8 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -62,6 +62,26 @@ + diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index d8362b6..b6ec628 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,8 +1,10 @@ import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { faTrashAlt, faCheckCircle, faTimesCircle, IconDefinition } from '@fortawesome/free-regular-svg-icons'; import { faRedoAlt, faSun, faMoon, faCircleHalfStroke, faCheck, faExternalLinkAlt, faDownload, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt } from '@fortawesome/free-solid-svg-icons'; import { faGithub } from '@fortawesome/free-brands-svg-icons'; +import { faGear } from "@fortawesome/free-solid-svg-icons"; import { CookieService } from 'ngx-cookie-service'; import { map, Observable, of, distinctUntilChanged } from 'rxjs'; @@ -11,6 +13,8 @@ import { MasterCheckboxComponent } from './master-checkbox.component'; import { Formats, Format, Quality } from './formats'; import { Theme, Themes } from './theme'; import {KeyValue} from "@angular/common"; +import { Setting, Settings } from './settings'; +import { YtdlpOptionComponent } from './settings/ytdlp-option/ytdlp-option.component'; @Component({ selector: 'app-root', @@ -31,6 +35,7 @@ export class AppComponent implements AfterViewInit { playlistItemLimit: number; addInProgress = false; themes: Theme[] = Themes; + settings: Setting[] = Settings; activeTheme: Theme; customDirs$: Observable; showBatchPanel: boolean = false; @@ -66,6 +71,7 @@ export class AppComponent implements AfterViewInit { faRedoAlt = faRedoAlt; faSun = faSun; faMoon = faMoon; + faGear = faGear; faCheck = faCheck; faCircleHalfStroke = faCircleHalfStroke; faDownload = faDownload; @@ -77,7 +83,7 @@ export class AppComponent implements AfterViewInit { faClock = faClock; faTachometerAlt = faTachometerAlt; - constructor(public downloads: DownloadsService, private cookieService: CookieService, private http: HttpClient) { + constructor(public downloads: DownloadsService, private cookieService: CookieService, private http: HttpClient,private modalService: NgbModal) { this.format = cookieService.get('metube_format') || 'any'; // Needs to be set or qualities won't automatically be set this.setQualities() @@ -497,9 +503,19 @@ export class AppComponent implements AfterViewInit { this.failedDownloads = Array.from(this.downloads.done.values()).filter(d => d.status === 'error').length; // Calculate total speed from downloading items - const downloadingItems = Array.from(this.downloads.queue.values()) - .filter(d => d.status === 'downloading'); + const downloadingItems = Array.from(this.downloads.queue.values()).filter(d => d.status === 'downloading'); this.totalSpeed = downloadingItems.reduce((total, item) => total + (item.speed || 0), 0); } + openSettingsModal() { + const modalRef = this.modalService.open(YtdlpOptionComponent, { + size: 'lg', + windowClass: 'modal-responsive' + }); + } + handleSettingClick(setting: Setting){ + if (setting.id === 'ytdl_options') { + this.openSettingsModal(); + } + } } diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index bdd04b0..fd462d2 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -12,12 +12,14 @@ import { MasterCheckboxComponent, SlaveCheckboxComponent } from './master-checkb import { MeTubeSocket } from './metube-socket'; import { NgSelectModule } from '@ng-select/ng-select'; import { ServiceWorkerModule } from '@angular/service-worker'; +import { YtdlpOptionComponent } from './settings/ytdlp-option/ytdlp-option.component'; @NgModule({ declarations: [ AppComponent, EtaPipe, SpeedPipe, FileSizePipe, + YtdlpOptionComponent, EncodeURIComponent, MasterCheckboxComponent, SlaveCheckboxComponent diff --git a/ui/src/app/settings.ts b/ui/src/app/settings.ts new file mode 100644 index 0000000..253a9e3 --- /dev/null +++ b/ui/src/app/settings.ts @@ -0,0 +1,16 @@ +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { faWrench } from "@fortawesome/free-solid-svg-icons"; + +export interface Setting { + id: string; + displayName: string; + icon: IconDefinition; +} + +export const Settings: Setting[] = [ + { + id: 'ytdl_options', + displayName: 'yt-dlp Options', + icon: faWrench, + }, +]; diff --git a/ui/src/app/settings/ytdlp-option/ytdlp-option.component.html b/ui/src/app/settings/ytdlp-option/ytdlp-option.component.html new file mode 100644 index 0000000..cf8144b --- /dev/null +++ b/ui/src/app/settings/ytdlp-option/ytdlp-option.component.html @@ -0,0 +1,33 @@ +
+
+ +
+

yt-dlp Options (JSON)

+

Edit the yt-dlp configuration below.

+ +
+
+ + +
+ +
+ +
+ +
{{ msg }}
+
+
+ +
+
\ No newline at end of file diff --git a/ui/src/app/settings/ytdlp-option/ytdlp-option.component.sass b/ui/src/app/settings/ytdlp-option/ytdlp-option.component.sass new file mode 100644 index 0000000..df205bf --- /dev/null +++ b/ui/src/app/settings/ytdlp-option/ytdlp-option.component.sass @@ -0,0 +1,19 @@ +.modal-responsive + .modal-dialog + max-width: 95vw + + @media (min-width: 576px) + max-width: 80vw + + @media (min-width: 768px) + max-width: 70vw + + @media (min-width: 992px) + max-width: 60vw + + @media (min-width: 1200px) + max-width: 50vw + + .modal-body + max-height: 80vh + overflow-y: auto \ No newline at end of file diff --git a/ui/src/app/settings/ytdlp-option/ytdlp-option.component.spec.ts b/ui/src/app/settings/ytdlp-option/ytdlp-option.component.spec.ts new file mode 100644 index 0000000..b797dca --- /dev/null +++ b/ui/src/app/settings/ytdlp-option/ytdlp-option.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { YtdlpOptionComponent } from './ytdlp-option.component'; + +describe('YtdlpOptionComponent', () => { + let component: YtdlpOptionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [YtdlpOptionComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(YtdlpOptionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/src/app/settings/ytdlp-option/ytdlp-option.component.ts b/ui/src/app/settings/ytdlp-option/ytdlp-option.component.ts new file mode 100644 index 0000000..aa1062a --- /dev/null +++ b/ui/src/app/settings/ytdlp-option/ytdlp-option.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { faSave } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'app-ytdlp-option', + templateUrl: './ytdlp-option.component.html', + styleUrls: ['./ytdlp-option.component.sass'], + standalone: false +}) + +export class YtdlpOptionComponent implements OnInit { + faSave = faSave; + ytdlpOptions: string = ''; + isSuccess: boolean = true; + isSaving: boolean = false; + msg: string = ''; + + constructor(private http: HttpClient) {} + + ngOnInit(): void { + this.loadOptions(); + } + async loadOptions(): Promise { + this.msg = ''; + + try { + const data = await firstValueFrom(this.http.get('ytdl_options')); + this.ytdlpOptions = JSON.stringify(data, null, 2); + } catch (error) { + this.msg = 'Failed to load options.'; + alert(`Error loading yt-dlp options: ${error.message}`); + this.isSuccess=false; + } + } + async saveOptions(): Promise { + this.isSaving = true; + this.isSuccess=true; + this.msg = ''; + if (!this.ytdlpOptions) { + this.ytdlpOptions='{}'; + } + + try { + const payload = JSON.parse(this.ytdlpOptions); + const data = await firstValueFrom(this.http.post('ytdl_options', payload)); + this.msg = data['msg']; + } catch (error) { + if (error instanceof SyntaxError) { + this.msg = 'Invalid JSON format. Please check your input.'; + } else { + this.msg = 'Failed to save options.'; + alert(`Error saving yt-dlp options: ${error.message}`); + } + this.isSuccess=false; + } finally { + this.isSaving = false; + } + } +} \ No newline at end of file