mirror of
https://github.com/alexta69/metube.git
synced 2026-03-18 22:43:51 +00:00
Add cookie file upload for authenticated downloads
This commit is contained in:
45
app/main.py
45
app/main.py
@@ -327,6 +327,43 @@ async def start(request):
|
|||||||
status = await dqueue.start_pending(ids)
|
status = await dqueue.start_pending(ids)
|
||||||
return web.Response(text=serializer.encode(status))
|
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')
|
@routes.get(config.URL_PREFIX + 'history')
|
||||||
async def history(request):
|
async def history(request):
|
||||||
history = { 'done': [], 'queue': [], 'pending': []}
|
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 + 'add', add_cors)
|
||||||
app.router.add_route('OPTIONS', config.URL_PREFIX + 'cancel-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):
|
async def on_prepare(request, response):
|
||||||
if 'Origin' in request.headers:
|
if 'Origin' in request.headers:
|
||||||
@@ -466,6 +505,12 @@ if __name__ == '__main__':
|
|||||||
logging.getLogger().setLevel(parseLogLevel(config.LOGLEVEL) or logging.INFO)
|
logging.getLogger().setLevel(parseLogLevel(config.LOGLEVEL) or logging.INFO)
|
||||||
log.info(f"Listening on {config.HOST}:{config.PORT}")
|
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:
|
if config.HTTPS:
|
||||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||||
ssl_context.load_cert_chain(certfile=config.CERTFILE, keyfile=config.KEYFILE)
|
ssl_context.load_cert_chain(certfile=config.CERTFILE, keyfile=config.KEYFILE)
|
||||||
|
|||||||
@@ -211,6 +211,24 @@
|
|||||||
ngbTooltip="Add a prefix to downloaded filenames">
|
ngbTooltip="Add a prefix to downloaded filenames">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="d-flex align-items-center mt-2">
|
||||||
|
<label class="btn btn-sm btn-outline-secondary me-2 mb-0" for="cookie-upload"
|
||||||
|
ngbTooltip="Upload a cookies.txt file for authenticated downloads">
|
||||||
|
<fa-icon [icon]="faUpload" class="me-1" />Cookies
|
||||||
|
</label>
|
||||||
|
<input type="file" id="cookie-upload" class="d-none" accept=".txt"
|
||||||
|
(change)="onCookieFileSelect($event)"
|
||||||
|
[disabled]="cookieUploadInProgress || addInProgress">
|
||||||
|
@if (hasCookies) {
|
||||||
|
<span class="badge bg-success me-2">Active</span>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
|
(click)="deleteCookies()" ngbTooltip="Remove uploaded cookies">
|
||||||
|
<fa-icon [icon]="faTrashAlt" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">Items Limit</span>
|
<span class="input-group-text">Items Limit</span>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NgSelectModule } from '@ng-select/ng-select';
|
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 { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
import { CookieService } from 'ngx-cookie-service';
|
||||||
import { DownloadsService } from './services/downloads.service';
|
import { DownloadsService } from './services/downloads.service';
|
||||||
@@ -54,6 +54,8 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
subtitleMode: string;
|
subtitleMode: string;
|
||||||
addInProgress = false;
|
addInProgress = false;
|
||||||
cancelRequested = false;
|
cancelRequested = false;
|
||||||
|
hasCookies = false;
|
||||||
|
cookieUploadInProgress = false;
|
||||||
themes: Theme[] = Themes;
|
themes: Theme[] = Themes;
|
||||||
activeTheme: Theme | undefined;
|
activeTheme: Theme | undefined;
|
||||||
customDirs$!: Observable<string[]>;
|
customDirs$!: Observable<string[]>;
|
||||||
@@ -108,6 +110,7 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
faSortAmountDown = faSortAmountDown;
|
faSortAmountDown = faSortAmountDown;
|
||||||
faSortAmountUp = faSortAmountUp;
|
faSortAmountUp = faSortAmountUp;
|
||||||
faChevronRight = faChevronRight;
|
faChevronRight = faChevronRight;
|
||||||
|
faUpload = faUpload;
|
||||||
subtitleFormats = [
|
subtitleFormats = [
|
||||||
{ id: 'srt', text: 'SRT' },
|
{ id: 'srt', text: 'SRT' },
|
||||||
{ id: 'txt', text: 'TXT (Text only)' },
|
{ id: 'txt', text: 'TXT (Text only)' },
|
||||||
@@ -205,6 +208,9 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.downloads.getCookieStatus().subscribe(data => {
|
||||||
|
this.hasCookies = data?.has_cookies || false;
|
||||||
|
});
|
||||||
this.getConfiguration();
|
this.getConfiguration();
|
||||||
this.getYtdlOptionsUpdateTime();
|
this.getYtdlOptionsUpdateTime();
|
||||||
this.customDirs$ = this.getMatchingCustomDir();
|
this.customDirs$ = this.getMatchingCustomDir();
|
||||||
@@ -782,6 +788,30 @@ export class App implements AfterViewInit, OnInit {
|
|||||||
return this.expandedErrors.has(id);
|
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() {
|
private updateMetrics() {
|
||||||
this.activeDownloads = Array.from(this.downloads.queue.values()).filter(d => d.status === 'downloading' || d.status === 'preparing').length;
|
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;
|
this.queuedDownloads = Array.from(this.downloads.queue.values()).filter(d => d.status === 'pending').length;
|
||||||
|
|||||||
@@ -213,4 +213,24 @@ export class DownloadsService {
|
|||||||
catchError(this.handleHTTPError)
|
catchError(this.handleHTTPError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadCookies(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('cookies', file);
|
||||||
|
return this.http.post<any>('upload-cookies', formData).pipe(
|
||||||
|
catchError(this.handleHTTPError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCookies() {
|
||||||
|
return this.http.post<any>('delete-cookies', {}).pipe(
|
||||||
|
catchError(this.handleHTTPError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCookieStatus() {
|
||||||
|
return this.http.get<any>('cookie-status').pipe(
|
||||||
|
catchError(this.handleHTTPError)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user