From ce638a0e32f8aea3ac2004d4ea24609c49fc5b79 Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Tue, 2 Jul 2024 00:06:59 +0000 Subject: [PATCH 01/27] upgraded yt-dlp --- Pipfile.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 52a4a75..5d94936 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -213,11 +213,11 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", + "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.6.2" }, "charset-normalizer": { "hashes": [ @@ -574,11 +574,11 @@ }, "requests": { "hashes": [ - "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", - "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "markers": "python_version >= '3.8'", - "version": "==2.32.2" + "version": "==2.32.3" }, "simple-websocket": { "hashes": [ @@ -590,11 +590,11 @@ }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], "markers": "python_version >= '3.8'", - "version": "==2.2.1" + "version": "==2.2.2" }, "websockets": { "hashes": [ @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:3566c0de240d0cd3d1c2285ce655f72ca38dfc618d634d46818b00d89d5288be", - "sha256:5dbedb7610ae9440cc44e744f528e078828142bbe09af950cf7e99ce69c866d9" + "sha256:2479540f7a7bdea30258c7a46dfbd96e0904e706247db95c94bfa5a04239698a", + "sha256:eb0019474ffde6979378c07555fa01173cf55bde90b172a0181b5716793aaef2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.5.27" + "version": "==2024.7.1" } }, "develop": { From de48d667a1f8d2dbc287a9b03690a350784cf06e Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Wed, 3 Jul 2024 00:06:48 +0000 Subject: [PATCH 02/27] upgraded yt-dlp --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 5d94936..ebbfa1f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:2479540f7a7bdea30258c7a46dfbd96e0904e706247db95c94bfa5a04239698a", - "sha256:eb0019474ffde6979378c07555fa01173cf55bde90b172a0181b5716793aaef2" + "sha256:2b0c86b579d4a044eaf3c4b00e3d7b24d82e6e26869fa11c288ea4395b387f41", + "sha256:4f76b48244c783e6ac06e8d7627bcf62cbeb4f6d79ba7e3cfc8249e680d4e691" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.7.1" + "version": "==2024.7.2" } }, "develop": { From 9619c4ed92c44fc075c14ceb2fa4bc9cd874bbed Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Mon, 8 Jul 2024 00:07:20 +0000 Subject: [PATCH 03/27] upgraded yt-dlp --- Pipfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index ebbfa1f..c1dfb3a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -213,11 +213,11 @@ }, "certifi": { "hashes": [ - "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", - "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2024.6.2" + "version": "==2024.7.4" }, "charset-normalizer": { "hashes": [ @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:2b0c86b579d4a044eaf3c4b00e3d7b24d82e6e26869fa11c288ea4395b387f41", - "sha256:4f76b48244c783e6ac06e8d7627bcf62cbeb4f6d79ba7e3cfc8249e680d4e691" + "sha256:2a0f89423d25d47db949925db5bd2c6f651960ae93dbbf5b3ed61cf3a4078ce5", + "sha256:2e90abeadc0199c787b1b4a3e0a1c8ed9d7c9f824f58da88467a1b30ed745e07" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.7.2" + "version": "==2024.7.7" } }, "develop": { From ae57a5d4a10c6b455edf03f4eb51e3415e56f92e Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Tue, 9 Jul 2024 00:06:52 +0000 Subject: [PATCH 04/27] upgraded yt-dlp --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index c1dfb3a..ab29f4a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:2a0f89423d25d47db949925db5bd2c6f651960ae93dbbf5b3ed61cf3a4078ce5", - "sha256:2e90abeadc0199c787b1b4a3e0a1c8ed9d7c9f824f58da88467a1b30ed745e07" + "sha256:9367e9b2e8dac4b37f512d21a4533273de22009dd74588ad24a7528c7f77b806", + "sha256:d12fb17540f7da7c2c9fcd68a655f573d124548ea37657662cd4e471d14c14d2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.7.7" + "version": "==2024.7.8" } }, "develop": { From 9afbc96e5bd4260a43f3ae0b55c147bad9146d09 Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Wed, 10 Jul 2024 00:07:10 +0000 Subject: [PATCH 05/27] upgraded yt-dlp --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index ab29f4a..356e7ed 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:9367e9b2e8dac4b37f512d21a4533273de22009dd74588ad24a7528c7f77b806", - "sha256:d12fb17540f7da7c2c9fcd68a655f573d124548ea37657662cd4e471d14c14d2" + "sha256:b50a595abde523b5cc84d788f97e69c642503bd673ba740f709ebf65b5ec6592", + "sha256:e19f00f9e55e90bca1c94bcaf809aa33e51634be9f0de2df84a72d3206934f94" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.7.8" + "version": "==2024.7.9" } }, "develop": { From 80569d7b5e46059261c7006e8f46015a726369a5 Mon Sep 17 00:00:00 2001 From: alegru Date: Sun, 14 Jul 2024 17:05:47 +0200 Subject: [PATCH 06/27] Fix basic auth not asking for credentials --- ui/src/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/index.html b/ui/src/index.html index 4e03924..fd45c95 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -11,7 +11,7 @@ - + From 77ed836fc3f64b46ff424bbc15946459d48b6ce7 Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Wed, 17 Jul 2024 00:07:24 +0000 Subject: [PATCH 07/27] upgraded yt-dlp --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 356e7ed..49db38a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:b50a595abde523b5cc84d788f97e69c642503bd673ba740f709ebf65b5ec6592", - "sha256:e19f00f9e55e90bca1c94bcaf809aa33e51634be9f0de2df84a72d3206934f94" + "sha256:424805a112e757b141e767bc938d49db56d13d6415a92fa4cd8acadd55790be0", + "sha256:c5bd517a49dea1923ec8e14f51858f10fd89dfece14cb701392b480b41b2f516" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.7.9" + "version": "==2024.7.16" } }, "develop": { From dc894efb82e8d743ae2405fb7d4f2ec948d41de7 Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Fri, 26 Jul 2024 00:06:47 +0000 Subject: [PATCH 08/27] upgraded yt-dlp --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 49db38a..97c0504 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:424805a112e757b141e767bc938d49db56d13d6415a92fa4cd8acadd55790be0", - "sha256:c5bd517a49dea1923ec8e14f51858f10fd89dfece14cb701392b480b41b2f516" + "sha256:7587aa25e236cf7b14bdb9378bbffff51202d901b04202be0cf62cbb56d3b52c", + "sha256:f44b5f33776b4f718900c670fe6e4698fb6fcd426455cd837cf25a1d6d4d9560" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.7.16" + "version": "==2024.7.25" } }, "develop": { From 014add35d757486b5ee913d8eccc60eda8bfa08f Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Fri, 2 Aug 2024 00:06:52 +0000 Subject: [PATCH 09/27] upgraded yt-dlp --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 97c0504..2ead41b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:7587aa25e236cf7b14bdb9378bbffff51202d901b04202be0cf62cbb56d3b52c", - "sha256:f44b5f33776b4f718900c670fe6e4698fb6fcd426455cd837cf25a1d6d4d9560" + "sha256:4318aa523694611562f01419c8d526b662a72df34ef8ba454016b34c8366c158", + "sha256:d0d927038e30a05f6eab26ff6189628456ea21bb159a3d9dc2e855eef2810eac" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.7.25" + "version": "==2024.8.1" } }, "develop": { From ceede4784111e6be990cee44ecb343d833433da4 Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Wed, 7 Aug 2024 00:07:06 +0000 Subject: [PATCH 10/27] upgraded yt-dlp --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 2ead41b..ad3c2c8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -780,12 +780,12 @@ }, "yt-dlp": { "hashes": [ - "sha256:4318aa523694611562f01419c8d526b662a72df34ef8ba454016b34c8366c158", - "sha256:d0d927038e30a05f6eab26ff6189628456ea21bb159a3d9dc2e855eef2810eac" + "sha256:ab507ff600bd9269ad4d654e309646976778f0e243eaa2f6c3c3214278bb2922", + "sha256:e8551f26bc8bf67b99c12373cc87ed2073436c3437e53290878d0f4b4bb1f663" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.8.1" + "version": "==2024.8.6" } }, "develop": { From c675db1ae5156ff63b693511f79d6889cb47374b Mon Sep 17 00:00:00 2001 From: Pawel Derehajlo Date: Sun, 18 Aug 2024 11:07:59 +0200 Subject: [PATCH 11/27] Added Playlist Strict mode and Item limits --- README.md | 2 ++ app/main.py | 15 +++++++++++++-- app/ytdl.py | 34 ++++++++++++++++++++++----------- ui/src/app/app.component.html | 18 +++++++++++++++++ ui/src/app/app.component.sass | 4 ++++ ui/src/app/app.component.ts | 33 +++++++++++++++++++++++++++----- ui/src/app/downloads.service.ts | 8 ++++++-- 7 files changed, 94 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 663de90..9a519a3 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ Certain values can be set via environment variables, using the `-e` parameter on * __PUBLIC_HOST_AUDIO_URL__: same as PUBLIC_HOST_URL but for audio downloads. * __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`. * __OUTPUT_TEMPLATE_CHAPTER__: the template for the filenames of the downloaded videos, when split into chapters via postprocessors. Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s`. +* __DEFAULT_OPTION_PLAYLIST_STRICT_MODE__: if `true`, the "Strict Playlist mode" switch will be enabled by default. In this mode the playlists will be downloaded only if the url strictly points to a playlist. Urls to videos inside a playlist will be treated same as direct video url. Defaults to `false` . +* __DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT__: Maximum numer of playlist items that can be downloaded. Defaults to `0` (no limit). * __YTDL_OPTIONS__: Additional options to pass to youtube-dl, in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L183). They roughly correspond to command-line options, though some do not have exact equivalents here, for example `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. * __YTDL_OPTIONS_FILE__: A path to a JSON file that will be loaded and used for populating `YTDL_OPTIONS` above. Please note that if both `YTDL_OPTIONS_FILE` and `YTDL_OPTIONS` are specified, the options in `YTDL_OPTIONS` take precedence. diff --git a/app/main.py b/app/main.py index 3daed83..1b14738 100644 --- a/app/main.py +++ b/app/main.py @@ -28,6 +28,8 @@ class Config: 'PUBLIC_HOST_AUDIO_URL': 'audio_download/', 'OUTPUT_TEMPLATE': '%(title)s.%(ext)s', 'OUTPUT_TEMPLATE_CHAPTER': '%(title)s - %(section_number)s %(section_title)s.%(ext)s', + 'DEFAULT_OPTION_PLAYLIST_STRICT_MODE' : 'false', + 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0', 'YTDL_OPTIONS': '{}', 'YTDL_OPTIONS_FILE': '', 'HOST': '0.0.0.0', @@ -36,7 +38,7 @@ class Config: 'DEFAULT_THEME': 'auto' } - _BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN') + _BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN', 'DEFAULT_OPTION_PLAYLIST_STRICT_MODE') def __init__(self): for k, v in self._DEFAULTS.items(): @@ -119,12 +121,21 @@ async def add(request): format = post.get('format') folder = post.get('folder') custom_name_prefix = post.get('custom_name_prefix') + playlist_strict_mode = post.get('playlist_strict_mode') + playlist_item_limit = post.get('playlist_item_limit') auto_start = post.get('auto_start') if custom_name_prefix is None: custom_name_prefix = '' if auto_start is None: auto_start = True - status = await dqueue.add(url, quality, format, folder, custom_name_prefix, auto_start) + if playlist_strict_mode is None: + playlist_strict_mode = config.DEFAULT_OPTION_PLAYLIST_STRICT_MODE + if playlist_item_limit is None: + playlist_item_limit = config.DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT + + playlist_item_limit = int(playlist_item_limit) + + status = await dqueue.add(url, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start) return web.Response(text=serializer.encode(status)) @routes.post(config.URL_PREFIX + 'delete') diff --git a/app/ytdl.py b/app/ytdl.py index 086d7c9..5bc7350 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -212,19 +212,20 @@ class DownloadQueue: async def __import_queue(self): for k, v in self.queue.saved_items(): - await self.add(v.url, v.quality, v.format, v.folder, v.custom_name_prefix) + await self.add(v.url, v.quality, v.format, v.folder, v.custom_name_prefix, v.playlist_strict_mode, v.playlist_item_limit) async def initialize(self): self.event = asyncio.Event() asyncio.create_task(self.__download()) asyncio.create_task(self.__import_queue()) - def __extract_info(self, url): + def __extract_info(self, url, playlist_strict_mode): return yt_dlp.YoutubeDL(params={ 'quiet': True, 'no_color': True, 'extract_flat': True, 'ignore_no_formats_error': True, + 'noplaylist': playlist_strict_mode, 'paths': {"home": self.config.DOWNLOAD_DIR, "temp": self.config.TEMP_DIR}, **self.config.YTDL_OPTIONS, }).extract_info(url, download=False) @@ -252,7 +253,7 @@ class DownloadQueue: dldirectory = base_directory return dldirectory, None - async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, auto_start, already): + async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already): if not entry: return {'status': 'error', 'msg': "Invalid/empty data was given."} @@ -265,18 +266,22 @@ class DownloadQueue: error = entry["msg"] etype = entry.get('_type') or 'video' + if etype == 'playlist': entries = entry['entries'] log.info(f'playlist detected with {len(entries)} entries') playlist_index_digits = len(str(len(entries))) results = [] + if playlist_item_limit > 0: + log.info(f'Playlist item limit is set. Processing only first {playlist_item_limit} entries') + entries = entries[:playlist_item_limit] for index, etr in enumerate(entries, start=1): etr["playlist"] = entry["id"] etr["playlist_index"] = '{{0:0{0:d}d}}'.format(playlist_index_digits).format(index) 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, folder, custom_name_prefix, auto_start, already)) + results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, 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'} @@ -291,19 +296,26 @@ class DownloadQueue: for property, value in entry.items(): if property.startswith("playlist"): output = output.replace(f"%({property})s", str(value)) + + ytdl_options = dict(self.config.YTDL_OPTIONS) + + if playlist_item_limit > 0: + log.info(f'playlist limit is set. Processing only first {playlist_item_limit} entries') + ytdl_options['playlistend'] = playlist_item_limit + if auto_start is True: - self.queue.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, self.config.YTDL_OPTIONS, dl)) + self.queue.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, ytdl_options, dl)) self.event.set() else: - self.pending.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, self.config.YTDL_OPTIONS, dl)) + self.pending.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, ytdl_options, dl)) await self.notifier.added(dl) return {'status': 'ok'} elif etype.startswith('url'): - return await self.add(entry['url'], quality, format, folder, custom_name_prefix, auto_start, already) + return await self.add(entry['url'], quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already) return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} - async def add(self, url, quality, format, folder, custom_name_prefix, auto_start=True, already=None): - log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=}') + async def add(self, url, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start=True, already=None): + log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} {playlist_strict_mode=} {playlist_item_limit=}') already = set() if already is None else already if url in already: log.info('recursion detected, skipping') @@ -311,10 +323,10 @@ class DownloadQueue: else: already.add(url) try: - entry = await asyncio.get_running_loop().run_in_executor(None, self.__extract_info, url) + entry = await asyncio.get_running_loop().run_in_executor(None, self.__extract_info, url, playlist_strict_mode) except yt_dlp.utils.YoutubeDLError as exc: return {'status': 'error', 'msg': str(exc)} - return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, auto_start, already) + return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already) async def start_pending(self, ids): for id in ids: diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index e5b7145..16d1443 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -94,6 +94,24 @@ Custom Name Prefix +
+
+
+
+
+ + +
+
+
+
+
+ Items limit + +
+
+
+
diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index 16913d4..4b20d8e 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -55,3 +55,7 @@ td .disabled opacity: 0.5 pointer-events: none + +.form-switch + input + margin-top: 5px diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 29af8ee..55ece6e 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -24,6 +24,8 @@ export class AppComponent implements AfterViewInit { folder: string; customNamePrefix: string; autoStart: boolean; + playlistStrictMode: boolean; + playlistItemLimit: number; addInProgress = false; themes: Theme[] = Themes; activeTheme: Theme; @@ -37,7 +39,6 @@ export class AppComponent implements AfterViewInit { @ViewChild('doneClearFailed') doneClearFailed: ElementRef; @ViewChild('doneRetryFailed') doneRetryFailed: ElementRef; - faTrashAlt = faTrashAlt; faCheckCircle = faCheckCircle; faTimesCircle = faTimesCircle; @@ -60,6 +61,7 @@ export class AppComponent implements AfterViewInit { } ngOnInit() { + this.getConfiguration(); this.customDirs$ = this.getMatchingCustomDir(); this.setTheme(this.activeTheme); @@ -129,6 +131,18 @@ export class AppComponent implements AfterViewInit { })); } + getConfiguration() { + this.downloads.configurationChanged.subscribe({ + next: (config) => { + this.playlistStrictMode = config['DEFAULT_OPTION_PLAYLIST_STRICT_MODE']; + const playlistItemLimit = config['DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT']; + if (playlistItemLimit !== '0') { + this.playlistItemLimit = playlistItemLimit; + } + } + }); + } + getPreferredTheme(cookieService: CookieService) { let theme = 'auto'; if (cookieService.check('metube_theme')) { @@ -179,17 +193,19 @@ export class AppComponent implements AfterViewInit { this.quality = exists ? this.quality : 'best' } - addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, autoStart?: boolean) { + addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, playlistStrictMode?: boolean, playlistItemLimit?: number, autoStart?: boolean) { url = url ?? this.addUrl quality = quality ?? this.quality format = format ?? this.format folder = folder ?? this.folder customNamePrefix = customNamePrefix ?? this.customNamePrefix + playlistStrictMode = playlistStrictMode ?? this.playlistStrictMode + playlistItemLimit = playlistItemLimit ?? this.playlistItemLimit autoStart = autoStart ?? this.autoStart - console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix+' autoStart='+autoStart); + console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix+' playlistStrictMode='+playlistStrictMode+' playlistItemLimit='+playlistItemLimit+' autoStart='+autoStart); this.addInProgress = true; - this.downloads.add(url, quality, format, folder, customNamePrefix, autoStart).subscribe((status: Status) => { + this.downloads.add(url, quality, format, folder, customNamePrefix, playlistStrictMode, playlistItemLimit, autoStart).subscribe((status: Status) => { if (status.status === 'error') { alert(`Error adding URL: ${status.msg}`); } else { @@ -204,7 +220,7 @@ export class AppComponent implements AfterViewInit { } retryDownload(key: string, download: Download) { - this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix, true); + this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix, download.playlist_strict_mode, download.playlist_item_limit, true); this.downloads.delById('done', [key]).subscribe(); } @@ -248,4 +264,11 @@ export class AppComponent implements AfterViewInit { identifyDownloadRow(index: number, row: KeyValue) { return row.key; } + + isNumber(event) { + const charCode = (event.which) ? event.which : event.keyCode; + if (charCode > 31 && (charCode < 48 || charCode > 57)) { + event.preventDefault(); + } + } } diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index 425ecb4..75db747 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -17,6 +17,8 @@ export interface Download { format: string; folder: string; custom_name_prefix: string; + playlist_strict_mode: boolean; + playlist_item_limit: number; status: string; msg: string; percent: number; @@ -37,6 +39,7 @@ export class DownloadsService { queueChanged = new Subject(); doneChanged = new Subject(); customDirsChanged = new Subject(); + configurationChanged = new Subject(); configuration = {}; customDirs = {}; @@ -85,6 +88,7 @@ export class DownloadsService { let data = JSON.parse(strdata); console.debug("got configuration:", data); this.configuration = data; + this.configurationChanged.next(data); }); socket.fromEvent('custom_dirs').subscribe((strdata: string) => { let data = JSON.parse(strdata); @@ -99,8 +103,8 @@ export class DownloadsService { return of({status: 'error', msg: msg}) } - public add(url: string, quality: string, format: string, folder: string, customNamePrefix: string, autoStart: boolean) { - return this.http.post('add', {url: url, quality: quality, format: format, folder: folder, custom_name_prefix: customNamePrefix, auto_start: autoStart}).pipe( + public add(url: string, quality: string, format: string, folder: string, customNamePrefix: string, playlistStrictMode: boolean, playlistItemLimit: number, autoStart: boolean) { + return this.http.post('add', {url: url, quality: quality, format: format, folder: folder, custom_name_prefix: customNamePrefix, playlist_strict_mode: playlistStrictMode, playlist_item_limit: playlistItemLimit, auto_start: autoStart}).pipe( catchError(this.handleHTTPError) ); } From 5b64f2f6153db846d73c501a508d081387f1d7ae Mon Sep 17 00:00:00 2001 From: Pawel Derehajlo Date: Sun, 18 Aug 2024 11:27:15 +0200 Subject: [PATCH 12/27] fixed: urls with a video in a playlist were incorrectly downloaded as a whole playlist as single item instead of split by each video --- app/ytdl.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/ytdl.py b/app/ytdl.py index 5bc7350..0a4756d 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -267,7 +267,9 @@ class DownloadQueue: etype = entry.get('_type') or 'video' - if etype == 'playlist': + if etype.startswith('url'): + return await self.add(entry['url'], quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already) + elif etype == 'playlist' or etype.startswith('url'): entries = entry['entries'] log.info(f'playlist detected with {len(entries)} entries') playlist_index_digits = len(str(len(entries))) @@ -310,8 +312,7 @@ class DownloadQueue: self.pending.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, ytdl_options, dl)) await self.notifier.added(dl) return {'status': 'ok'} - elif etype.startswith('url'): - return await self.add(entry['url'], quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already) + return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} async def add(self, url, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start=True, already=None): From 301ff92b58a4f9a513481563f79789f289ecd5db Mon Sep 17 00:00:00 2001 From: Pawel Derehajlo Date: Mon, 19 Aug 2024 16:31:25 +0200 Subject: [PATCH 13/27] added OUTPUT_TEMPLATE_PLAYLIST variable --- README.md | 1 + app/main.py | 2 ++ app/ytdl.py | 17 ++++++++++++----- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9a519a3..6fa7193 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Certain values can be set via environment variables, using the `-e` parameter on * __PUBLIC_HOST_AUDIO_URL__: same as PUBLIC_HOST_URL but for audio downloads. * __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`. * __OUTPUT_TEMPLATE_CHAPTER__: the template for the filenames of the downloaded videos, when split into chapters via postprocessors. Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s`. +* __OUTPUT_TEMPLATE_PLAYLIST__: the template for the filenames of the downloaded videos, when downloaded as a playlist. Defaults to `%(playlist_title)s/%(title)s.%(ext)s`. When empty then `OUTPUT_TEMPLATE` is used. * __DEFAULT_OPTION_PLAYLIST_STRICT_MODE__: if `true`, the "Strict Playlist mode" switch will be enabled by default. In this mode the playlists will be downloaded only if the url strictly points to a playlist. Urls to videos inside a playlist will be treated same as direct video url. Defaults to `false` . * __DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT__: Maximum numer of playlist items that can be downloaded. Defaults to `0` (no limit). * __YTDL_OPTIONS__: Additional options to pass to youtube-dl, in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L183). They roughly correspond to command-line options, though some do not have exact equivalents here, for example `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. diff --git a/app/main.py b/app/main.py index 1b14738..48826df 100644 --- a/app/main.py +++ b/app/main.py @@ -28,6 +28,7 @@ class Config: 'PUBLIC_HOST_AUDIO_URL': 'audio_download/', 'OUTPUT_TEMPLATE': '%(title)s.%(ext)s', 'OUTPUT_TEMPLATE_CHAPTER': '%(title)s - %(section_number)s %(section_title)s.%(ext)s', + 'OUTPUT_TEMPLATE_PLAYLIST': '%(playlist_title)s/%(title)s.%(ext)s', 'DEFAULT_OPTION_PLAYLIST_STRICT_MODE' : 'false', 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0', 'YTDL_OPTIONS': '{}', @@ -124,6 +125,7 @@ async def add(request): playlist_strict_mode = post.get('playlist_strict_mode') playlist_item_limit = post.get('playlist_item_limit') auto_start = post.get('auto_start') + if custom_name_prefix is None: custom_name_prefix = '' if auto_start is None: diff --git a/app/ytdl.py b/app/ytdl.py index 0a4756d..1140061 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -268,8 +268,10 @@ class DownloadQueue: etype = entry.get('_type') or 'video' if etype.startswith('url'): + log.debug('Processing as an url') return await self.add(entry['url'], quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already) - elif etype == 'playlist' or etype.startswith('url'): + elif etype == 'playlist': + log.debug('Processing as a playlist') entries = entry['entries'] log.info(f'playlist detected with {len(entries)} entries') playlist_index_digits = len(str(len(entries))) @@ -278,6 +280,7 @@ class DownloadQueue: log.info(f'Playlist item limit is set. Processing only first {playlist_item_limit} entries') entries = entries[:playlist_item_limit] for index, etr in enumerate(entries, start=1): + etr["_type"] = "video" # Prevents video to be treated as url and lose below properties during processing etr["playlist"] = entry["id"] etr["playlist_index"] = '{{0:0{0:d}d}}'.format(playlist_index_digits).format(index) for property in ("id", "title", "uploader", "uploader_id"): @@ -288,6 +291,7 @@ class DownloadQueue: 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: + log.debug('Processing as a video') if not self.queue.exists(entry['id']): dl = DownloadInfo(entry['id'], entry['title'], entry.get('webpage_url') or entry['url'], quality, format, folder, custom_name_prefix, error) dldirectory, error_message = self.__calc_download_path(quality, format, folder) @@ -295,9 +299,13 @@ class DownloadQueue: return error_message output = self.config.OUTPUT_TEMPLATE if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}' output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER - for property, value in entry.items(): - if property.startswith("playlist"): - output = output.replace(f"%({property})s", str(value)) + if 'playlist' in entry: + if len(self.config.OUTPUT_TEMPLATE_PLAYLIST): + output = self.config.OUTPUT_TEMPLATE_PLAYLIST + + for property, value in entry.items(): + if property.startswith("playlist"): + output = output.replace(f"%({property})s", str(value)) ytdl_options = dict(self.config.YTDL_OPTIONS) @@ -312,7 +320,6 @@ class DownloadQueue: self.pending.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, ytdl_options, dl)) await self.notifier.added(dl) return {'status': 'ok'} - return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} async def add(self, url, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start=True, already=None): From 9675cbe988747eca85a83f8e71b976856fabdc62 Mon Sep 17 00:00:00 2001 From: Pawel Derehajlo Date: Wed, 21 Aug 2024 23:52:38 +0200 Subject: [PATCH 14/27] fixed: Regression causing playlist output format to be applied to every video --- app/ytdl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ytdl.py b/app/ytdl.py index 1140061..a1af68a 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -299,7 +299,7 @@ class DownloadQueue: return error_message output = self.config.OUTPUT_TEMPLATE if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}' output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER - if 'playlist' in entry: + if 'playlist' in entry and entry['playlist'] is not None: if len(self.config.OUTPUT_TEMPLATE_PLAYLIST): output = self.config.OUTPUT_TEMPLATE_PLAYLIST From d654b6060c9ce8f0ad88e4eab9e4d943b4573916 Mon Sep 17 00:00:00 2001 From: VolumeData21 Date: Wed, 21 Aug 2024 20:55:45 -0400 Subject: [PATCH 15/27] removed version line from Docker Compose YAML section --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 6fa7193..54e4451 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ docker run -d -p 8081:8081 -v /path/to/downloads:/downloads ghcr.io/alexta69/met ## Run using docker-compose ```yaml -version: "3" services: metube: image: ghcr.io/alexta69/metube From 04e82533c795b10c40f533d09071e4be8c543889 Mon Sep 17 00:00:00 2001 From: VergilGao <8655163+VergilGao@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:21:15 +0800 Subject: [PATCH 16/27] Add a supports_reuse_port method to check if the operating system supports reuse port. --- app/main.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 48826df..4bad876 100644 --- a/app/main.py +++ b/app/main.py @@ -4,6 +4,7 @@ import os import sys from aiohttp import web +import socket import socketio import logging import json @@ -247,8 +248,16 @@ async def on_prepare(request, response): app.on_response_prepare.append(on_prepare) +def supports_reuse_port(): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + sock.close() + return True + except (AttributeError, OSError): + return False if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) log.info(f"Listening on {config.HOST}:{config.PORT}") - web.run_app(app, host=config.HOST, port=int(config.PORT), reuse_port=True) + web.run_app(app, host=config.HOST, port=int(config.PORT), reuse_port=supports_reuse_port()) From 84ed2c5f00b2af4026994f13029ad14e27979042 Mon Sep 17 00:00:00 2001 From: VergilGao <8655163+VergilGao@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:54:36 +0800 Subject: [PATCH 17/27] Add the ability to be configured as an HTTPS service --- README.md | 25 +++++++++++++++++++++++++ app/main.py | 12 +++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 54e4451..4a3b420 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ docker run -d -p 8081:8081 -v /path/to/downloads:/downloads ghcr.io/alexta69/met ## Run using docker-compose +serve a http host: + ```yaml services: metube: @@ -29,6 +31,26 @@ services: - /path/to/downloads:/downloads ``` +serve a https host: + +```yaml +services: + metube: + image: ghcr.io/alexta69/metube + container_name: metube + restart: unless-stopped + ports: + - "8081:8081" + volumes: + - /path/to/downloads:/downloads + - /path/to/ssl/crt:/ssl/crt.pem + - /path/to/ssl/key:/ssl/key.pem + environment: + - HTTPS=true + - CERTFILE=/ssl/crt.pem + - KEYFILE=/ssl/key.pem +``` + ## Configuration via environment variables Certain values can be set via environment variables, using the `-e` parameter on the docker command line, or the `environment:` section in docker-compose. @@ -49,6 +71,9 @@ Certain values can be set via environment variables, using the `-e` parameter on * __DELETE_FILE_ON_TRASHCAN__: if `true`, downloaded files are deleted on the server, when they are trashed from the "Completed" section of the UI. Defaults to `false`. * __URL_PREFIX__: base path for the web server (for use when hosting behind a reverse proxy). Defaults to `/`. * __PUBLIC_HOST_URL__: base URL for the download links shown in the UI for completed files. By default MeTube serves them under its own URL. If your download directory is accessible on another URL and you want the download links to be based there, use this variable to set it. +* __HTTPS__: use `https` instead of `http`(__CERTFILE__ and __KEYFILE__ required). Defaults to `false`. +* __CERTFILE__: HTTPS certificate file path. Defaults to ` `. +* __KEYFILE__: HTTPS key file path. Defaults to ` `. * __PUBLIC_HOST_AUDIO_URL__: same as PUBLIC_HOST_URL but for audio downloads. * __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`. * __OUTPUT_TEMPLATE_CHAPTER__: the template for the filenames of the downloaded videos, when split into chapters via postprocessors. Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s`. diff --git a/app/main.py b/app/main.py index 4bad876..351fdad 100644 --- a/app/main.py +++ b/app/main.py @@ -4,6 +4,7 @@ import os import sys from aiohttp import web +import ssl import socket import socketio import logging @@ -36,6 +37,9 @@ class Config: 'YTDL_OPTIONS_FILE': '', 'HOST': '0.0.0.0', 'PORT': '8081', + 'HTTPS': 'false', + 'CERTFILE': '', + 'KEYFILE': '', 'BASE_DIR': '', 'DEFAULT_THEME': 'auto' } @@ -260,4 +264,10 @@ def supports_reuse_port(): if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) log.info(f"Listening on {config.HOST}:{config.PORT}") - web.run_app(app, host=config.HOST, port=int(config.PORT), reuse_port=supports_reuse_port()) + + if config.HTTPS: + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain(certfile=config.CERTFILE, keyfile=config.KEYFILE) + web.run_app(app, host=config.HOST, port=int(config.PORT), reuse_port=supports_reuse_port(), ssl_context=ssl_context) + else: + web.run_app(app, host=config.HOST, port=int(config.PORT), reuse_port=supports_reuse_port()) From 07b0b9caed9ceda863f1c93cedc2b0789a8f031e Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Wed, 11 Sep 2024 19:40:45 +0300 Subject: [PATCH 18/27] documentation fixes and HTTPS boolean fix --- README.md | 52 +++++++++++++++++++++++++--------------------------- app/main.py | 2 +- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 4a3b420..4d40542 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,6 @@ docker run -d -p 8081:8081 -v /path/to/downloads:/downloads ghcr.io/alexta69/met ## Run using docker-compose -serve a http host: - ```yaml services: metube: @@ -31,26 +29,6 @@ services: - /path/to/downloads:/downloads ``` -serve a https host: - -```yaml -services: - metube: - image: ghcr.io/alexta69/metube - container_name: metube - restart: unless-stopped - ports: - - "8081:8081" - volumes: - - /path/to/downloads:/downloads - - /path/to/ssl/crt:/ssl/crt.pem - - /path/to/ssl/key:/ssl/key.pem - environment: - - HTTPS=true - - CERTFILE=/ssl/crt.pem - - KEYFILE=/ssl/key.pem -``` - ## Configuration via environment variables Certain values can be set via environment variables, using the `-e` parameter on the docker command line, or the `environment:` section in docker-compose. @@ -72,8 +50,8 @@ Certain values can be set via environment variables, using the `-e` parameter on * __URL_PREFIX__: base path for the web server (for use when hosting behind a reverse proxy). Defaults to `/`. * __PUBLIC_HOST_URL__: base URL for the download links shown in the UI for completed files. By default MeTube serves them under its own URL. If your download directory is accessible on another URL and you want the download links to be based there, use this variable to set it. * __HTTPS__: use `https` instead of `http`(__CERTFILE__ and __KEYFILE__ required). Defaults to `false`. -* __CERTFILE__: HTTPS certificate file path. Defaults to ` `. -* __KEYFILE__: HTTPS key file path. Defaults to ` `. +* __CERTFILE__: HTTPS certificate file path. +* __KEYFILE__: HTTPS key file path. * __PUBLIC_HOST_AUDIO_URL__: same as PUBLIC_HOST_URL but for audio downloads. * __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`. * __OUTPUT_TEMPLATE_CHAPTER__: the template for the filenames of the downloaded videos, when split into chapters via postprocessors. Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s`. @@ -139,7 +117,7 @@ iOS has strict requirements for video files, requiring h264 or h265 video codec ## Bookmarklet -[kushfest](https://github.com/kushfest) has created a Chrome bookmarklet for sending the currently open webpage to MeTube. Please note that if you're on an HTTPS page, your MeTube instance must be behind an HTTPS reverse proxy (see below) for the bookmarklet to work. +[kushfest](https://github.com/kushfest) has created a Chrome bookmarklet for sending the currently open webpage to MeTube. Please note that if you're on an HTTPS page, your MeTube instance must be configured with `HTTPS` as `true` in the environment, or be behind an HTTPS reverse proxy (see below) for the bookmarklet to work. GitHub doesn't allow embedding JavaScript as a link, so the bookmarklet has to be created manually by copying the following code to a new bookmark you create on your bookmarks bar. Change the hostname in the URL below to point to your MeTube instance. @@ -167,9 +145,29 @@ Firefox: javascript:(function(){function notify(msg) {var sc = document.scrollingElement.scrollTop; var text = document.createElement('span');text.innerHTML=msg;var ts = text.style;ts.all = 'revert';ts.color = '#000';ts.fontFamily = 'Verdana, sans-serif';ts.fontSize = '15px';ts.backgroundColor = 'white';ts.padding = '15px';ts.border = '1px solid gainsboro';ts.boxShadow = '3px 3px 10px';ts.zIndex = '100';document.body.appendChild(text);ts.position = 'absolute'; ts.top = 50 + sc + 'px'; ts.left = (window.innerWidth / 2)-(text.offsetWidth / 2) + 'px'; setTimeout(function () { text.style.visibility = "hidden"; }, 1500);}xhr=new XMLHttpRequest();xhr.open("POST","https://metube.domain.com/add");xhr.send(JSON.stringify({"url":document.location.href,"quality":"best"}));xhr.onload=function() { if(xhr.status==200){notify("Sent to metube!")}else {notify("Send to metube failed. Check the javascript console for clues.")}}})(); ``` -## Running behind a reverse proxy +## HTTPS support, and running behind a reverse proxy -It's advisable to run MeTube behind a reverse proxy, if authentication and/or HTTPS support are required. +It's possible to configure MeTube to listen in HTTPS mode. `docker-compose` example: + +```yaml +services: + metube: + image: ghcr.io/alexta69/metube + container_name: metube + restart: unless-stopped + ports: + - "8081:8081" + volumes: + - /path/to/downloads:/downloads + - /path/to/ssl/crt:/ssl/crt.pem + - /path/to/ssl/key:/ssl/key.pem + environment: + - HTTPS=true + - CERTFILE=/ssl/crt.pem + - KEYFILE=/ssl/key.pem +``` + +It's also possible to run MeTube behind a reverse proxy, in order to support authentication. HTTPS support can also be added in this way. When running behind a reverse proxy which remaps the URL (i.e. serves MeTube under a subdirectory and not under root), don't forget to set the URL_PREFIX environment variable to the correct value. diff --git a/app/main.py b/app/main.py index 351fdad..9e10242 100644 --- a/app/main.py +++ b/app/main.py @@ -44,7 +44,7 @@ class Config: 'DEFAULT_THEME': 'auto' } - _BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN', 'DEFAULT_OPTION_PLAYLIST_STRICT_MODE') + _BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN', 'DEFAULT_OPTION_PLAYLIST_STRICT_MODE', 'HTTPS') def __init__(self): for k, v in self._DEFAULTS.items(): From 1112d6c5dbee855a190d9bdd9bf237b908588c1f Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Wed, 11 Sep 2024 20:10:37 +0300 Subject: [PATCH 19/27] add note about feature requests --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4d40542..a97f0b5 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,10 @@ cd /downloads Once there, you can use the yt-dlp command freely. +## Submitting feature requests + +MeTube development relies on code contributions by the community. The program as it currently stands fits my own use cases, and is therefore feature-complete as far as I'm concerned. If your use cases are different and require additional features, please feel free to submit PRs that implement those features. It's advisable to create an issue first to discuss the planned implementation, because in an effort to reduce bloat, some PRs may not be accepted. However, note that opening a feature request when you don't intend to implement the feature will rarely result in the request being fulfilled. + ## Building and running locally Make sure you have node.js and Python 3.11 installed. From abe7e88e44e79271e80468d83bd9036a759ba851 Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Mon, 16 Sep 2024 20:36:31 +0300 Subject: [PATCH 20/27] add note about Raycast extension (closes #512) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a97f0b5..d730f2e 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,10 @@ Firefox: javascript:(function(){function notify(msg) {var sc = document.scrollingElement.scrollTop; var text = document.createElement('span');text.innerHTML=msg;var ts = text.style;ts.all = 'revert';ts.color = '#000';ts.fontFamily = 'Verdana, sans-serif';ts.fontSize = '15px';ts.backgroundColor = 'white';ts.padding = '15px';ts.border = '1px solid gainsboro';ts.boxShadow = '3px 3px 10px';ts.zIndex = '100';document.body.appendChild(text);ts.position = 'absolute'; ts.top = 50 + sc + 'px'; ts.left = (window.innerWidth / 2)-(text.offsetWidth / 2) + 'px'; setTimeout(function () { text.style.visibility = "hidden"; }, 1500);}xhr=new XMLHttpRequest();xhr.open("POST","https://metube.domain.com/add");xhr.send(JSON.stringify({"url":document.location.href,"quality":"best"}));xhr.onload=function() { if(xhr.status==200){notify("Sent to metube!")}else {notify("Send to metube failed. Check the javascript console for clues.")}}})(); ``` +## Raycast extension + +[dotvhs](https://github.com/dotvhs) has created an [extension for Raycast](https://www.raycast.com/dot/metube) that allows adding videos to MeTube directly from Raycast. + ## HTTPS support, and running behind a reverse proxy It's possible to configure MeTube to listen in HTTPS mode. `docker-compose` example: From bde077d23a32ed34c36408d0d811f7567eb428c3 Mon Sep 17 00:00:00 2001 From: Jean-Francois Simoneau Date: Sun, 22 Sep 2024 02:10:36 -0400 Subject: [PATCH 21/27] Add the ability to specify a robots.txt file, with a default disallowing the download links --- README.md | 1 + app/main.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index d730f2e..d88dcc9 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Certain values can be set via environment variables, using the `-e` parameter on * __DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT__: Maximum numer of playlist items that can be downloaded. Defaults to `0` (no limit). * __YTDL_OPTIONS__: Additional options to pass to youtube-dl, in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L183). They roughly correspond to command-line options, though some do not have exact equivalents here, for example `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. * __YTDL_OPTIONS_FILE__: A path to a JSON file that will be loaded and used for populating `YTDL_OPTIONS` above. Please note that if both `YTDL_OPTIONS_FILE` and `YTDL_OPTIONS` are specified, the options in `YTDL_OPTIONS` take precedence. +* __ROBOTS_TXT__: A path to a `robots.txt` file mounted in the container The following example value for `YTDL_OPTIONS` embeds English subtitles and chapter markers (for videos that have them), and also changes the permissions on the downloaded video and sets the file modification timestamp to the date of when it was downloaded: diff --git a/app/main.py b/app/main.py index 9e10242..2ff3ec2 100644 --- a/app/main.py +++ b/app/main.py @@ -35,6 +35,7 @@ class Config: 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0', 'YTDL_OPTIONS': '{}', 'YTDL_OPTIONS_FILE': '', + 'ROBOTS_TXT': '', 'HOST': '0.0.0.0', 'PORT': '8081', 'HTTPS': 'false', @@ -218,6 +219,16 @@ def index(request): response.set_cookie('metube_theme', config.DEFAULT_THEME) return response +@routes.get(config.URL_PREFIX + 'robots.txt') +def robots(request): + if config.ROBOTS_TXT: + response = web.FileResponse(os.path.join(config.BASE_DIR, config.ROBOTS_TXT)) + else: + response = web.Response( + text="User-agent: *\nDisallow: /download/\nDisallow: /audio_download/\n" + ) + return response + if config.URL_PREFIX != '/': @routes.get('/') def index_redirect_root(request): From 0b1b0c4f5f56a46b3b24864a0629e4a5528237e1 Mon Sep 17 00:00:00 2001 From: Dallas Hoffman Date: Wed, 25 Sep 2024 23:06:29 -0400 Subject: [PATCH 22/27] Responsive design improvements --- ui/src/app/app.component.html | 210 +++++++++++++++++----------------- ui/src/app/app.component.sass | 6 + 2 files changed, 112 insertions(+), 104 deletions(-) diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 16d1443..01719d8 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -23,7 +23,7 @@ data-bs-display="static"> -