From de322ad5d138187aca24aff080c2db58f9845d25 Mon Sep 17 00:00:00 2001 From: 1RandomDev <84292805+1RandomDev@users.noreply.github.com> Date: Mon, 6 Jun 2022 16:26:53 +0200 Subject: Added option for thumbnail only --- app/dl_formats.py | 9 +++++++++ ui/src/app/formats.ts | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/app/dl_formats.py b/app/dl_formats.py index c06b93b..316d5df 100644 --- a/app/dl_formats.py +++ b/app/dl_formats.py @@ -19,6 +19,10 @@ def get_format(format: str, quality: str) -> str: if format.startswith("custom:"): return format[7:] + if format == "thumbnail": + # Quality is irrelevant in this case since we skip the download + return "bestaudio/best" + if format == "mp3": # Audio quality needs to be set post-download, set in opts return "bestaudio/best" @@ -66,4 +70,9 @@ def get_opts(format: str, quality: str, ytdl_opts: dict) -> dict: opts["postprocessors"].append({"key": "FFmpegMetadata"}) opts["postprocessors"].append({"key": "EmbedThumbnail"}) + if format == "thumbnail": + opts["skip_download"] = True + opts["writethumbnail"] = True + opts["postprocessors"].append({"key": "FFmpegThumbnailsConvertor", "format": "jpg", "when": "before_dl"}) + return opts diff --git a/ui/src/app/formats.ts b/ui/src/app/formats.ts index 15be903..a3bf3e8 100644 --- a/ui/src/app/formats.ts +++ b/ui/src/app/formats.ts @@ -43,4 +43,11 @@ export const Formats: Format[] = [ { id: '128', text: '128 kbps' }, ], }, + { + id: 'thumbnail', + text: 'Thumbnail', + qualities: [ + { id: 'best', text: 'Best' } + ], + }, ]; -- cgit From 0f27bf854560fa37a5fa9f9d9645fe4c321fe487 Mon Sep 17 00:00:00 2001 From: 1RandomDev <84292805+1RandomDev@users.noreply.github.com> Date: Wed, 24 Aug 2022 18:10:49 +0200 Subject: Set correct file extension for thumbnails --- app/ytdl.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/ytdl.py b/app/ytdl.py index 38e29b8..8382184 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -6,6 +6,7 @@ import time import asyncio import multiprocessing import logging +import re from dl_formats import get_format, get_opts log = logging.getLogger('ytdl') @@ -124,6 +125,10 @@ class Download: self.tmpfilename = status.get('tmpfilename') if 'filename' in status: self.info.filename = os.path.relpath(status.get('filename'), self.download_dir) + + # Set correct file extension for thumbnails + if(self.info.format == 'thumbnail'): + self.info.filename = re.sub(r'\.webm$', '.jpg', self.info.filename) self.info.status = status['status'] self.info.msg = status.get('msg') if 'downloaded_bytes' in status: -- cgit From e28458a74f28fa9eeed374cc851497deaf90b88d Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 18:25:29 -0400 Subject: Backend: support "folder" POST param and add config options --- app/main.py | 7 +++++-- app/ytdl.py | 33 +++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/app/main.py b/app/main.py index 9fbdf19..ecc5040 100644 --- a/app/main.py +++ b/app/main.py @@ -16,11 +16,13 @@ class Config: _DEFAULTS = { 'DOWNLOAD_DIR': '.', 'AUDIO_DOWNLOAD_DIR': '%%DOWNLOAD_DIR', + 'CUSTOM_DIR': 'true', + 'AUTO_CREATE_CUSTOM_DIR': 'false', 'STATE_DIR': '.', 'URL_PREFIX': '', 'OUTPUT_TEMPLATE': '%(title)s.%(ext)s', 'OUTPUT_TEMPLATE_CHAPTER': '%(title)s - %(section_number)s %(section_title)s.%(ext)s', - 'YTDL_OPTIONS': '{}', + 'YTDL_OPTIONS': '{}' } def __init__(self): @@ -80,7 +82,8 @@ async def add(request): if not url or not quality: raise web.HTTPBadRequest() format = post.get('format') - status = await dqueue.add(url, quality, format) + folder = post.get('folder') + status = await dqueue.add(url, quality, format, folder) return web.Response(text=serializer.encode(status)) @routes.post(config.URL_PREFIX + 'delete') diff --git a/app/ytdl.py b/app/ytdl.py index 21b82da..29fa4bd 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -27,10 +27,11 @@ class DownloadQueueNotifier: raise NotImplementedError class DownloadInfo: - def __init__(self, id, title, url, quality, format): + def __init__(self, id, title, url, quality, format, folder): self.id, self.title, self.url = id, title, url self.quality = quality self.format = format + self.folder = folder self.status = self.msg = self.percent = self.speed = self.eta = None self.filename = None self.timestamp = time.time_ns() @@ -192,7 +193,7 @@ class DownloadQueue: async def __import_queue(self): for k, v in self.queue.saved_items(): - await self.add(v.url, v.quality, v.format) + await self.add(v.url, v.quality, v.format, folder=v.folder) async def initialize(self): self.event = asyncio.Event() @@ -207,7 +208,7 @@ class DownloadQueue: **self.config.YTDL_OPTIONS, }).extract_info(url, download=False) - async def __add_entry(self, entry, quality, format, already): + async def __add_entry(self, entry, quality, format, already, folder=None): etype = entry.get('_type') or 'video' if etype == 'playlist': entries = entry['entries'] @@ -220,14 +221,26 @@ class DownloadQueue: for property in ("id", "title", "uploader", "uploader_id"): if property in entry: etr[f"playlist_{property}"] = entry[property] - results.append(await self.__add_entry(etr, quality, format, already)) + results.append(await self.__add_entry(etr, quality, format, already, folder=folder)) 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 and 'title' in entry: if not self.queue.exists(entry['id']): - dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality, format) - dldirectory = self.config.DOWNLOAD_DIR if (quality != 'audio' and format != 'mp3') else self.config.AUDIO_DOWNLOAD_DIR + dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality, format, folder) + base_directory = self.config.DOWNLOAD_DIR if (quality != 'audio' and format != 'mp3') else self.config.AUDIO_DOWNLOAD_DIR + if folder: + if self.config.CUSTOM_DIR != 'true': + return {'status': 'error', 'msg': f'A folder for the download was specified but CUSTOM_DIR is not true in the configuration.'} + dldirectory = os.path.realpath(os.path.join(base_directory, folder)) + if not dldirectory.startswith(base_directory): + return {'status': 'error', 'msg': f'Folder "{folder}" must resolve inside the base download directory "{base_directory}"'} + if not os.path.isdir(dldirectory): + if self.config.AUTO_CREATE_CUSTOM_DIR != 'true': + return {'status': 'error', 'msg': f'Folder "{folder}" for download does not exist, and AUTO_CREATE_CUSTOM_DIR is not true in the configuration.'} + os.makedirs(dldirectory, exist_ok=True) + else: + dldirectory = base_directory output = self.config.OUTPUT_TEMPLATE output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER for property, value in entry.items(): @@ -238,11 +251,11 @@ class DownloadQueue: await self.notifier.added(dl) return {'status': 'ok'} elif etype.startswith('url'): - return await self.add(entry['url'], quality, format, already) + return await self.add(entry['url'], quality, format, already, folder=folder) return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} - async def add(self, url, quality, format, already=None): - log.info(f'adding {url}') + async def add(self, url, quality, format, already=None, folder=None): + log.info(f'adding {url}: {quality=} {format=} {already=} {folder=}') already = set() if already is None else already if url in already: log.info('recursion detected, skipping') @@ -253,7 +266,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, format, already) + return await self.__add_entry(entry, quality, format, already, folder=folder) async def cancel(self, ids): for id in ids: -- cgit From 47e797cfcbda237f91078082cdefcb19f41d09ff Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 18:26:43 -0400 Subject: update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0e1528a..ed08478 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Certain values can be set via environment variables, using the `-e` parameter on * __GID__: group under which MeTube will run. Defaults to `1000`. * __UMASK__: umask value used by MeTube. Defaults to `022`. * __DOWNLOAD_DIR__: path to where the downloads will be saved. Defaults to `/downloads` in the docker image, and `.` otherwise. +* __CUSTOM_DIR__: whether to enable downloading videos into folders within the __DOWNLOAD_DIR__. Defaults to `true`. +* __AUTO_CREATE_CUSTOM_DIR__: whether to support automatically creating folders within the __DOWNLOAD_DIR__ if they do not exist. Defaults to `false`. * __AUDIO_DOWNLOAD_DIR__: path to where audio-only downloads will be saved, if you wish to separate them from the video downloads. Defaults to the value of `DOWNLOAD_DIR`. * __STATE_DIR__: path to where the queue persistence files will be saved. Defaults to `/downloads/.metube` in the docker image, and `.` otherwise. * __URL_PREFIX__: base path for the web server (for use when hosting behind a reverse proxy). Defaults to `/`. -- cgit From 2d70c2c4ce6272b17eb8893c6c98d81fdedafeba Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 18:30:54 -0400 Subject: Allow running github actions manually --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3f7af43..d43ef5e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,7 @@ on: push: branches: - 'master' + workflow_dispatch: jobs: dockerhub-build-push: -- cgit From 1b146d71494078afd37640806cd8b862edb4fe0a Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 18:32:06 -0400 Subject: support running temporarily on this branch --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d43ef5e..2bef689 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: push: branches: - 'master' - workflow_dispatch: + - 'custom-download-folder' jobs: dockerhub-build-push: -- cgit From 8857878ec2fe6712de46347b5a2d65c64b94ee28 Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 19:01:50 -0400 Subject: show error if static assets are not found --- app/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index ecc5040..dcacfec 100644 --- a/app/main.py +++ b/app/main.py @@ -116,7 +116,13 @@ if config.URL_PREFIX != '/': routes.static(config.URL_PREFIX + 'favicon/', 'favicon') routes.static(config.URL_PREFIX + 'download/', config.DOWNLOAD_DIR) routes.static(config.URL_PREFIX, 'ui/dist/metube') -app.add_routes(routes) +try: + app.add_routes(routes) +except ValueError as e: + if 'ui/dist/metube' in str(e): + raise RuntimeError('Could not find the frontend UI static assets. Please run `node_modules/.bin/ng build`') from e + raise e + # https://github.com/aio-libs/aiohttp/pull/4615 waiting for release -- cgit From bbfde99aeb84d226a0de3ea15a439d18766d7ee4 Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 19:02:00 -0400 Subject: Use angular primitives to toggle --- ui/src/app/app.component.html | 20 ++++++++++++++++---- ui/src/app/app.component.sass | 3 +++ ui/src/app/app.component.ts | 5 +++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 74425aa..0d607d7 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -47,10 +47,22 @@
- +
+ + + +
diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index 517af03..656fac2 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -9,6 +9,9 @@ .add-url-component margin: 0.5rem auto +.add-url-group + width: 100% + button.add-url width: 100% diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index a9b46a3..f9e5a41 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -19,6 +19,7 @@ export class AppComponent implements AfterViewInit { quality: string; format: string; addInProgress = false; + showFolderDropdown = false; darkMode: boolean; @ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent; @@ -114,6 +115,10 @@ export class AppComponent implements AfterViewInit { this.quality = exists ? this.quality : 'best' } + clickFolderDropdown() { + this.showFolderDropdown = !this.showFolderDropdown; + } + addDownload(url?: string, quality?: string, format?: string) { url = url ?? this.addUrl quality = quality ?? this.quality -- cgit From 4a9f55adda55a35c67c5e6699aa71fa56295c9b4 Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 20:27:34 -0400 Subject: Propagate configuration on load via downloads socket --- app/main.py | 1 + ui/src/app/app.component.html | 17 ++++++++--------- ui/src/app/app.component.ts | 14 +++++++++----- ui/src/app/downloads.service.ts | 6 ++++++ 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/main.py b/app/main.py index dcacfec..bbdfc87 100644 --- a/app/main.py +++ b/app/main.py @@ -99,6 +99,7 @@ async def delete(request): @sio.event async def connect(sid, environ): await sio.emit('all', serializer.encode(dqueue.get()), to=sid) + await sio.emit('configuration', serializer.encode(config), to=sid) @routes.get(config.URL_PREFIX) def index(request): diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 0d607d7..e58e007 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -47,20 +47,19 @@
-
+
- -
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index f9e5a41..f48b934 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -18,8 +18,8 @@ export class AppComponent implements AfterViewInit { qualities: Quality[]; quality: string; format: string; + folder: string; addInProgress = false; - showFolderDropdown = false; darkMode: boolean; @ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent; @@ -73,6 +73,14 @@ export class AppComponent implements AfterViewInit { this.cookieService.set('metube_quality', this.quality, { expires: 3650 }); } + showAdvanced() { + return this.downloads.configuration['CUSTOM_DIR'] == 'true'; + } + + folderChanged() { + console.log("folder changed", this.folder); + } + setupTheme(cookieService) { if (cookieService.check('metube_dark')) { this.darkMode = cookieService.get('metube_dark') === "true" @@ -115,10 +123,6 @@ export class AppComponent implements AfterViewInit { this.quality = exists ? this.quality : 'best' } - clickFolderDropdown() { - this.showFolderDropdown = !this.showFolderDropdown; - } - addDownload(url?: string, quality?: string, format?: string) { url = url ?? this.addUrl quality = quality ?? this.quality diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 8580a70..eb7bac3 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -33,6 +33,7 @@ export class DownloadsService { done = new Map(); queueChanged = new Subject(); doneChanged = new Subject(); + configuration = {}; constructor(private http: HttpClient, private socket: MeTubeSocket) { socket.fromEvent('all').subscribe((strdata: string) => { @@ -74,6 +75,11 @@ export class DownloadsService { this.done.delete(data); this.doneChanged.next(null); }); + socket.fromEvent('configuration').subscribe((strdata: string) => { + let data: string = JSON.parse(strdata); + console.debug("got configuration:", data); + this.configuration = data; + }) } handleHTTPError(error: HttpErrorResponse) { -- cgit From ebb62e370ab944dd232bca571ed375f1de9a318b Mon Sep 17 00:00:00 2001 From: PikuZheng Date: Tue, 30 Aug 2022 08:40:01 +0800 Subject: add autocomplete="off" spellcheck="false" #175 it's also useful on android --- ui/src/app/app.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 74425aa..e96ee0d 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -26,7 +26,7 @@
- +
-- cgit From f79c8fa7542822cd3edd542d646fc5881d3bb80e Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 20:41:21 -0400 Subject: pass custom_directories from server to client --- app/main.py | 15 ++++++++++++++- ui/src/app/app.component.sass | 3 +++ ui/src/app/downloads.service.ts | 10 ++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index bbdfc87..56adc00 100644 --- a/app/main.py +++ b/app/main.py @@ -7,6 +7,7 @@ from aiohttp import web import socketio import logging import json +import pathlib from ytdl import DownloadQueueNotifier, DownloadQueue @@ -100,6 +101,18 @@ async def delete(request): async def connect(sid, environ): await sio.emit('all', serializer.encode(dqueue.get()), to=sid) await sio.emit('configuration', serializer.encode(config), to=sid) + if config.CUSTOM_DIR: + await sio.emit('custom_directories', serializer.encode(get_custom_directories()), to=sid) + +def get_custom_directories(): + path = pathlib.Path(config.DOWNLOAD_DIR) + # Recursively lists all subdirectories, and converts PosixPath objects to string + dirs = list(map(str, path.glob('**'))) + + if '.' in dirs: + dirs.remove('.') + + return {"directories": dirs} @routes.get(config.URL_PREFIX) def index(request): @@ -121,7 +134,7 @@ try: app.add_routes(routes) except ValueError as e: if 'ui/dist/metube' in str(e): - raise RuntimeError('Could not find the frontend UI static assets. Please run `node_modules/.bin/ng build`') from e + raise RuntimeError('Could not find the frontend UI static assets. Please run `node_modules/.bin/ng build` inside the ui folder') from e raise e diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index 656fac2..d95fc0d 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -15,6 +15,9 @@ button.add-url width: 100% +.folder-dropdown-menu + width: 500px + $metube-section-color-bg: rgba(0,0,0,.07) .metube-section-header diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index eb7bac3..018f225 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -34,6 +34,7 @@ export class DownloadsService { queueChanged = new Subject(); doneChanged = new Subject(); configuration = {}; + custom_directories = {}; constructor(private http: HttpClient, private socket: MeTubeSocket) { socket.fromEvent('all').subscribe((strdata: string) => { @@ -76,10 +77,15 @@ export class DownloadsService { this.doneChanged.next(null); }); socket.fromEvent('configuration').subscribe((strdata: string) => { - let data: string = JSON.parse(strdata); + let data = JSON.parse(strdata); console.debug("got configuration:", data); this.configuration = data; - }) + }); + socket.fromEvent('custom_directories').subscribe((strdata: string) => { + let data = JSON.parse(strdata); + console.debug("got custom_directories:", data); + this.custom_directories = data["directories"]; + }); } handleHTTPError(error: HttpErrorResponse) { -- cgit From 67be71cdeb3c7b33a8397d4cf91ab74075749063 Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 21:11:25 -0400 Subject: load selectize --- ui/angular.json | 11 ++- ui/package-lock.json | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++- ui/package.json | 1 + ui/src/styles.sass | 2 +- 4 files changed, 221 insertions(+), 5 deletions(-) diff --git a/ui/angular.json b/ui/angular.json index 5639d48..3c9a781 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -30,7 +30,16 @@ "styles": [ "src/styles.sass" ], - "scripts": [] + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/@selectize/selectize/dist/scss/selectize.bootstrap5.scss" + ] + }, + "scripts": [ + "node_modules/jquery/dist/jquery.min.js", + "node_modules/bootstrap/dist/js/bootstrap.min.js", + "node_modules/@selectize/selectize/dist/js/standalone/selectize.min.js" + ] }, "configurations": { "production": { diff --git a/ui/package-lock.json b/ui/package-lock.json index e8c9fd1..8bbb724 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -22,6 +22,7 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@ng-bootstrap/ng-bootstrap": "^12.0.0", + "@selectize/selectize": "^0.13.6", "bootstrap": "^5.0.0", "ngx-cookie-service": "^13.0.0", "ngx-socket-io": "^4.2.0", @@ -2688,6 +2689,44 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@selectize/selectize": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/@selectize/selectize/-/selectize-0.13.6.tgz", + "integrity": "sha512-UVkHH92l/4zLH/acfukry419K2yAKiDf6VmFQ9hZzMpWW2epE7xJIgK47mJLjAQnimP0ROo8BStfwelWcoTQyg==", + "dependencies": { + "@selectize/sifter": "^0.6.2", + "microplugin": "0.0.3" + }, + "engines": { + "node": "*" + }, + "optionalDependencies": { + "jquery-ui": "^1.13.0" + }, + "peerDependencies": { + "jquery": "^1.7.0 || ^2 || ^3" + } + }, + "node_modules/@selectize/sifter": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@selectize/sifter/-/sifter-0.6.2.tgz", + "integrity": "sha512-rpWQuzaCEV2GOEfBmP5NGST3IsA/RGRd3vugOGQVxAaN1xMI9tsrMmx5rC811iInq6OeFH3Jix5EfglYQ3XwKQ==", + "dependencies": { + "async": "^3.2.3", + "cardinal": "^2.1.1", + "csv-parse": "^5.0.4", + "humanize": "^0.0.9", + "optimist": "^0.5.2" + }, + "bin": { + "sifter": "bin/sifter.js" + } + }, + "node_modules/@selectize/sifter/node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -3299,6 +3338,11 @@ "node": ">=4" } }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==" + }, "node_modules/anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -3901,6 +3945,18 @@ } ] }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4704,6 +4760,11 @@ "node": ">=4" } }, + "node_modules/csv-parse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.0.tgz", + "integrity": "sha512-UXJCGwvJ2fep39purtAn27OUYmxB1JQto+zhZ4QlJpzsirtSFbzLvip1aIgziqNdZp/TptvsKEV5BZSxe10/DQ==" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5550,7 +5611,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -6411,6 +6471,14 @@ "node": ">=10.17.0" } }, + "node_modules/humanize": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/humanize/-/humanize-0.0.9.tgz", + "integrity": "sha512-bvZZ7vXpr1RKoImjuQ45hJb5OvE2oJafHysiD/AL3nkqTZH2hFCjQ3YZfCd63FefDitbJze/ispUPP0gfDsT2Q==", + "engines": { + "node": "*" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -7004,6 +7072,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jquery": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz", + "integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==" + }, + "node_modules/jquery-ui": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.2.tgz", + "integrity": "sha512-wBZPnqWs5GaYJmo1Jj0k/mrSkzdQzKDwhXNtHKcBdAcKVxMM3KNYFq+iJ2i1rwiG53Z8M4mTn3Qxrm17uH1D4Q==", + "optional": true, + "dependencies": { + "jquery": ">=1.8.0 <4.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7486,6 +7568,14 @@ "node": ">=8.6" } }, + "node_modules/microplugin": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/microplugin/-/microplugin-0.0.3.tgz", + "integrity": "sha512-3wKXex4/iyALV0GX2juow66J9dabkEMgHeZAihdLTaRTzm0N+RubXioNPpfIQDPuBRxr3JbjNt7B0Lr/3yE9yQ==", + "engines": { + "node": "*" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -8368,6 +8458,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optimist": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.5.2.tgz", + "integrity": "sha512-r9M8ZpnM9SXV5Wii7TCqienfcaY3tAiJe9Jchof87icbmbruKgK0xKXngmrnowTDnEawmmI1Qbha59JEoBkBGA==", + "dependencies": { + "wordwrap": "~0.0.2" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -9572,6 +9670,14 @@ "node": ">=8.10.0" } }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "dependencies": { + "esprima": "~4.0.0" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -11516,6 +11622,14 @@ "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "node_modules/wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -13520,6 +13634,35 @@ "jsonc-parser": "3.0.0" } }, + "@selectize/selectize": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/@selectize/selectize/-/selectize-0.13.6.tgz", + "integrity": "sha512-UVkHH92l/4zLH/acfukry419K2yAKiDf6VmFQ9hZzMpWW2epE7xJIgK47mJLjAQnimP0ROo8BStfwelWcoTQyg==", + "requires": { + "@selectize/sifter": "^0.6.2", + "jquery-ui": "^1.13.0", + "microplugin": "0.0.3" + } + }, + "@selectize/sifter": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@selectize/sifter/-/sifter-0.6.2.tgz", + "integrity": "sha512-rpWQuzaCEV2GOEfBmP5NGST3IsA/RGRd3vugOGQVxAaN1xMI9tsrMmx5rC811iInq6OeFH3Jix5EfglYQ3XwKQ==", + "requires": { + "async": "^3.2.3", + "cardinal": "^2.1.1", + "csv-parse": "^5.0.4", + "humanize": "^0.0.9", + "optimist": "^0.5.2" + }, + "dependencies": { + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + } + } + }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -14065,6 +14208,11 @@ "color-convert": "^1.9.0" } }, + "ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==" + }, "anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -14510,6 +14658,15 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001358.tgz", "integrity": "sha512-hvp8PSRymk85R20bsDra7ZTCpSVGN/PAz9pSAjPSjKC+rNmnUk5vCRgJwiTT/O4feQ/yu/drvZYpKxxhbFuChw==" }, + "cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "requires": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -15110,6 +15267,11 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "csv-parse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.0.tgz", + "integrity": "sha512-UXJCGwvJ2fep39purtAn27OUYmxB1JQto+zhZ4QlJpzsirtSFbzLvip1aIgziqNdZp/TptvsKEV5BZSxe10/DQ==" + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -15661,8 +15823,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esrecurse": { "version": "4.3.0", @@ -16317,6 +16478,11 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "humanize": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/humanize/-/humanize-0.0.9.tgz", + "integrity": "sha512-bvZZ7vXpr1RKoImjuQ45hJb5OvE2oJafHysiD/AL3nkqTZH2hFCjQ3YZfCd63FefDitbJze/ispUPP0gfDsT2Q==" + }, "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -16737,6 +16903,20 @@ } } }, + "jquery": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz", + "integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==" + }, + "jquery-ui": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.2.tgz", + "integrity": "sha512-wBZPnqWs5GaYJmo1Jj0k/mrSkzdQzKDwhXNtHKcBdAcKVxMM3KNYFq+iJ2i1rwiG53Z8M4mTn3Qxrm17uH1D4Q==", + "optional": true, + "requires": { + "jquery": ">=1.8.0 <4.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17096,6 +17276,11 @@ "picomatch": "^2.3.1" } }, + "microplugin": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/microplugin/-/microplugin-0.0.3.tgz", + "integrity": "sha512-3wKXex4/iyALV0GX2juow66J9dabkEMgHeZAihdLTaRTzm0N+RubXioNPpfIQDPuBRxr3JbjNt7B0Lr/3yE9yQ==" + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -17761,6 +17946,14 @@ "is-wsl": "^2.2.0" } }, + "optimist": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.5.2.tgz", + "integrity": "sha512-r9M8ZpnM9SXV5Wii7TCqienfcaY3tAiJe9Jchof87icbmbruKgK0xKXngmrnowTDnEawmmI1Qbha59JEoBkBGA==", + "requires": { + "wordwrap": "~0.0.2" + } + }, "ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -18583,6 +18776,14 @@ "picomatch": "^2.2.1" } }, + "redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "requires": { + "esprima": "~4.0.0" + } + }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -20016,6 +20217,11 @@ "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==" + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index 14cd4b3..3ea2e71 100644 --- a/ui/package.json +++ b/ui/package.json @@ -25,6 +25,7 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@ng-bootstrap/ng-bootstrap": "^12.0.0", + "@selectize/selectize": "^0.13.6", "bootstrap": "^5.0.0", "ngx-cookie-service": "^13.0.0", "ngx-socket-io": "^4.2.0", diff --git a/ui/src/styles.sass b/ui/src/styles.sass index 5d54ecb..68bef4f 100644 --- a/ui/src/styles.sass +++ b/ui/src/styles.sass @@ -1,4 +1,4 @@ /* You can add global styles to this file, and also import other style files */ /* Importing Bootstrap SCSS file. */ -@import '~bootstrap/scss/bootstrap' +@import '~bootstrap/scss/bootstrap' \ No newline at end of file -- cgit From 8abacc2a3610701e6258d9c79ee0c577ad2b2376 Mon Sep 17 00:00:00 2001 From: James Woglom Date: Mon, 29 Aug 2022 21:52:54 -0400 Subject: almost functional with selectize --- ui/src/app/app.component.html | 3 ++- ui/src/app/app.component.ts | 16 ++++++++++++++-- ui/src/app/downloads.service.ts | 5 +++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index e58e007..8af200c 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -58,7 +58,8 @@
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index f48b934..052e182 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { Component, ViewChild, ElementRef, AfterViewInit, ChangeDetectorRef } from '@angular/core'; import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; import { faRedoAlt, faSun, faMoon, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; import { CookieService } from 'ngx-cookie-service'; @@ -7,10 +7,13 @@ import { DownloadsService, Status } from './downloads.service'; import { MasterCheckboxComponent } from './master-checkbox.component'; import { Formats, Format, Quality } from './formats'; +// jQuery is loaded in angular.json for selectize +declare var $: any; + @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.sass'] + styleUrls: ['./app.component.sass'], }) export class AppComponent implements AfterViewInit { addUrl: string; @@ -19,6 +22,7 @@ export class AppComponent implements AfterViewInit { quality: string; format: string; folder: string; + customDirs: string[] = []; addInProgress = false; darkMode: boolean; @@ -28,6 +32,7 @@ export class AppComponent implements AfterViewInit { @ViewChild('doneDelSelected') doneDelSelected: ElementRef; @ViewChild('doneClearCompleted') doneClearCompleted: ElementRef; @ViewChild('doneClearFailed') doneClearFailed: ElementRef; + @ViewChild('folderSelect') folderSelect: ElementRef; faTrashAlt = faTrashAlt; faCheckCircle = faCheckCircle; @@ -46,6 +51,13 @@ export class AppComponent implements AfterViewInit { } ngAfterViewInit() { + // Trigger folderSelect to update + this.downloads.customDirsChanged.subscribe((dirs: string[]) => { + console.log("customDirsChanged:", dirs); + $(this.folderSelect.nativeElement).selectize({options: dirs}); + this.customDirs = dirs; + }); + this.downloads.queueChanged.subscribe(() => { this.queueMasterCheckbox.selectionChanged(); }); diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 018f225..42ffe6d 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -33,8 +33,8 @@ export class DownloadsService { done = new Map(); queueChanged = new Subject(); doneChanged = new Subject(); + customDirsChanged = new Subject(); configuration = {}; - custom_directories = {}; constructor(private http: HttpClient, private socket: MeTubeSocket) { socket.fromEvent('all').subscribe((strdata: string) => { @@ -84,7 +84,8 @@ export class DownloadsService { socket.fromEvent('custom_directories').subscribe((strdata: string) => { let data = JSON.parse(strdata); console.debug("got custom_directories:", data); - this.custom_directories = data["directories"]; + let customDirectories = data["directories"]; + this.customDirsChanged.next(customDirectories); }); } -- cgit From ba712fc071398e615ead822c8bd81aad42a90c8f Mon Sep 17 00:00:00 2001 From: James Woglom Date: Tue, 30 Aug 2022 00:55:16 -0400 Subject: Fill in download_dir or audio_download_dir on launch --- README.md | 4 +- app/main.py | 43 ++++++-- app/ytdl.py | 9 +- ui/angular.json | 3 - ui/package-lock.json | 238 +++++----------------------------------- ui/package.json | 2 +- ui/src/app/app.component.html | 3 +- ui/src/app/app.component.sass | 5 + ui/src/app/app.component.ts | 39 ++++--- ui/src/app/app.module.ts | 4 +- ui/src/app/downloads.service.ts | 11 +- ui/src/styles.sass | 3 +- 12 files changed, 108 insertions(+), 256 deletions(-) diff --git a/README.md b/README.md index ed08478..692ccd5 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ Certain values can be set via environment variables, using the `-e` parameter on * __GID__: group under which MeTube will run. Defaults to `1000`. * __UMASK__: umask value used by MeTube. Defaults to `022`. * __DOWNLOAD_DIR__: path to where the downloads will be saved. Defaults to `/downloads` in the docker image, and `.` otherwise. -* __CUSTOM_DIR__: whether to enable downloading videos into folders within the __DOWNLOAD_DIR__. Defaults to `true`. -* __AUTO_CREATE_CUSTOM_DIR__: whether to support automatically creating folders within the __DOWNLOAD_DIR__ if they do not exist. Defaults to `false`. * __AUDIO_DOWNLOAD_DIR__: path to where audio-only downloads will be saved, if you wish to separate them from the video downloads. Defaults to the value of `DOWNLOAD_DIR`. +* __CUSTOM_DIRS__: whether to enable downloading videos into custom directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__). When enabled, a drop-down appears next to the Add button to specify the download directory. Defaults to `true`. +* __CREATE_DIRS__: whether to support automatically creating directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) if they do not exist. When enabled, the download directory selector becomes supports free-text input, and the specified directory will be created recursively. Defaults to `false`. * __STATE_DIR__: path to where the queue persistence files will be saved. Defaults to `/downloads/.metube` in the docker image, and `.` otherwise. * __URL_PREFIX__: base path for the web server (for use when hosting behind a reverse proxy). Defaults to `/`. * __OUTPUT_TEMPLATE__: the template for the filenames of the downloaded videos, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s.%(ext)s`. diff --git a/app/main.py b/app/main.py index 56adc00..de13f1e 100644 --- a/app/main.py +++ b/app/main.py @@ -17,8 +17,8 @@ class Config: _DEFAULTS = { 'DOWNLOAD_DIR': '.', 'AUDIO_DOWNLOAD_DIR': '%%DOWNLOAD_DIR', - 'CUSTOM_DIR': 'true', - 'AUTO_CREATE_CUSTOM_DIR': 'false', + 'CUSTOM_DIRS': 'true', + 'CREATE_DIRS': 'false', 'STATE_DIR': '.', 'URL_PREFIX': '', 'OUTPUT_TEMPLATE': '%(title)s.%(ext)s', @@ -101,18 +101,39 @@ async def delete(request): async def connect(sid, environ): await sio.emit('all', serializer.encode(dqueue.get()), to=sid) await sio.emit('configuration', serializer.encode(config), to=sid) - if config.CUSTOM_DIR: - await sio.emit('custom_directories', serializer.encode(get_custom_directories()), to=sid) + if config.CUSTOM_DIRS: + await sio.emit('custom_dirs', serializer.encode(get_custom_dirs()), to=sid) -def get_custom_directories(): - path = pathlib.Path(config.DOWNLOAD_DIR) - # Recursively lists all subdirectories, and converts PosixPath objects to string - dirs = list(map(str, path.glob('**'))) +def get_custom_dirs(): + def recursive_dirs(base): + path = pathlib.Path(base) + + # Converts PosixPath object to string, and remove base/ prefix + def convert(p): + s = str(p) + if s.startswith(base): + s = s[len(base):] + + if s.startswith('/'): + s = s[1:] + + return s + + # Recursively lists all subdirectories of DOWNLOAD_DIR + dirs = list(filter(None, map(convert, path.glob('**')))) + + return dirs - if '.' in dirs: - dirs.remove('.') + download_dir = recursive_dirs(config.DOWNLOAD_DIR) - return {"directories": dirs} + audio_download_dir = download_dir + if config.DOWNLOAD_DIR != config.AUDIO_DOWNLOAD_DIR: + audio_download_dir = recursive_dirs(config.AUDIO_DOWNLOAD_DIR) + + return { + "download_dir": download_dir, + "audio_download_dir": audio_download_dir + } @routes.get(config.URL_PREFIX) def index(request): diff --git a/app/ytdl.py b/app/ytdl.py index 29fa4bd..4329147 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -228,16 +228,17 @@ class DownloadQueue: elif etype == 'video' or etype.startswith('url') and 'id' in entry and 'title' in entry: if not self.queue.exists(entry['id']): dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality, format, folder) + # Keep consistent with frontend base_directory = self.config.DOWNLOAD_DIR if (quality != 'audio' and format != 'mp3') else self.config.AUDIO_DOWNLOAD_DIR if folder: - if self.config.CUSTOM_DIR != 'true': - return {'status': 'error', 'msg': f'A folder for the download was specified but CUSTOM_DIR is not true in the configuration.'} + if self.config.CUSTOM_DIRS != 'true': + return {'status': 'error', 'msg': f'A folder for the download was specified but CUSTOM_DIRS is not true in the configuration.'} dldirectory = os.path.realpath(os.path.join(base_directory, folder)) if not dldirectory.startswith(base_directory): return {'status': 'error', 'msg': f'Folder "{folder}" must resolve inside the base download directory "{base_directory}"'} if not os.path.isdir(dldirectory): - if self.config.AUTO_CREATE_CUSTOM_DIR != 'true': - return {'status': 'error', 'msg': f'Folder "{folder}" for download does not exist, and AUTO_CREATE_CUSTOM_DIR is not true in the configuration.'} + if self.config.CREATE_DIRS != 'true': + return {'status': 'error', 'msg': f'Folder "{folder}" for download does not exist inside base directory "{base_directory}", and CREATE_DIRS is not true in the configuration.'} os.makedirs(dldirectory, exist_ok=True) else: dldirectory = base_directory diff --git a/ui/angular.json b/ui/angular.json index 3c9a781..1529f3f 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -32,13 +32,10 @@ ], "stylePreprocessorOptions": { "includePaths": [ - "node_modules/@selectize/selectize/dist/scss/selectize.bootstrap5.scss" ] }, "scripts": [ - "node_modules/jquery/dist/jquery.min.js", "node_modules/bootstrap/dist/js/bootstrap.min.js", - "node_modules/@selectize/selectize/dist/js/standalone/selectize.min.js" ] }, "configurations": { diff --git a/ui/package-lock.json b/ui/package-lock.json index 8bbb724..fbcb8a4 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -22,7 +22,7 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@ng-bootstrap/ng-bootstrap": "^12.0.0", - "@selectize/selectize": "^0.13.6", + "@ng-select/ng-select": "^8.3.0", "bootstrap": "^5.0.0", "ngx-cookie-service": "^13.0.0", "ngx-socket-io": "^4.2.0", @@ -2530,6 +2530,23 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@ng-select/ng-select": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-8.3.0.tgz", + "integrity": "sha512-AwAuDs+86++D2kEsik2/ZiQuRk0khd1HESofOm1yMBwbzsw+xLSyVOMml/OehDFSOxli7fAkk07wYtzAhxSB3Q==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 12.20.0", + "npm": ">= 6.0.0" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0 <14.0.0", + "@angular/core": ">=13.0.0 <14.0.0", + "@angular/forms": ">=13.0.0 <14.0.0" + } + }, "node_modules/@ngtools/webpack": { "version": "13.3.8", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.8.tgz", @@ -2689,44 +2706,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@selectize/selectize": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/@selectize/selectize/-/selectize-0.13.6.tgz", - "integrity": "sha512-UVkHH92l/4zLH/acfukry419K2yAKiDf6VmFQ9hZzMpWW2epE7xJIgK47mJLjAQnimP0ROo8BStfwelWcoTQyg==", - "dependencies": { - "@selectize/sifter": "^0.6.2", - "microplugin": "0.0.3" - }, - "engines": { - "node": "*" - }, - "optionalDependencies": { - "jquery-ui": "^1.13.0" - }, - "peerDependencies": { - "jquery": "^1.7.0 || ^2 || ^3" - } - }, - "node_modules/@selectize/sifter": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@selectize/sifter/-/sifter-0.6.2.tgz", - "integrity": "sha512-rpWQuzaCEV2GOEfBmP5NGST3IsA/RGRd3vugOGQVxAaN1xMI9tsrMmx5rC811iInq6OeFH3Jix5EfglYQ3XwKQ==", - "dependencies": { - "async": "^3.2.3", - "cardinal": "^2.1.1", - "csv-parse": "^5.0.4", - "humanize": "^0.0.9", - "optimist": "^0.5.2" - }, - "bin": { - "sifter": "bin/sifter.js" - } - }, - "node_modules/@selectize/sifter/node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -3338,11 +3317,6 @@ "node": ">=4" } }, - "node_modules/ansicolors": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", - "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==" - }, "node_modules/anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -3945,18 +3919,6 @@ } ] }, - "node_modules/cardinal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", - "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", - "dependencies": { - "ansicolors": "~0.3.2", - "redeyed": "~2.1.0" - }, - "bin": { - "cdl": "bin/cdl.js" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4760,11 +4722,6 @@ "node": ">=4" } }, - "node_modules/csv-parse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.0.tgz", - "integrity": "sha512-UXJCGwvJ2fep39purtAn27OUYmxB1JQto+zhZ4QlJpzsirtSFbzLvip1aIgziqNdZp/TptvsKEV5BZSxe10/DQ==" - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5611,6 +5568,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -6471,14 +6429,6 @@ "node": ">=10.17.0" } }, - "node_modules/humanize": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/humanize/-/humanize-0.0.9.tgz", - "integrity": "sha512-bvZZ7vXpr1RKoImjuQ45hJb5OvE2oJafHysiD/AL3nkqTZH2hFCjQ3YZfCd63FefDitbJze/ispUPP0gfDsT2Q==", - "engines": { - "node": "*" - } - }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -7072,20 +7022,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jquery": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz", - "integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==" - }, - "node_modules/jquery-ui": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.2.tgz", - "integrity": "sha512-wBZPnqWs5GaYJmo1Jj0k/mrSkzdQzKDwhXNtHKcBdAcKVxMM3KNYFq+iJ2i1rwiG53Z8M4mTn3Qxrm17uH1D4Q==", - "optional": true, - "dependencies": { - "jquery": ">=1.8.0 <4.0.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7568,14 +7504,6 @@ "node": ">=8.6" } }, - "node_modules/microplugin": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/microplugin/-/microplugin-0.0.3.tgz", - "integrity": "sha512-3wKXex4/iyALV0GX2juow66J9dabkEMgHeZAihdLTaRTzm0N+RubXioNPpfIQDPuBRxr3JbjNt7B0Lr/3yE9yQ==", - "engines": { - "node": "*" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -8458,14 +8386,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optimist": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.5.2.tgz", - "integrity": "sha512-r9M8ZpnM9SXV5Wii7TCqienfcaY3tAiJe9Jchof87icbmbruKgK0xKXngmrnowTDnEawmmI1Qbha59JEoBkBGA==", - "dependencies": { - "wordwrap": "~0.0.2" - } - }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -9670,14 +9590,6 @@ "node": ">=8.10.0" } }, - "node_modules/redeyed": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", - "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", - "dependencies": { - "esprima": "~4.0.0" - } - }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -11622,14 +11534,6 @@ "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, - "node_modules/wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -13511,6 +13415,14 @@ "tslib": "^2.3.0" } }, + "@ng-select/ng-select": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-8.3.0.tgz", + "integrity": "sha512-AwAuDs+86++D2kEsik2/ZiQuRk0khd1HESofOm1yMBwbzsw+xLSyVOMml/OehDFSOxli7fAkk07wYtzAhxSB3Q==", + "requires": { + "tslib": "^2.3.1" + } + }, "@ngtools/webpack": { "version": "13.3.8", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.8.tgz", @@ -13634,35 +13546,6 @@ "jsonc-parser": "3.0.0" } }, - "@selectize/selectize": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/@selectize/selectize/-/selectize-0.13.6.tgz", - "integrity": "sha512-UVkHH92l/4zLH/acfukry419K2yAKiDf6VmFQ9hZzMpWW2epE7xJIgK47mJLjAQnimP0ROo8BStfwelWcoTQyg==", - "requires": { - "@selectize/sifter": "^0.6.2", - "jquery-ui": "^1.13.0", - "microplugin": "0.0.3" - } - }, - "@selectize/sifter": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@selectize/sifter/-/sifter-0.6.2.tgz", - "integrity": "sha512-rpWQuzaCEV2GOEfBmP5NGST3IsA/RGRd3vugOGQVxAaN1xMI9tsrMmx5rC811iInq6OeFH3Jix5EfglYQ3XwKQ==", - "requires": { - "async": "^3.2.3", - "cardinal": "^2.1.1", - "csv-parse": "^5.0.4", - "humanize": "^0.0.9", - "optimist": "^0.5.2" - }, - "dependencies": { - "async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" - } - } - }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -14208,11 +14091,6 @@ "color-convert": "^1.9.0" } }, - "ansicolors": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", - "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==" - }, "anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -14658,15 +14536,6 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001358.tgz", "integrity": "sha512-hvp8PSRymk85R20bsDra7ZTCpSVGN/PAz9pSAjPSjKC+rNmnUk5vCRgJwiTT/O4feQ/yu/drvZYpKxxhbFuChw==" }, - "cardinal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", - "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", - "requires": { - "ansicolors": "~0.3.2", - "redeyed": "~2.1.0" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -15267,11 +15136,6 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, - "csv-parse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.0.tgz", - "integrity": "sha512-UXJCGwvJ2fep39purtAn27OUYmxB1JQto+zhZ4QlJpzsirtSFbzLvip1aIgziqNdZp/TptvsKEV5BZSxe10/DQ==" - }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -15823,7 +15687,8 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true }, "esrecurse": { "version": "4.3.0", @@ -16478,11 +16343,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, - "humanize": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/humanize/-/humanize-0.0.9.tgz", - "integrity": "sha512-bvZZ7vXpr1RKoImjuQ45hJb5OvE2oJafHysiD/AL3nkqTZH2hFCjQ3YZfCd63FefDitbJze/ispUPP0gfDsT2Q==" - }, "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -16903,20 +16763,6 @@ } } }, - "jquery": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz", - "integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==" - }, - "jquery-ui": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.13.2.tgz", - "integrity": "sha512-wBZPnqWs5GaYJmo1Jj0k/mrSkzdQzKDwhXNtHKcBdAcKVxMM3KNYFq+iJ2i1rwiG53Z8M4mTn3Qxrm17uH1D4Q==", - "optional": true, - "requires": { - "jquery": ">=1.8.0 <4.0.0" - } - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17276,11 +17122,6 @@ "picomatch": "^2.3.1" } }, - "microplugin": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/microplugin/-/microplugin-0.0.3.tgz", - "integrity": "sha512-3wKXex4/iyALV0GX2juow66J9dabkEMgHeZAihdLTaRTzm0N+RubXioNPpfIQDPuBRxr3JbjNt7B0Lr/3yE9yQ==" - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -17946,14 +17787,6 @@ "is-wsl": "^2.2.0" } }, - "optimist": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.5.2.tgz", - "integrity": "sha512-r9M8ZpnM9SXV5Wii7TCqienfcaY3tAiJe9Jchof87icbmbruKgK0xKXngmrnowTDnEawmmI1Qbha59JEoBkBGA==", - "requires": { - "wordwrap": "~0.0.2" - } - }, "ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -18776,14 +18609,6 @@ "picomatch": "^2.2.1" } }, - "redeyed": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", - "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", - "requires": { - "esprima": "~4.0.0" - } - }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -20217,11 +20042,6 @@ "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==" - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index 3ea2e71..b46cd94 100644 --- a/ui/package.json +++ b/ui/package.json @@ -25,7 +25,7 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@ng-bootstrap/ng-bootstrap": "^12.0.0", - "@selectize/selectize": "^0.13.6", + "@ng-select/ng-select": "^8.3.0", "bootstrap": "^5.0.0", "ngx-cookie-service": "^13.0.0", "ngx-socket-io": "^4.2.0", diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 8af200c..23dccce 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -58,8 +58,7 @@
diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index d95fc0d..0f98556 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -18,6 +18,11 @@ button.add-url .folder-dropdown-menu width: 500px +.folder-dropdown-menu .input-group + display: flex + padding-left: 5px + padding-right: 5px + $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 052e182..e716395 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,15 +1,13 @@ -import { Component, ViewChild, ElementRef, AfterViewInit, ChangeDetectorRef } from '@angular/core'; +import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { faTrashAlt, faCheckCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; import { faRedoAlt, faSun, faMoon, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; import { CookieService } from 'ngx-cookie-service'; +import { map, Observable, of } from 'rxjs'; import { DownloadsService, Status } from './downloads.service'; import { MasterCheckboxComponent } from './master-checkbox.component'; import { Formats, Format, Quality } from './formats'; -// jQuery is loaded in angular.json for selectize -declare var $: any; - @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -22,9 +20,9 @@ export class AppComponent implements AfterViewInit { quality: string; format: string; folder: string; - customDirs: string[] = []; addInProgress = false; darkMode: boolean; + customDirs$: Observable; @ViewChild('queueMasterCheckbox') queueMasterCheckbox: MasterCheckboxComponent; @ViewChild('queueDelSelected') queueDelSelected: ElementRef; @@ -32,7 +30,6 @@ export class AppComponent implements AfterViewInit { @ViewChild('doneDelSelected') doneDelSelected: ElementRef; @ViewChild('doneClearCompleted') doneClearCompleted: ElementRef; @ViewChild('doneClearFailed') doneClearFailed: ElementRef; - @ViewChild('folderSelect') folderSelect: ElementRef; faTrashAlt = faTrashAlt; faCheckCircle = faCheckCircle; @@ -50,14 +47,11 @@ export class AppComponent implements AfterViewInit { this.setupTheme(cookieService) } - ngAfterViewInit() { - // Trigger folderSelect to update - this.downloads.customDirsChanged.subscribe((dirs: string[]) => { - console.log("customDirsChanged:", dirs); - $(this.folderSelect.nativeElement).selectize({options: dirs}); - this.customDirs = dirs; - }); + ngOnInit() { + this.customDirs$ = this.getMatchingCustomDir(); + } + ngAfterViewInit() { this.downloads.queueChanged.subscribe(() => { this.queueMasterCheckbox.selectionChanged(); }); @@ -86,11 +80,24 @@ export class AppComponent implements AfterViewInit { } showAdvanced() { - return this.downloads.configuration['CUSTOM_DIR'] == 'true'; + return this.downloads.configuration['CUSTOM_DIRS'] == 'true'; } - folderChanged() { - console.log("folder changed", this.folder); + allowCustomDir() { + return this.downloads.configuration['CREATE_DIRS'] == 'true'; + } + + getMatchingCustomDir() : Observable { + return this.downloads.customDirs.asObservable().pipe(map((output) => { + // Keep logic consistent with app/ytdl.py + if (this.quality != 'audio' && this.format != 'mp3') { + console.debug("download_dir", output["download_dir"]) + return output["download_dir"]; + } else { + console.debug("audio_download_dir", output["audio_download_dir"]) + return output["audio_download_dir"]; + } + })); } setupTheme(cookieService) { diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 6ad5978..8eddbca 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -10,6 +10,7 @@ import { AppComponent } from './app.component'; import { EtaPipe, SpeedPipe, EncodeURIComponent } from './downloads.pipe'; import { MasterCheckboxComponent, SlaveCheckboxComponent } from './master-checkbox.component'; import { MeTubeSocket } from './metube-socket'; +import { NgSelectModule } from '@ng-select/ng-select'; @NgModule({ declarations: [ @@ -25,7 +26,8 @@ import { MeTubeSocket } from './metube-socket'; FormsModule, NgbModule, HttpClientModule, - FontAwesomeModule + FontAwesomeModule, + NgSelectModule ], providers: [CookieService, MeTubeSocket], bootstrap: [AppComponent] diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 42ffe6d..1fd75e3 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { of, Subject } from 'rxjs'; +import { Observable, of, Subject } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { MeTubeSocket } from './metube-socket'; @@ -33,7 +33,7 @@ export class DownloadsService { done = new Map(); queueChanged = new Subject(); doneChanged = new Subject(); - customDirsChanged = new Subject(); + customDirs = new Subject>(); configuration = {}; constructor(private http: HttpClient, private socket: MeTubeSocket) { @@ -81,11 +81,10 @@ export class DownloadsService { console.debug("got configuration:", data); this.configuration = data; }); - socket.fromEvent('custom_directories').subscribe((strdata: string) => { + socket.fromEvent('custom_dirs').subscribe((strdata: string) => { let data = JSON.parse(strdata); - console.debug("got custom_directories:", data); - let customDirectories = data["directories"]; - this.customDirsChanged.next(customDirectories); + console.debug("got custom_dirs:", data); + this.customDirs.next(data); }); } diff --git a/ui/src/styles.sass b/ui/src/styles.sass index 68bef4f..4ce00cc 100644 --- a/ui/src/styles.sass +++ b/ui/src/styles.sass @@ -1,4 +1,5 @@ /* You can add global styles to this file, and also import other style files */ /* Importing Bootstrap SCSS file. */ -@import '~bootstrap/scss/bootstrap' \ No newline at end of file +@import '~bootstrap/scss/bootstrap' +@import '~@ng-select/ng-select/themes/default.theme.css' \ No newline at end of file -- cgit From 52e3307d99b6f809c6a7a281b2a33bbc67c0e006 Mon Sep 17 00:00:00 2001 From: James Woglom Date: Tue, 30 Aug 2022 00:58:19 -0400 Subject: switch between audio and default custom directories on change --- ui/src/app/app.component.ts | 10 +++++++--- ui/src/app/downloads.service.ts | 7 +++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index e716395..cdcd8f6 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -77,6 +77,8 @@ export class AppComponent implements AfterViewInit { qualityChanged() { this.cookieService.set('metube_quality', this.quality, { expires: 3650 }); + // Re-trigger custom directory change + this.downloads.customDirsChanged.next(this.downloads.customDirs); } showAdvanced() { @@ -88,13 +90,13 @@ export class AppComponent implements AfterViewInit { } getMatchingCustomDir() : Observable { - return this.downloads.customDirs.asObservable().pipe(map((output) => { + return this.downloads.customDirsChanged.asObservable().pipe(map((output) => { // Keep logic consistent with app/ytdl.py if (this.quality != 'audio' && this.format != 'mp3') { - console.debug("download_dir", output["download_dir"]) + console.debug("Showing default download directories"); return output["download_dir"]; } else { - console.debug("audio_download_dir", output["audio_download_dir"]) + console.debug("Showing audio-specific download directories"); return output["audio_download_dir"]; } })); @@ -125,6 +127,8 @@ export class AppComponent implements AfterViewInit { this.cookieService.set('metube_format', this.format, { expires: 3650 }); // Updates to use qualities available this.setQualities() + // Re-trigger custom directory change + this.downloads.customDirsChanged.next(this.downloads.customDirs); } queueSelectionChanged(checked: number) { diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 1fd75e3..a2ea912 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -33,8 +33,10 @@ export class DownloadsService { done = new Map(); queueChanged = new Subject(); doneChanged = new Subject(); - customDirs = new Subject>(); + customDirsChanged = new Subject(); + configuration = {}; + customDirs = {}; constructor(private http: HttpClient, private socket: MeTubeSocket) { socket.fromEvent('all').subscribe((strdata: string) => { @@ -84,7 +86,8 @@ export class DownloadsService { socket.fromEvent('custom_dirs').subscribe((strdata: string) => { let data = JSON.parse(strdata); console.debug("got custom_dirs:", data); - this.customDirs.next(data); + this.customDirs = data; + this.customDirsChanged.next(data); }); } -- cgit From 63baa1fc25a7ee02832b043bb38470fe611cfb01 Mon Sep 17 00:00:00 2001 From: James Woglom Date: Tue, 30 Aug 2022 01:22:24 -0400 Subject: Link to audio files and those with custom folders properly --- app/main.py | 1 + app/ytdl.py | 10 +++++----- ui/src/app/app.component.html | 4 ++-- ui/src/app/app.component.ts | 36 +++++++++++++++++++++++++++--------- ui/src/app/downloads.service.ts | 7 ++++--- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/app/main.py b/app/main.py index de13f1e..d70d360 100644 --- a/app/main.py +++ b/app/main.py @@ -150,6 +150,7 @@ if config.URL_PREFIX != '/': routes.static(config.URL_PREFIX + 'favicon/', 'favicon') routes.static(config.URL_PREFIX + 'download/', config.DOWNLOAD_DIR) +routes.static(config.URL_PREFIX + 'audio_download/', config.AUDIO_DOWNLOAD_DIR) routes.static(config.URL_PREFIX, 'ui/dist/metube') try: app.add_routes(routes) diff --git a/app/ytdl.py b/app/ytdl.py index 4329147..b7a0f41 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -208,7 +208,7 @@ class DownloadQueue: **self.config.YTDL_OPTIONS, }).extract_info(url, download=False) - async def __add_entry(self, entry, quality, format, already, folder=None): + async def __add_entry(self, entry, quality, format, folder, already): etype = entry.get('_type') or 'video' if etype == 'playlist': entries = entry['entries'] @@ -221,7 +221,7 @@ class DownloadQueue: for property in ("id", "title", "uploader", "uploader_id"): if property in entry: etr[f"playlist_{property}"] = entry[property] - results.append(await self.__add_entry(etr, quality, format, already, folder=folder)) + results.append(await self.__add_entry(etr, quality, format, folder, 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'} @@ -252,10 +252,10 @@ class DownloadQueue: await self.notifier.added(dl) return {'status': 'ok'} elif etype.startswith('url'): - return await self.add(entry['url'], quality, format, already, folder=folder) + return await self.add(entry['url'], quality, format, folder, already) return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} - async def add(self, url, quality, format, already=None, folder=None): + async def add(self, url, quality, format, folder, already=None): log.info(f'adding {url}: {quality=} {format=} {already=} {folder=}') already = set() if already is None else already if url in already: @@ -267,7 +267,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, format, already, folder=folder) + return await self.__add_entry(entry, quality, format, folder, already) async def cancel(self, ids): for id in ids: diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 23dccce..4ca9a07 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -129,11 +129,11 @@
- {{ download.value.title }} + {{ download.value.title }} {{ download.value.title }} - + diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index cdcd8f6..97ff020 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -4,7 +4,7 @@ import { faRedoAlt, faSun, faMoon, faExternalLinkAlt } from '@fortawesome/free-s import { CookieService } from 'ngx-cookie-service'; import { map, Observable, of } from 'rxjs'; -import { DownloadsService, Status } from './downloads.service'; +import { Download, DownloadsService, Status } from './downloads.service'; import { MasterCheckboxComponent } from './master-checkbox.component'; import { Formats, Format, Quality } from './formats'; @@ -89,15 +89,19 @@ export class AppComponent implements AfterViewInit { return this.downloads.configuration['CREATE_DIRS'] == 'true'; } + isAudioType() { + return this.quality == 'audio' || this.format == 'mp3'; + } + getMatchingCustomDir() : Observable { return this.downloads.customDirsChanged.asObservable().pipe(map((output) => { // Keep logic consistent with app/ytdl.py - if (this.quality != 'audio' && this.format != 'mp3') { - console.debug("Showing default download directories"); - return output["download_dir"]; - } else { + if (this.isAudioType()) { console.debug("Showing audio-specific download directories"); return output["audio_download_dir"]; + } else { + console.debug("Showing default download directories"); + return output["download_dir"]; } })); } @@ -146,13 +150,14 @@ export class AppComponent implements AfterViewInit { this.quality = exists ? this.quality : 'best' } - addDownload(url?: string, quality?: string, format?: string) { + addDownload(url?: string, quality?: string, format?: string, folder?: string) { url = url ?? this.addUrl quality = quality ?? this.quality format = format ?? this.format + folder = folder ?? this.folder this.addInProgress = true; - this.downloads.add(url, quality, format).subscribe((status: Status) => { + this.downloads.add(url, quality, format, folder).subscribe((status: Status) => { if (status.status === 'error') { alert(`Error adding URL: ${status.msg}`); } else { @@ -162,8 +167,8 @@ export class AppComponent implements AfterViewInit { }); } - retryDownload(key: string, url: string, quality: string, format: string) { - this.addDownload(url, quality, format); + retryDownload(key: string, url: string, quality: string, format: string, folder: string) { + this.addDownload(url, quality, format, folder); this.downloads.delById('done', [key]).subscribe(); } @@ -182,4 +187,17 @@ export class AppComponent implements AfterViewInit { clearFailedDownloads() { this.downloads.delByFilter('done', dl => dl.status === 'error').subscribe(); } + + buildDownloadLink(download: Download) { + let baseDir = 'download/'; + if (download.quality == 'audio' || download.filename.endsWith('.mp3')) { + baseDir = 'audio_download/'; + } + + if (download.folder) { + baseDir += download.folder + '/'; + } + + return baseDir + encodeURIComponent(download.filename); + } } diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index a2ea912..77d2fed 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -9,13 +9,14 @@ export interface Status { msg?: string; } -interface Download { +export interface Download { id: string; title: string; url: string, status: string; msg: string; filename: string; + folder: string; quality: string; percent: number; speed: number; @@ -96,8 +97,8 @@ export class DownloadsService { return of({status: 'error', msg: msg}) } - public add(url: string, quality: string, format: string) { - return this.http.post('add', {url: url, quality: quality, format: format}).pipe( + public add(url: string, quality: string, format: string, folder: string) { + return this.http.post('add', {url: url, quality: quality, format: format, folder: folder}).pipe( catchError(this.handleHTTPError) ); } -- cgit From e911105c19ab979f7dc2771e7ecf65f2746e711b Mon Sep 17 00:00:00 2001 From: James Woglom Date: Tue, 30 Aug 2022 01:29:55 -0400 Subject: fix button rounding when CUSTOM_DIRS=false --- ui/src/app/app.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 4ca9a07..b31c441 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -47,12 +47,12 @@
-
+
-