diff --git a/app/main.py b/app/main.py index 8489e4f..1287705 100644 --- a/app/main.py +++ b/app/main.py @@ -62,9 +62,10 @@ dqueue = DownloadQueue(config, Notifier()) async def add(request): post = await request.json() url = post.get('url') - if not url: + quality = post.get('quality') + if not url or not quality: raise web.HTTPBadRequest() - status = await dqueue.add(url) + status = await dqueue.add(url, quality) return web.Response(text=serializer.encode(status)) @routes.post(config.URL_PREFIX + 'delete') diff --git a/app/ytdl.py b/app/ytdl.py index 1543f18..b787ae6 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -32,8 +32,15 @@ class DownloadInfo: class Download: manager = None - def __init__(self, download_dir, info): + def __init__(self, download_dir, quality, info): self.download_dir = download_dir + if quality == 'best': + self.format = None + elif quality in ('1080p', '720p', '480p'): + res = quality[:-1] + self.format = f'bestvideo[height<={res}]+bestaudio/best[height<={res}]' + else: + raise Exception(f'unknown quality {quality}') self.info = info self.canceled = False self.tmpfilename = None @@ -49,6 +56,7 @@ class Download: 'no_color': True, #'skip_download': True, 'outtmpl': os.path.join(self.download_dir, '%(title)s.%(ext)s'), + 'format': self.format, 'cachedir': False, 'socket_timeout': 30, 'progress_hooks': [lambda d: self.status_queue.put(d)], @@ -121,29 +129,29 @@ class DownloadQueue: 'extract_flat': True, }).extract_info(url, download=False) - async def __add_entry(self, entry, already): + async def __add_entry(self, entry, quality, 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, already)) + results.append(await self.__add_entry(etr, quality, 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 == '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']) - self.queue[entry['id']] = Download(self.config.DOWNLOAD_DIR, dl) + self.queue[entry['id']] = Download(self.config.DOWNLOAD_DIR, quality, dl) self.event.set() await self.notifier.added(dl) return {'status': 'ok'} elif etype == 'url': - return await self.add(entry['url'], already) + return await self.add(entry['url'], quality, already) return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} - async def add(self, url, already=None): + async def add(self, url, quality, already=None): log.info(f'adding {url}') already = set() if already is None else already if url in already: @@ -155,7 +163,7 @@ class DownloadQueue: entry = await asyncio.get_running_loop().run_in_executor(None, self.__extract_info, url) except youtube_dl.utils.YoutubeDLError as exc: return {'status': 'error', 'msg': str(exc)} - return await self.__add_entry(entry, already) + return await self.__add_entry(entry, quality, already) async def cancel(self, ids): for id in ids: diff --git a/screenshot.gif b/screenshot.gif index cb7b42a..067f6eb 100644 Binary files a/screenshot.gif and b/screenshot.gif differ diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index e0c67cd..e1f29d6 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -19,11 +19,14 @@
- +
+
diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index 71b07f8..99e1a37 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -2,6 +2,9 @@ max-width: 720px margin: 4rem auto +button.add-url + min-width: 7rem + $metube-section-color-bg: rgba(0,0,0,.07) .metube-section-header diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index e0961ce..7bb8181 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -11,6 +11,13 @@ import { MasterCheckboxComponent } from './master-checkbox.component'; }) export class AppComponent implements AfterViewInit { addUrl: string; + qualities: Array = [ + {id: "best", text: "Best"}, + {id: "1080p", text: "1080p"}, + {id: "720p", text: "720p"}, + {id: "480p", text: "480p"} + ]; + quality: string = "best"; addInProgress = false; @ViewChild('queueMasterCheckbox', {static: false}) queueMasterCheckbox: MasterCheckboxComponent; @@ -61,7 +68,7 @@ export class AppComponent implements AfterViewInit { addDownload() { this.addInProgress = true; - this.downloads.add(this.addUrl).subscribe((status: Status) => { + this.downloads.add(this.addUrl, this.quality).subscribe((status: Status) => { if (status.status === 'error') { alert(`Error adding URL: ${status.msg}`); } else { diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 935feba..4da30fe 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -79,8 +79,8 @@ export class DownloadsService { return of({status: 'error', msg: msg}) } - public add(url: string) { - return this.http.post('add', {url: url}).pipe( + public add(url: string, quality: string) { + return this.http.post('add', {url: url, quality: quality}).pipe( catchError(this.handleHTTPError) ); }