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)
|
||||
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'))
|
||||
|
|
|
|||
|
|
@ -62,6 +62,26 @@
|
|||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -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<string[]>;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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