From 54e25484c588deb8e0ab6bb3d1c6ccad288058af Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Fri, 6 Mar 2026 14:20:16 +0200 Subject: [PATCH] some fixes in cookie upload functionality --- README.md | 16 ++----- app/main.py | 49 ++++++++++++++++++--- ui/src/app/app.html | 103 +++++++++++++++++++++++++++----------------- ui/src/app/app.sass | 45 +++++++++++++++++++ ui/src/app/app.ts | 52 ++++++++++++++++++++-- 5 files changed, 203 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 3812b8d..a913a0f 100644 --- a/README.md +++ b/README.md @@ -89,21 +89,13 @@ The project's Wiki contains examples of useful configurations contributed by use In case you need to use your browser's cookies with MeTube, for example to download restricted or private videos: -* Add the following to your docker-compose.yml: - -```yaml - volumes: - - /path/to/cookies:/cookies - environment: - - YTDL_OPTIONS={"cookiefile":"/cookies/cookies.txt"} -``` - * Install in your browser an extension to extract cookies: * [Firefox](https://addons.mozilla.org/en-US/firefox/addon/export-cookies-txt/) * [Chrome](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) -* Extract the cookies you need with the extension and rename the file `cookies.txt` -* Drop the file in the folder you configured in the docker-compose.yml above -* Restart the container +* Extract the cookies you need with the extension and save/export them as `cookies.txt`. +* In MeTube, open **Advanced Options** and use the **Upload Cookies** button to upload the file. +* After upload, the cookie indicator should show as active. +* Use **Delete Cookies** in the same section to remove uploaded cookies. ## 🔌 Browser extensions diff --git a/app/main.py b/app/main.py index 19efbd6..1c4bd66 100644 --- a/app/main.py +++ b/app/main.py @@ -97,10 +97,23 @@ class Config: if self.YTDL_OPTIONS_FILE and self.YTDL_OPTIONS_FILE.startswith('.'): self.YTDL_OPTIONS_FILE = str(Path(self.YTDL_OPTIONS_FILE).resolve()) + self._runtime_overrides = {} + success,_ = self.load_ytdl_options() if not success: sys.exit(1) + def set_runtime_override(self, key, value): + self._runtime_overrides[key] = value + self.YTDL_OPTIONS[key] = value + + def remove_runtime_override(self, key): + self._runtime_overrides.pop(key, None) + self.YTDL_OPTIONS.pop(key, None) + + def _apply_runtime_overrides(self): + self.YTDL_OPTIONS.update(self._runtime_overrides) + def load_ytdl_options(self) -> tuple[bool, str]: try: self.YTDL_OPTIONS = json.loads(os.environ.get('YTDL_OPTIONS', '{}')) @@ -111,6 +124,7 @@ class Config: return (False, msg) if not self.YTDL_OPTIONS_FILE: + self._apply_runtime_overrides() return (True, '') log.info(f'Loading yt-dlp custom options from "{self.YTDL_OPTIONS_FILE}"') @@ -128,6 +142,7 @@ class Config: return (False, msg) self.YTDL_OPTIONS.update(opts) + self._apply_runtime_overrides() return (True, '') config = Config() @@ -347,21 +362,43 @@ async def upload_cookies(request): os.remove(COOKIES_PATH) return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'Cookie file too large (max 1MB)'})) f.write(chunk) - config.YTDL_OPTIONS['cookiefile'] = COOKIES_PATH + config.set_runtime_override('cookiefile', COOKIES_PATH) log.info(f'Cookies file uploaded ({size} bytes)') return web.Response(text=serializer.encode({'status': 'ok', 'msg': f'Cookies uploaded ({size} bytes)'})) @routes.post(config.URL_PREFIX + 'delete-cookies') async def delete_cookies(request): - if os.path.exists(COOKIES_PATH): - os.remove(COOKIES_PATH) - config.YTDL_OPTIONS.pop('cookiefile', None) + has_uploaded_cookies = os.path.exists(COOKIES_PATH) + configured_cookiefile = config.YTDL_OPTIONS.get('cookiefile') + has_manual_cookiefile = isinstance(configured_cookiefile, str) and configured_cookiefile and configured_cookiefile != COOKIES_PATH + + if not has_uploaded_cookies: + if has_manual_cookiefile: + return web.Response( + status=400, + text=serializer.encode({ + 'status': 'error', + 'msg': 'Cookies are configured manually via YTDL_OPTIONS (cookiefile). Remove or change that setting manually; UI delete only removes uploaded cookies.' + }) + ) + return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'No uploaded cookies to delete'})) + + os.remove(COOKIES_PATH) + config.remove_runtime_override('cookiefile') + success, msg = config.load_ytdl_options() + if not success: + log.error(f'Cookies file deleted, but failed to reload YTDL_OPTIONS: {msg}') + return web.Response(status=500, text=serializer.encode({'status': 'error', 'msg': f'Cookies file deleted, but failed to reload YTDL_OPTIONS: {msg}'})) + log.info('Cookies file deleted') return web.Response(text=serializer.encode({'status': 'ok'})) @routes.get(config.URL_PREFIX + 'cookie-status') async def cookie_status(request): - exists = os.path.exists(COOKIES_PATH) + configured_cookiefile = config.YTDL_OPTIONS.get('cookiefile') + has_configured_cookies = isinstance(configured_cookiefile, str) and os.path.exists(configured_cookiefile) + has_uploaded_cookies = os.path.exists(COOKIES_PATH) + exists = has_uploaded_cookies or has_configured_cookies return web.Response(text=serializer.encode({'status': 'ok', 'has_cookies': exists})) @routes.get(config.URL_PREFIX + 'history') @@ -508,7 +545,7 @@ if __name__ == '__main__': # Auto-detect cookie file on startup if os.path.exists(COOKIES_PATH): - config.YTDL_OPTIONS['cookiefile'] = COOKIES_PATH + config.set_runtime_override('cookiefile', COOKIES_PATH) log.info(f'Cookie file detected at {COOKIES_PATH}') if config.HTTPS: diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 631b354..6504932 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -211,24 +211,6 @@ ngbTooltip="Add a prefix to downloaded filenames"> -
-
- - - @if (hasCookies) { - Active - - } -
-
Items Limit @@ -326,30 +308,71 @@

-
+
- +
Cookies
+ +
+ + @if (hasCookies) { + + } +
+
-
- -
-
- +
+
Bulk Actions
+
+
+ +
+
+ +
+
+ +
+
diff --git a/ui/src/app/app.sass b/ui/src/app/app.sass index a8a57b2..e1d7e31 100644 --- a/ui/src/app/app.sass +++ b/ui/src/app/app.sass @@ -209,3 +209,48 @@ main span white-space: nowrap + +.cookie-btn + flex: 1 1 auto + background-color: var(--bs-secondary-bg) + border-color: var(--bs-border-color) + color: var(--bs-emphasis-color) + + &:hover + background-color: var(--bs-tertiary-bg) + border-color: var(--bs-secondary) + color: var(--bs-emphasis-color) + + &.disabled + opacity: 0.65 + pointer-events: none + +.cookie-active-btn + flex: 1 1 auto + background-color: var(--bs-success-bg-subtle) + border-color: var(--bs-success-border-subtle) + color: var(--bs-success-text-emphasis) + + &:hover + background-color: var(--bs-success-bg-subtle) + border-color: var(--bs-success) + color: var(--bs-success-text-emphasis) + + &.disabled + opacity: 0.65 + pointer-events: none + +.action-group-label + font-size: 0.7rem + text-transform: uppercase + letter-spacing: 0.05em + color: var(--bs-secondary-color) + margin-bottom: 0.4rem + +.cookie-status + font-size: 0.8rem + margin-top: 0.35rem + color: var(--bs-secondary-color) + + &.active + color: var(--bs-success-text-emphasis) diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index f72f9df..15f7ce3 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -793,22 +793,66 @@ export class App implements AfterViewInit, OnInit { if (!input.files?.length) return; this.cookieUploadInProgress = true; this.downloads.uploadCookies(input.files[0]).subscribe({ - next: () => { - this.hasCookies = true; + next: (response) => { + if (response?.status === 'ok') { + this.hasCookies = true; + } else { + this.refreshCookieStatus(); + alert(`Error uploading cookies: ${this.formatErrorMessage(response?.msg)}`); + } this.cookieUploadInProgress = false; input.value = ''; }, error: () => { + this.refreshCookieStatus(); this.cookieUploadInProgress = false; input.value = ''; + alert('Error uploading cookies.'); } }); } + private formatErrorMessage(error: unknown): string { + if (typeof error === 'string') { + return error; + } + if (error && typeof error === 'object') { + const obj = error as Record; + for (const key of ['msg', 'reason', 'error', 'detail']) { + const value = obj[key]; + if (typeof value === 'string' && value.trim()) { + return value; + } + } + try { + return JSON.stringify(error); + } catch { + return 'Unknown error'; + } + } + return 'Unknown error'; + } + deleteCookies() { this.downloads.deleteCookies().subscribe({ - next: () => { this.hasCookies = false; }, - error: () => {} + next: (response) => { + if (response?.status === 'ok') { + this.refreshCookieStatus(); + return; + } + this.refreshCookieStatus(); + alert(`Error deleting cookies: ${this.formatErrorMessage(response?.msg)}`); + }, + error: () => { + this.refreshCookieStatus(); + alert('Error deleting cookies.'); + } + }); + } + + private refreshCookieStatus() { + this.downloads.getCookieStatus().subscribe(data => { + this.hasCookies = data?.has_cookies || false; }); }