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)
+ );
+ }
}