From 7cfb0c3a1d50f4bc0d651807dab2dd6603e8ae68 Mon Sep 17 00:00:00 2001 From: ddmoney420 Date: Wed, 4 Mar 2026 13:29:43 -0700 Subject: [PATCH] Add cookie file upload for authenticated downloads --- app/main.py | 45 ++++++++++++++++++++++++ ui/src/app/app.html | 18 ++++++++++ ui/src/app/app.ts | 32 ++++++++++++++++- ui/src/app/services/downloads.service.ts | 20 +++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index dad519b..19efbd6 100644 --- a/app/main.py +++ b/app/main.py @@ -327,6 +327,43 @@ async def start(request): status = await dqueue.start_pending(ids) return web.Response(text=serializer.encode(status)) + +COOKIES_PATH = os.path.join(config.STATE_DIR, 'cookies.txt') + +@routes.post(config.URL_PREFIX + 'upload-cookies') +async def upload_cookies(request): + reader = await request.multipart() + field = await reader.next() + if field is None or field.name != 'cookies': + return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'No cookies file provided'})) + size = 0 + with open(COOKIES_PATH, 'wb') as f: + while True: + chunk = await field.read_chunk() + if not chunk: + break + size += len(chunk) + if size > 1_000_000: # 1MB limit + 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 + 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) + 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) + return web.Response(text=serializer.encode({'status': 'ok', 'has_cookies': exists})) + @routes.get(config.URL_PREFIX + 'history') async def history(request): history = { 'done': [], 'queue': [], 'pending': []} @@ -439,6 +476,8 @@ async def add_cors(request): app.router.add_route('OPTIONS', config.URL_PREFIX + 'add', add_cors) app.router.add_route('OPTIONS', config.URL_PREFIX + 'cancel-add', add_cors) +app.router.add_route('OPTIONS', config.URL_PREFIX + 'upload-cookies', add_cors) +app.router.add_route('OPTIONS', config.URL_PREFIX + 'delete-cookies', add_cors) async def on_prepare(request, response): if 'Origin' in request.headers: @@ -466,6 +505,12 @@ if __name__ == '__main__': logging.getLogger().setLevel(parseLogLevel(config.LOGLEVEL) or logging.INFO) log.info(f"Listening on {config.HOST}:{config.PORT}") + + # Auto-detect cookie file on startup + if os.path.exists(COOKIES_PATH): + config.YTDL_OPTIONS['cookiefile'] = COOKIES_PATH + log.info(f'Cookie file detected at {COOKIES_PATH}') + if config.HTTPS: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(certfile=config.CERTFILE, keyfile=config.KEYFILE) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 42adcc9..631b354 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -211,6 +211,24 @@ ngbTooltip="Add a prefix to downloaded filenames"> +
+
+ + + @if (hasCookies) { + Active + + } +
+
Items Limit diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 26cca3a..f72f9df 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -6,7 +6,7 @@ import { FormsModule } from '@angular/forms'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgSelectModule } from '@ng-select/ng-select'; -import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faUpload } from '@fortawesome/free-solid-svg-icons'; import { faGithub } from '@fortawesome/free-brands-svg-icons'; import { CookieService } from 'ngx-cookie-service'; import { DownloadsService } from './services/downloads.service'; @@ -54,6 +54,8 @@ export class App implements AfterViewInit, OnInit { subtitleMode: string; addInProgress = false; cancelRequested = false; + hasCookies = false; + cookieUploadInProgress = false; themes: Theme[] = Themes; activeTheme: Theme | undefined; customDirs$!: Observable; @@ -108,6 +110,7 @@ export class App implements AfterViewInit, OnInit { faSortAmountDown = faSortAmountDown; faSortAmountUp = faSortAmountUp; faChevronRight = faChevronRight; + faUpload = faUpload; subtitleFormats = [ { id: 'srt', text: 'SRT' }, { id: 'txt', text: 'TXT (Text only)' }, @@ -205,6 +208,9 @@ export class App implements AfterViewInit, OnInit { } ngOnInit() { + this.downloads.getCookieStatus().subscribe(data => { + this.hasCookies = data?.has_cookies || false; + }); this.getConfiguration(); this.getYtdlOptionsUpdateTime(); this.customDirs$ = this.getMatchingCustomDir(); @@ -782,6 +788,30 @@ export class App implements AfterViewInit, OnInit { return this.expandedErrors.has(id); } + onCookieFileSelect(event: Event) { + const input = event.target as HTMLInputElement; + if (!input.files?.length) return; + this.cookieUploadInProgress = true; + this.downloads.uploadCookies(input.files[0]).subscribe({ + next: () => { + this.hasCookies = true; + this.cookieUploadInProgress = false; + input.value = ''; + }, + error: () => { + this.cookieUploadInProgress = false; + input.value = ''; + } + }); + } + + deleteCookies() { + this.downloads.deleteCookies().subscribe({ + next: () => { this.hasCookies = false; }, + error: () => {} + }); + } + private updateMetrics() { this.activeDownloads = Array.from(this.downloads.queue.values()).filter(d => d.status === 'downloading' || d.status === 'preparing').length; this.queuedDownloads = Array.from(this.downloads.queue.values()).filter(d => d.status === 'pending').length; diff --git a/ui/src/app/services/downloads.service.ts b/ui/src/app/services/downloads.service.ts index e94ca9b..37f4dc7 100644 --- a/ui/src/app/services/downloads.service.ts +++ b/ui/src/app/services/downloads.service.ts @@ -213,4 +213,24 @@ export class DownloadsService { catchError(this.handleHTTPError) ); } + + uploadCookies(file: File) { + const formData = new FormData(); + formData.append('cookies', file); + return this.http.post('upload-cookies', formData).pipe( + catchError(this.handleHTTPError) + ); + } + + deleteCookies() { + return this.http.post('delete-cookies', {}).pipe( + catchError(this.handleHTTPError) + ); + } + + getCookieStatus() { + return this.http.get('cookie-status').pipe( + catchError(this.handleHTTPError) + ); + } }