predownload working

This commit is contained in:
DaKheera47 2025-10-15 11:54:35 +01:00
parent 8fcdfb7ff7
commit 3d74f9b53b
5 changed files with 160 additions and 79 deletions

View file

@ -223,23 +223,41 @@ async def add(request):
raise web.HTTPBadRequest()
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
if playlist_strict_mode is None:
playlist_strict_mode = config.DEFAULT_OPTION_PLAYLIST_STRICT_MODE
if playlist_item_limit is None:
custom_name_prefix = post.get('custom_name_prefix')
custom_name = post.get('custom_name')
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 custom_name is not None and not isinstance(custom_name, str):
log.error("Bad request: custom_name must be a string")
return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'invalid custom name'}))
sanitized_custom_name = ''
if isinstance(custom_name, str):
sanitized_custom_name = custom_name.strip()
if sanitized_custom_name:
if any(sep in sanitized_custom_name for sep in ('/', '\\')) or '..' in sanitized_custom_name:
log.error("Bad request: custom_name contains invalid characters")
return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'invalid custom name'}))
dot_index = sanitized_custom_name.rfind('.')
if dot_index > 0:
sanitized_custom_name = sanitized_custom_name[:dot_index]
sanitized_custom_name = sanitized_custom_name.strip()
if not sanitized_custom_name:
log.error("Bad request: custom_name missing after sanitization")
return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'invalid custom name'}))
if auto_start is None:
auto_start = True
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)
status = await dqueue.add(url, quality, format, folder, custom_name_prefix, sanitized_custom_name, playlist_strict_mode, playlist_item_limit, auto_start)
return web.Response(text=serializer.encode(status))
@routes.post(config.URL_PREFIX + 'delete')

View file

@ -1,9 +1,10 @@
import os
import yt_dlp
from collections import OrderedDict
import shelve
import time
import asyncio
import os
import glob
import yt_dlp
from collections import OrderedDict
import shelve
import time
import asyncio
import multiprocessing
import logging
import re
@ -33,20 +34,21 @@ class DownloadQueueNotifier:
async def renamed(self, dl):
raise NotImplementedError
class DownloadInfo:
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error, entry, playlist_item_limit):
self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}'
self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}'
self.url = url
self.quality = quality
self.format = format
self.folder = folder
self.custom_name_prefix = custom_name_prefix
self.msg = self.percent = self.speed = self.eta = None
self.status = "pending"
self.size = None
self.timestamp = time.time_ns()
self.error = error
class DownloadInfo:
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, custom_name, error, entry, playlist_item_limit):
self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}'
self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}'
self.url = url
self.quality = quality
self.format = format
self.folder = folder
self.custom_name_prefix = custom_name_prefix
self.custom_name = custom_name
self.msg = self.percent = self.speed = self.eta = None
self.status = "pending"
self.size = None
self.timestamp = time.time_ns()
self.error = error
self.entry = entry
self.playlist_item_limit = playlist_item_limit
@ -332,16 +334,24 @@ class DownloadQueue:
dldirectory = base_directory
return dldirectory, None
async def __add_download(self, dl, auto_start):
dldirectory, error_message = self.__calc_download_path(dl.quality, dl.format, dl.folder)
if error_message is not None:
return error_message
output = self.config.OUTPUT_TEMPLATE if len(dl.custom_name_prefix) == 0 else f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER
entry = getattr(dl, 'entry', None)
if entry is not None and 'playlist' in entry and entry['playlist'] is not None:
if len(self.config.OUTPUT_TEMPLATE_PLAYLIST):
output = self.config.OUTPUT_TEMPLATE_PLAYLIST
async def __add_download(self, dl, auto_start):
dldirectory, error_message = self.__calc_download_path(dl.quality, dl.format, dl.folder)
if error_message is not None:
return error_message
custom_name = getattr(dl, 'custom_name', '') or ''
if custom_name:
conflict = self.__check_custom_name_conflict(dldirectory, custom_name)
if conflict is not None:
log.info(f'Custom name conflict for download {dl.url} at {dldirectory} name "{custom_name}"')
return conflict
output = f'{custom_name}.%(ext)s'
else:
output = self.config.OUTPUT_TEMPLATE if len(dl.custom_name_prefix) == 0 else f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
output_chapter = self.config.OUTPUT_TEMPLATE_CHAPTER
entry = getattr(dl, 'entry', None)
if entry is not None and 'playlist' in entry and entry['playlist'] is not None:
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))
@ -356,14 +366,24 @@ class DownloadQueue:
asyncio.create_task(self.__start_download(download))
else:
self.pending.put(download)
await self.notifier.added(dl)
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."}
error = None
if "live_status" in entry and "release_timestamp" in entry and entry.get("live_status") == "is_upcoming":
await self.notifier.added(dl)
def __check_custom_name_conflict(self, directory, custom_name):
base_path = os.path.join(directory, custom_name)
if os.path.exists(base_path):
return {'status': 'error', 'msg': 'custom filename already exists'}
pattern = os.path.join(directory, f'{custom_name}.*')
for candidate in glob.glob(pattern):
if os.path.isfile(candidate):
return {'status': 'error', 'msg': 'custom filename already exists'}
return None
async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start, already):
if not entry:
return {'status': 'error', 'msg': "Invalid/empty data was given."}
error = None
if "live_status" in entry and "release_timestamp" in entry and entry.get("live_status") == "is_upcoming":
dt_ts = datetime.fromtimestamp(entry.get("release_timestamp")).strftime('%Y-%m-%d %H:%M:%S %z')
error = f"Live stream is scheduled to start at {dt_ts}"
else:
@ -374,13 +394,15 @@ class DownloadQueue:
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':
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)))
results = []
return await self.add(entry['url'], quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start, already)
elif etype == 'playlist':
if custom_name:
return {'status': 'error', 'msg': 'custom name is only supported for single downloads'}
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)))
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]
@ -395,28 +417,30 @@ class DownloadQueue:
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'}
elif etype == 'video' or (etype.startswith('url') and 'id' in entry and 'title' in entry):
log.debug('Processing as a video')
key = entry.get('webpage_url') or entry['url']
if not self.queue.exists(key):
dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], key, quality, format, folder, custom_name_prefix, error, entry, playlist_item_limit)
await self.__add_download(dl, auto_start)
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):
log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} {playlist_strict_mode=} {playlist_item_limit=} {auto_start=}')
already = set() if already is None else already
if url in already:
log.info('recursion detected, skipping')
return {'status': 'ok'}
elif etype == 'video' or (etype.startswith('url') and 'id' in entry and 'title' in entry):
log.debug('Processing as a video')
key = entry.get('webpage_url') or entry['url']
if not self.queue.exists(key):
dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], key, quality, format, folder, custom_name_prefix, custom_name, error, entry, playlist_item_limit)
result = await self.__add_download(dl, auto_start)
if isinstance(result, dict) and result.get('status') == 'error':
return result
return {'status': 'ok'}
return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}
async def add(self, url, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start=True, already=None):
log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} {custom_name=} {playlist_strict_mode=} {playlist_item_limit=} {auto_start=}')
already = set() if already is None else already
if url in already:
log.info('recursion detected, skipping')
return {'status': 'ok'}
else:
already.add(url)
try:
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, playlist_strict_mode, playlist_item_limit, auto_start, already)
return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start, already)
async def start_pending(self, ids):
for id in ids:

View file

@ -91,6 +91,22 @@
</div>
</div>
</div>
<div class="row mb-3">
<div class="col">
<label for="customName" class="form-label mb-1">Name (no extension) <span class="text-muted">(optional)</span></label>
<input type="text"
id="customName"
name="customName"
class="form-control"
autocomplete="off"
spellcheck="false"
placeholder="e.g. my-video"
[disabled]="addInProgress || downloads.loading"
[(ngModel)]="customName"
[ngModelOptions]="{standalone: true}">
<small class="form-text text-muted">The extension is added automatically; avoid dots or slashes.</small>
</div>
</div>
<!-- Options Row -->
<div class="row mb-3 g-3">

View file

@ -33,6 +33,7 @@ export class AppComponent implements AfterViewInit {
format: string;
folder: string;
customNamePrefix: string;
customName: string = '';
autoStart: boolean;
playlistStrictMode: boolean;
playlistItemLimit: number;
@ -270,14 +271,31 @@ export class AppComponent implements AfterViewInit {
playlistStrictMode = playlistStrictMode ?? this.playlistStrictMode
playlistItemLimit = playlistItemLimit ?? this.playlistItemLimit
autoStart = autoStart ?? this.autoStart
let sanitizedCustomName = '';
if (this.customName && this.customName.trim()) {
const raw = this.customName.trim();
if (/[\\/]/.test(raw) || raw.includes('..')) {
alert('Name cannot include path separators or "..".');
return;
}
sanitizedCustomName = this.getFilenameStem(raw);
if (!sanitizedCustomName) {
alert('Invalid name provided.');
return;
}
this.customName = sanitizedCustomName;
} else {
this.customName = '';
}
console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix+' playlistStrictMode='+playlistStrictMode+' playlistItemLimit='+playlistItemLimit+' autoStart='+autoStart);
console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix+' customName='+sanitizedCustomName+' playlistStrictMode='+playlistStrictMode+' playlistItemLimit='+playlistItemLimit+' autoStart='+autoStart);
this.addInProgress = true;
this.downloads.add(url, quality, format, folder, customNamePrefix, playlistStrictMode, playlistItemLimit, autoStart).subscribe((status: Status) => {
this.downloads.add(url, quality, format, folder, customNamePrefix, playlistStrictMode, playlistItemLimit, autoStart, sanitizedCustomName).subscribe((status: Status) => {
if (status.status === 'error') {
alert(`Error adding URL: ${status.msg}`);
} else {
this.addUrl = '';
this.customName = '';
}
this.addInProgress = false;
});
@ -479,7 +497,7 @@ export class AppComponent implements AfterViewInit {
this.batchImportStatus = `Importing URL ${index + 1} of ${urls.length}: ${url}`;
// Now pass the selected quality, format, folder, etc. to the add() method
this.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix,
this.playlistStrictMode, this.playlistItemLimit, this.autoStart)
this.playlistStrictMode, this.playlistItemLimit, this.autoStart, undefined)
.subscribe({
next: (status: Status) => {
if (status.status === 'error') {

View file

@ -26,6 +26,7 @@ export interface Download {
speed: number;
eta: number;
filename: string;
custom_name?: string;
checked?: boolean;
deleting?: boolean;
}
@ -125,8 +126,12 @@ export class DownloadsService {
return of({status: 'error', msg: msg})
}
public add(url: string, quality: string, format: string, folder: string, customNamePrefix: string, playlistStrictMode: boolean, playlistItemLimit: number, autoStart: boolean) {
return this.http.post<Status>('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(
public add(url: string, quality: string, format: string, folder: string, customNamePrefix: string, playlistStrictMode: boolean, playlistItemLimit: number, autoStart: boolean, customName?: string) {
const payload: Record<string, any> = {url: url, quality: quality, format: format, folder: folder, custom_name_prefix: customNamePrefix, playlist_strict_mode: playlistStrictMode, playlist_item_limit: playlistItemLimit, auto_start: autoStart};
if (customName) {
payload.custom_name = customName;
}
return this.http.post<Status>('add', payload).pipe(
catchError(this.handleHTTPError)
);
}