add ytdlp-option setting in web
This commit is contained in:
parent
ca0aac4051
commit
ae4ebacc27
9 changed files with 228 additions and 3 deletions
35
app/main.py
35
app/main.py
|
|
@ -92,6 +92,20 @@ class Config:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
self.YTDL_OPTIONS.update(opts)
|
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()
|
config = Config()
|
||||||
|
|
||||||
class ObjectSerializer(json.JSONEncoder):
|
class ObjectSerializer(json.JSONEncoder):
|
||||||
|
|
@ -242,6 +256,27 @@ def get_custom_dirs():
|
||||||
"audio_download_dir": audio_download_dir
|
"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)
|
@routes.get(config.URL_PREFIX)
|
||||||
def index(request):
|
def index(request):
|
||||||
response = web.FileResponse(os.path.join(config.BASE_DIR, 'ui/dist/metube/browser/index.html'))
|
response = web.FileResponse(os.path.join(config.BASE_DIR, 'ui/dist/metube/browser/index.html'))
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,26 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item dropdown">
|
||||||
|
<button class="btn btn-link nav-link py-2 px-0 px-sm-2 dropdown-toggle d-flex align-items-center"
|
||||||
|
id="settingsDropdown"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
data-bs-display="static">
|
||||||
|
<fa-icon [icon]="faGear"></fa-icon>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end position-absolute" aria-labelledby="theme-select">
|
||||||
|
<li *ngFor="let setting of settings">
|
||||||
|
<button type="button" class="dropdown-item d-flex align-items-center" (click)="handleSettingClick(setting)">
|
||||||
|
<span class="me-2 opacity-50">
|
||||||
|
<fa-icon [icon]="setting.icon"></fa-icon>
|
||||||
|
</span>
|
||||||
|
{{ setting.displayName }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
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 { 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 { 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 { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
import { faGear } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
import { CookieService } from 'ngx-cookie-service';
|
||||||
import { map, Observable, of, distinctUntilChanged } from 'rxjs';
|
import { map, Observable, of, distinctUntilChanged } from 'rxjs';
|
||||||
|
|
||||||
|
|
@ -11,6 +13,8 @@ import { MasterCheckboxComponent } from './master-checkbox.component';
|
||||||
import { Formats, Format, Quality } from './formats';
|
import { Formats, Format, Quality } from './formats';
|
||||||
import { Theme, Themes } from './theme';
|
import { Theme, Themes } from './theme';
|
||||||
import {KeyValue} from "@angular/common";
|
import {KeyValue} from "@angular/common";
|
||||||
|
import { Setting, Settings } from './settings';
|
||||||
|
import { YtdlpOptionComponent } from './settings/ytdlp-option/ytdlp-option.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
|
|
@ -31,6 +35,7 @@ export class AppComponent implements AfterViewInit {
|
||||||
playlistItemLimit: number;
|
playlistItemLimit: number;
|
||||||
addInProgress = false;
|
addInProgress = false;
|
||||||
themes: Theme[] = Themes;
|
themes: Theme[] = Themes;
|
||||||
|
settings: Setting[] = Settings;
|
||||||
activeTheme: Theme;
|
activeTheme: Theme;
|
||||||
customDirs$: Observable<string[]>;
|
customDirs$: Observable<string[]>;
|
||||||
showBatchPanel: boolean = false;
|
showBatchPanel: boolean = false;
|
||||||
|
|
@ -66,6 +71,7 @@ export class AppComponent implements AfterViewInit {
|
||||||
faRedoAlt = faRedoAlt;
|
faRedoAlt = faRedoAlt;
|
||||||
faSun = faSun;
|
faSun = faSun;
|
||||||
faMoon = faMoon;
|
faMoon = faMoon;
|
||||||
|
faGear = faGear;
|
||||||
faCheck = faCheck;
|
faCheck = faCheck;
|
||||||
faCircleHalfStroke = faCircleHalfStroke;
|
faCircleHalfStroke = faCircleHalfStroke;
|
||||||
faDownload = faDownload;
|
faDownload = faDownload;
|
||||||
|
|
@ -77,7 +83,7 @@ export class AppComponent implements AfterViewInit {
|
||||||
faClock = faClock;
|
faClock = faClock;
|
||||||
faTachometerAlt = faTachometerAlt;
|
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';
|
this.format = cookieService.get('metube_format') || 'any';
|
||||||
// Needs to be set or qualities won't automatically be set
|
// Needs to be set or qualities won't automatically be set
|
||||||
this.setQualities()
|
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;
|
this.failedDownloads = Array.from(this.downloads.done.values()).filter(d => d.status === 'error').length;
|
||||||
|
|
||||||
// Calculate total speed from downloading items
|
// Calculate total speed from downloading items
|
||||||
const downloadingItems = Array.from(this.downloads.queue.values())
|
const downloadingItems = Array.from(this.downloads.queue.values()).filter(d => d.status === 'downloading');
|
||||||
.filter(d => d.status === 'downloading');
|
|
||||||
|
|
||||||
this.totalSpeed = downloadingItems.reduce((total, item) => total + (item.speed || 0), 0);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,14 @@ import { MasterCheckboxComponent, SlaveCheckboxComponent } from './master-checkb
|
||||||
import { MeTubeSocket } from './metube-socket';
|
import { MeTubeSocket } from './metube-socket';
|
||||||
import { NgSelectModule } from '@ng-select/ng-select';
|
import { NgSelectModule } from '@ng-select/ng-select';
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
|
import { YtdlpOptionComponent } from './settings/ytdlp-option/ytdlp-option.component';
|
||||||
|
|
||||||
@NgModule({ declarations: [
|
@NgModule({ declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
EtaPipe,
|
EtaPipe,
|
||||||
SpeedPipe,
|
SpeedPipe,
|
||||||
FileSizePipe,
|
FileSizePipe,
|
||||||
|
YtdlpOptionComponent,
|
||||||
EncodeURIComponent,
|
EncodeURIComponent,
|
||||||
MasterCheckboxComponent,
|
MasterCheckboxComponent,
|
||||||
SlaveCheckboxComponent
|
SlaveCheckboxComponent
|
||||||
|
|
|
||||||
16
ui/src/app/settings.ts
Normal file
16
ui/src/app/settings.ts
Normal file
|
|
@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
33
ui/src/app/settings/ytdlp-option/ytdlp-option.component.html
Normal file
33
ui/src/app/settings/ytdlp-option/ytdlp-option.component.html
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<!-- <div class="col-12 col-sm-11 col-md-10 col-lg-9 col-xl-8"> -->
|
||||||
|
<div class="p-4 bg-body rounded shadow-sm">
|
||||||
|
<h3>yt-dlp Options (JSON)</h3>
|
||||||
|
<p>Edit the yt-dlp configuration below.</p>
|
||||||
|
|
||||||
|
<form (ngSubmit)="saveOptions()" #form="ngForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<!-- <label for="jsonInput" class="form-label">yt-dlp Options (JSON)</label> -->
|
||||||
|
<textarea
|
||||||
|
id="jsonInput"
|
||||||
|
class="form-control"
|
||||||
|
rows="15"
|
||||||
|
[(ngModel)]="ytdlpOptions"
|
||||||
|
name="ytdlpOptions"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-3">
|
||||||
|
<button type="submit" class="btn btn-primary" [disabled]="isSaving">
|
||||||
|
<fa-icon [icon]="faSave" *ngIf="!isSaving"></fa-icon>
|
||||||
|
{{ isSaving ? 'Saving...' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [ngClass]="{'text-success': isSuccess, 'text-danger': !isSuccess}" class="mt-2">{{ msg }}</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- </div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
19
ui/src/app/settings/ytdlp-option/ytdlp-option.component.sass
Normal file
19
ui/src/app/settings/ytdlp-option/ytdlp-option.component.sass
Normal file
|
|
@ -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
|
||||||
|
|
@ -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<YtdlpOptionComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [YtdlpOptionComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(YtdlpOptionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
61
ui/src/app/settings/ytdlp-option/ytdlp-option.component.ts
Normal file
61
ui/src/app/settings/ytdlp-option/ytdlp-option.component.ts
Normal file
|
|
@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue