This commit is contained in:
Bjoernsen 2025-11-12 18:00:05 -08:00 committed by GitHub
commit b6eb9e5668
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 101 additions and 22 deletions

View file

@ -220,12 +220,15 @@ async def add(request):
format = post.get('format')
folder = post.get('folder')
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 None:
custom_name = ''
if auto_start is None:
auto_start = True
if playlist_strict_mode is None:
@ -235,7 +238,7 @@ async def add(request):
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, 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

@ -14,6 +14,24 @@ from datetime import datetime
log = logging.getLogger('ytdl')
def sanitize_custom_input(input):
# Remove or replace dangerous characters
# Path separators and traversal patterns
input = input.replace('/', '_')
input = input.replace('\\', '_')
input = input.replace('..', '_')
# Command injection patterns
input = re.sub(r'[;&|`$(){}[\]<>]', '_', input)
# Control characters and other problematic characters
input = re.sub(r'[\x00-\x1f\x7f-\x9f]', '_', input)
# Replace multiple underscores with single underscore
input = re.sub(r'_+', '_', input)
return input
class DownloadQueueNotifier:
async def added(self, dl):
raise NotImplementedError
@ -31,14 +49,30 @@ class DownloadQueueNotifier:
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}'
def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, custom_name, error, entry, playlist_item_limit):
# Sanitize custom inputs to prevent path traversal and command injection attacks
if custom_name:
custom_name = sanitize_custom_input(custom_name)
if custom_name_prefix:
custom_name_prefix = sanitize_custom_input(custom_name_prefix)
# Handle custom name logic - if custom_name is provided, use it; otherwise use prefix logic
if custom_name and len(custom_name.strip()) > 0:
self.id = id
self.title = custom_name
elif len(custom_name_prefix) > 0:
self.id = f'{custom_name_prefix}.{id}'
self.title = f'{custom_name_prefix}.{title}'
else:
self.id = id
self.title = 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
@ -333,12 +367,37 @@ class DownloadQueue:
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}'
# Handle custom naming in output template
if dl.custom_name and len(dl.custom_name.strip()) > 0:
# Create a custom output template using the custom name
# Replace %(title)s with the custom name in the template
output = self.config.OUTPUT_TEMPLATE.replace('%(title)s', dl.custom_name)
elif len(dl.custom_name_prefix) > 0:
output = f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}'
else:
output = 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
# Start with the playlist template
playlist_output = self.config.OUTPUT_TEMPLATE_PLAYLIST
# Apply custom naming logic to playlist template
if dl.custom_name and len(dl.custom_name.strip()) > 0:
# If playlist has multiple entries, append entry id to custom name to avoid overwriting
playlist_entries = entry.get('entries')
if playlist_entries and isinstance(playlist_entries, list) and len(playlist_entries) > 1:
entry_id = entry.get('id') or ''
custom_name_with_id = f"{dl.custom_name}_{entry_id}" if entry_id else dl.custom_name
output = playlist_output.replace('%(title)s', custom_name_with_id)
else:
output = playlist_output.replace('%(title)s', dl.custom_name)
elif len(dl.custom_name_prefix) > 0:
# Add prefix to the playlist template
output = f'{dl.custom_name_prefix}.{playlist_output}'
else:
output = playlist_output
for property, value in entry.items():
if property.startswith("playlist"):
output = output.replace(f"%({property})s", str(value))
@ -347,6 +406,7 @@ class DownloadQueue:
if playlist_item_limit > 0:
log.info(f'playlist limit is set. Processing only first {playlist_item_limit} entries')
ytdl_options['playlistend'] = playlist_item_limit
download = Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, dl.quality, dl.format, ytdl_options, dl)
if auto_start is True:
self.queue.put(download)
@ -355,7 +415,7 @@ class DownloadQueue:
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):
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."}
@ -371,7 +431,7 @@ 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)
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':
log.debug('Processing as a playlist')
entries = entry['entries']
@ -388,7 +448,7 @@ class DownloadQueue:
for property in ("id", "title", "uploader", "uploader_id"):
if property in entry:
etr[f"playlist_{property}"] = entry[property]
results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, playlist_strict_mode, playlist_item_limit, auto_start, already))
results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, custom_name, playlist_strict_mode, playlist_item_limit, auto_start, already))
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'}
@ -396,13 +456,13 @@ class DownloadQueue:
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)
dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], key, quality, format, folder, custom_name_prefix, custom_name, 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=}')
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')
@ -413,7 +473,7 @@ class DownloadQueue:
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

@ -179,6 +179,18 @@
ngbTooltip="Add a prefix to downloaded filenames">
</div>
</div>
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">Custom Name</span>
<input type="text"
class="form-control"
placeholder="Default"
name="customName"
[(ngModel)]="customName"
[disabled]="addInProgress || downloads.loading"
ngbTooltip="Rename downloaded file (extension will be added automatically)">
</div>
</div>
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">Items Limit</span>
@ -193,7 +205,7 @@
ngbTooltip="Maximum number of items to download from a playlist (0 = no limit)">
</div>
</div>
<div class="col-12">
<div class="col-6">
<div class="form-check form-switch">
<input class="form-check-input"
type="checkbox"

View file

@ -26,6 +26,7 @@ export class AppComponent implements AfterViewInit {
format: string;
folder: string;
customNamePrefix: string;
customName: string;
autoStart: boolean;
playlistStrictMode: boolean;
playlistItemLimit: number;
@ -252,19 +253,20 @@ export class AppComponent implements AfterViewInit {
this.quality = exists ? this.quality : 'best'
}
addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, playlistStrictMode?: boolean, playlistItemLimit?: number, autoStart?: boolean) {
addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, customName?: string, playlistStrictMode?: boolean, playlistItemLimit?: number, autoStart?: boolean) {
url = url ?? this.addUrl
quality = quality ?? this.quality
format = format ?? this.format
folder = folder ?? this.folder
customNamePrefix = customNamePrefix ?? this.customNamePrefix
customName = customName ?? this.customName
playlistStrictMode = playlistStrictMode ?? this.playlistStrictMode
playlistItemLimit = playlistItemLimit ?? this.playlistItemLimit
autoStart = autoStart ?? this.autoStart
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='+customName+' 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, customName, playlistStrictMode, playlistItemLimit, autoStart).subscribe((status: Status) => {
if (status.status === 'error') {
alert(`Error adding URL: ${status.msg}`);
} else {
@ -279,7 +281,7 @@ export class AppComponent implements AfterViewInit {
}
retryDownload(key: string, download: Download) {
this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix, download.playlist_strict_mode, download.playlist_item_limit, true);
this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix, download.custom_name, download.playlist_strict_mode, download.playlist_item_limit, true);
this.downloads.delById('done', [key]).subscribe();
}
@ -397,7 +399,7 @@ export class AppComponent implements AfterViewInit {
const url = urls[index];
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.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix, this.customName,
this.playlistStrictMode, this.playlistItemLimit, this.autoStart)
.subscribe({
next: (status: Status) => {

View file

@ -17,6 +17,7 @@ export interface Download {
format: string;
folder: string;
custom_name_prefix: string;
custom_name: string;
playlist_strict_mode: boolean;
playlist_item_limit: number;
status: string;
@ -110,8 +111,8 @@ 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, customName: 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, custom_name: customName, playlist_strict_mode: playlistStrictMode, playlist_item_limit: playlistItemLimit, auto_start: autoStart}).pipe(
catchError(this.handleHTTPError)
);
}
@ -141,12 +142,13 @@ export class DownloadsService {
const defaultFormat = 'mp4';
const defaultFolder = '';
const defaultCustomNamePrefix = '';
const defaultCustomName = '';
const defaultPlaylistStrictMode = false;
const defaultPlaylistItemLimit = 0;
const defaultAutoStart = true;
return new Promise((resolve, reject) => {
this.add(url, defaultQuality, defaultFormat, defaultFolder, defaultCustomNamePrefix, defaultPlaylistStrictMode, defaultPlaylistItemLimit, defaultAutoStart)
this.add(url, defaultQuality, defaultFormat, defaultFolder, defaultCustomNamePrefix, defaultCustomName, defaultPlaylistStrictMode, defaultPlaylistItemLimit, defaultAutoStart)
.subscribe(
response => resolve(response),
error => reject(error)