diff options
author | Alex Shnitman <alexta69@gmail.com> | 2021-09-13 20:25:32 +0300 |
---|---|---|
committer | Alex Shnitman <alexta69@gmail.com> | 2021-09-13 20:25:32 +0300 |
commit | ee0fcc39937ce0598329ce246c17947ec7cac7fd (patch) | |
tree | a025b02a80abc6b0d1fd74e93e4866a8c92270d4 | |
parent | upgrade to angular 12 (diff) | |
download | metube-ee0fcc39937ce0598329ce246c17947ec7cac7fd.tar.gz metube-ee0fcc39937ce0598329ce246c17947ec7cac7fd.tar.bz2 metube-ee0fcc39937ce0598329ce246c17947ec7cac7fd.zip |
allow selecting MP4 in the GUI
-rw-r--r-- | app/main.py | 3 | ||||
-rw-r--r-- | app/ytdl.py | 29 | ||||
-rw-r--r-- | ui/package-lock.json | 21 | ||||
-rw-r--r-- | ui/package.json | 1 | ||||
-rw-r--r-- | ui/src/app/app.component.html | 43 | ||||
-rw-r--r-- | ui/src/app/app.component.sass | 6 | ||||
-rw-r--r-- | ui/src/app/app.component.ts | 29 | ||||
-rw-r--r-- | ui/src/app/app.module.ts | 3 | ||||
-rw-r--r-- | ui/src/app/downloads.service.ts | 4 |
9 files changed, 105 insertions, 34 deletions
diff --git a/app/main.py b/app/main.py index 000cb28..9469214 100644 --- a/app/main.py +++ b/app/main.py @@ -76,7 +76,8 @@ async def add(request): quality = post.get('quality')
if not url or not quality:
raise web.HTTPBadRequest()
- status = await dqueue.add(url, quality)
+ format = post.get('format')
+ status = await dqueue.add(url, quality, format)
return web.Response(text=serializer.encode(status))
@routes.post(config.URL_PREFIX + 'delete')
diff --git a/app/ytdl.py b/app/ytdl.py index 052637d..33318fd 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -24,24 +24,28 @@ class DownloadQueueNotifier: raise NotImplementedError
class DownloadInfo:
- def __init__(self, id, title, url, quality):
+ def __init__(self, id, title, url, quality, format):
self.id, self.title, self.url = id, title, url
self.quality = quality
+ self.format = format
self.status = self.msg = self.percent = self.speed = self.eta = None
class Download:
manager = None
- def __init__(self, download_dir, output_template, quality, ytdl_opts, info):
+ def __init__(self, download_dir, output_template, quality, format, ytdl_opts, info):
self.download_dir = download_dir
self.output_template = output_template
+ vfmt, afmt = '', ''
+ if format == 'mp4':
+ vfmt, afmt = '[ext=mp4]', '[ext=m4a]'
if quality == 'best':
- self.format = 'bestvideo+bestaudio/best[ext=mp4]/best'
+ self.format = f'bestvideo{vfmt}+bestaudio{afmt}/best{vfmt}'
elif quality in ('1440p', '1080p', '720p', '480p'):
res = quality[:-1]
- self.format = f'bestvideo[height<={res}]+bestaudio/best[height<={res}][ext=mp4]/best[height<={res}]'
+ self.format = f'bestvideo[height<={res}]{vfmt}+bestaudio{afmt}/best[height<={res}]{vfmt}'
elif quality == 'audio':
- self.format = 'bestaudio'
+ self.format = f'bestaudio{afmt}'
elif quality.startswith('custom:'):
self.format = quality[7:]
else:
@@ -74,7 +78,6 @@ class Download: #'skip_download': True,
'outtmpl': os.path.join(self.download_dir, self.output_template),
'format': self.format,
- 'merge_output_format': 'mp4',
'cachedir': False,
'socket_timeout': 30,
'progress_hooks': [put_status],
@@ -148,30 +151,30 @@ class DownloadQueue: 'extract_flat': True,
}).extract_info(url, download=False)
- async def __add_entry(self, entry, quality, already):
+ async def __add_entry(self, entry, quality, format, already):
etype = entry.get('_type') or 'video'
if etype == 'playlist':
entries = entry['entries']
log.info(f'playlist detected with {len(entries)} entries')
results = []
for etr in entries:
- results.append(await self.__add_entry(etr, quality, already))
+ results.append(await self.__add_entry(etr, quality, format, already))
if any(res['status'] == 'error' for res in results):
return {'status': 'error', 'msg': ', '.join(res['msg'] for res in results if res['status'] == 'error' and 'msg' in res)}
return {'status': 'ok'}
elif etype == 'video' or etype.startswith('url') and 'id' in entry:
if entry['id'] not in self.queue:
- dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality)
+ dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality, format)
dldirectory = self.config.DOWNLOAD_DIR if quality != 'audio' else self.config.AUDIO_DOWNLOAD_DIR
- self.queue[entry['id']] = Download(dldirectory, self.config.OUTPUT_TEMPLATE, quality, self.config.YTDL_OPTIONS, dl)
+ self.queue[entry['id']] = Download(dldirectory, self.config.OUTPUT_TEMPLATE, quality, format, self.config.YTDL_OPTIONS, dl)
self.event.set()
await self.notifier.added(dl)
return {'status': 'ok'}
elif etype == 'url':
- return await self.add(entry['url'], quality, already)
+ return await self.add(entry['url'], quality, format, already)
return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}
- async def add(self, url, quality, already=None):
+ async def add(self, url, quality, format, already=None):
log.info(f'adding {url}')
already = set() if already is None else already
if url in already:
@@ -183,7 +186,7 @@ class DownloadQueue: entry = await asyncio.get_running_loop().run_in_executor(None, self.__extract_info, url)
except yt_dlp.utils.YoutubeDLError as exc:
return {'status': 'error', 'msg': str(exc)}
- return await self.__add_entry(entry, quality, already)
+ return await self.__add_entry(entry, quality, format, already)
async def cancel(self, ids):
for id in ids:
diff --git a/ui/package-lock.json b/ui/package-lock.json index ab2f013..bbe51ef 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -23,6 +23,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@ng-bootstrap/ng-bootstrap": "^10.0.0", "bootstrap": "^4.5.0", + "ngx-cookie-service": "^12.0.3", "ngx-socket-io": "^4.1.0", "rxjs": "~6.6.0", "tslib": "^2.3.0", @@ -9692,6 +9693,18 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/ngx-cookie-service": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-12.0.3.tgz", + "integrity": "sha512-F5xJBTrrreI2DERGOrO6U+L7s031HxTER+3Z4gDCwxdTl4AXmtWddMxxQVw7KflOLZ4InYEs6FjQsXmKU4HsJg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^12.0.0", + "@angular/core": "^12.0.0" + } + }, "node_modules/ngx-socket-io": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ngx-socket-io/-/ngx-socket-io-4.1.0.tgz", @@ -24937,6 +24950,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "ngx-cookie-service": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-12.0.3.tgz", + "integrity": "sha512-F5xJBTrrreI2DERGOrO6U+L7s031HxTER+3Z4gDCwxdTl4AXmtWddMxxQVw7KflOLZ4InYEs6FjQsXmKU4HsJg==", + "requires": { + "tslib": "^2.0.0" + } + }, "ngx-socket-io": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ngx-socket-io/-/ngx-socket-io-4.1.0.tgz", diff --git a/ui/package.json b/ui/package.json index 1bca2f0..b01925b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,6 +26,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@ng-bootstrap/ng-bootstrap": "^10.0.0", "bootstrap": "^4.5.0", + "ngx-cookie-service": "^12.0.3", "ngx-socket-io": "^4.1.0", "rxjs": "~6.6.0", "tslib": "^2.3.0", diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 2ec41dd..20a78b4 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -16,17 +16,40 @@ <main role="main" class="container"> <form #f="ngForm"> - <div class="input-group add-url-box"> - <input type="text" class="form-control" placeholder="Video or playlist URL" name="addUrl" [(ngModel)]="addUrl" [disabled]="addInProgress || downloads.loading"> - <div class="input-group-append"> - <select class="custom-select" name="quality" [(ngModel)]="quality" [disabled]="addInProgress || downloads.loading"> - <option *ngFor="let q of qualities" [ngValue]="q.id">{{ q.text }}</option> - </select> + <div class="container add-url-box"> + <div class="row"> + <div class="col add-url-component input-group"> + <input type="text" class="form-control" placeholder="Video or playlist URL" name="addUrl" [(ngModel)]="addUrl" [disabled]="addInProgress || downloads.loading"> + </div> + </div> + <div class="row"> + <div class="col-md-5 add-url-component"> + <div class="input-group"> + <div class="input-group-prepend"> + <span class="input-group-text">Video quality</span> + </div> + <select class="custom-select" name="quality" [(ngModel)]="quality" (change)="qualityChanged()" [disabled]="addInProgress || downloads.loading"> + <option *ngFor="let q of qualities" [ngValue]="q.id">{{ q.text }}</option> + </select> + </div> + </div> + <div class="col-md-4 add-url-component"> + <div class="input-group"> + <div class="input-group-prepend"> + <span class="input-group-text">Format</span> + </div> + <select class="custom-select" name="format" [(ngModel)]="format" (change)="formatChanged()" [disabled]="addInProgress || downloads.loading"> + <option *ngFor="let f of formats" [ngValue]="f.id">{{ f.text }}</option> + </select> + </div> + </div> + <div class="col-md-3 add-url-component"> + <button class="btn btn-primary add-url" type="submit" (click)="addDownload()" [disabled]="addInProgress || downloads.loading"> + <span class="spinner-border spinner-border-sm" role="status" id="add-spinner" *ngIf="addInProgress"></span> + {{ addInProgress ? "Adding..." : "Add" }} + </button> + </div> </div> - <button class="btn btn-primary add-url" type="submit" (click)="addDownload()" [disabled]="addInProgress || downloads.loading"> - <span class="spinner-border spinner-border-sm" role="status" id="add-spinner" *ngIf="addInProgress"></span> - {{ addInProgress ? "Adding..." : "Add" }} - </button> </div> </form> diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index 99e1a37..2cc0ede 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -2,8 +2,12 @@ max-width: 720px
margin: 4rem auto
+.add-url-component
+ margin: 0.5rem auto
+
button.add-url
- min-width: 7rem
+ zmin-width: 7rem
+ width: 100%
$metube-section-color-bg: rgba(0,0,0,.07)
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 8b668c6..55f7e6e 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,6 +1,7 @@ import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; +import { CookieService } from 'ngx-cookie-service'; import { DownloadsService, Status } from './downloads.service'; import { MasterCheckboxComponent } from './master-checkbox.component'; @@ -20,7 +21,12 @@ export class AppComponent implements AfterViewInit { {id: "480p", text: "480p"}, {id: "audio", text: "Audio only"} ]; - quality: string = "best"; + quality: string; + formats: Array<Object> = [ + {id: "any", text: "Any"}, + {id: "mp4", text: "MP4"} + ]; + format: string; addInProgress = false; @ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent; @@ -35,7 +41,9 @@ export class AppComponent implements AfterViewInit { faTimesCircle = faTimesCircle; faRedoAlt = faRedoAlt; - constructor(public downloads: DownloadsService) { + constructor(public downloads: DownloadsService, private cookieService: CookieService) { + this.quality = cookieService.get('metube_quality') || 'best'; + this.format = cookieService.get('metube_format') || 'any'; } ngAfterViewInit() { @@ -62,6 +70,14 @@ export class AppComponent implements AfterViewInit { return 1; } + qualityChanged() { + this.cookieService.set('metube_quality', this.quality, { expires: 3650 }); + } + + formatChanged() { + this.cookieService.set('metube_format', this.format, { expires: 3650 }); + } + queueSelectionChanged(checked: number) { this.queueDelSelected.nativeElement.disabled = checked == 0; } @@ -70,12 +86,13 @@ export class AppComponent implements AfterViewInit { this.doneDelSelected.nativeElement.disabled = checked == 0; } - addDownload(url?: string, quality?: string) { + addDownload(url?: string, quality?: string, format?: string) { url = url ?? this.addUrl quality = quality ?? this.quality + format = format ?? this.format this.addInProgress = true; - this.downloads.add(url, quality).subscribe((status: Status) => { + this.downloads.add(url, quality, format).subscribe((status: Status) => { if (status.status === 'error') { alert(`Error adding URL: ${status.msg}`); } else { @@ -85,8 +102,8 @@ export class AppComponent implements AfterViewInit { }); } - retryDownload(key: string, quality:string){ - this.addDownload(key, quality); + retryDownload(key: string, quality: string, format: string) { + this.addDownload(key, quality, format); this.downloads.delById('done', [key]).subscribe(); } diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 5f70069..0452ae7 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -4,6 +4,7 @@ import { FormsModule } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { HttpClientModule } from '@angular/common/http'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { CookieService } from 'ngx-cookie-service'; import { AppComponent } from './app.component'; import { EtaPipe, SpeedPipe } from './downloads.pipe'; @@ -25,7 +26,7 @@ import { MeTubeSocket } from './metube-socket'; HttpClientModule, FontAwesomeModule ], - providers: [MeTubeSocket], + providers: [CookieService, MeTubeSocket], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 5fd3b5a..e0db252 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -80,8 +80,8 @@ export class DownloadsService { return of({status: 'error', msg: msg}) } - public add(url: string, quality: string) { - return this.http.post<Status>('add', {url: url, quality: quality}).pipe( + public add(url: string, quality: string, format: string) { + return this.http.post<Status>('add', {url: url, quality: quality, format: format}).pipe( catchError(this.handleHTTPError) ); } |