From c61542f852b50ca70d1cd53c543b6863ee9a1973 Mon Sep 17 00:00:00 2001 From: xerdream Date: Tue, 19 Aug 2025 08:39:35 +0800 Subject: [PATCH 1/4] Add "Format" column to frontend interface to display downloaded audio/video formats --- app/ytdl.py | 15 ++++++++------- ui/src/app/app.component.html | 8 ++++++-- ui/src/app/downloads.service.ts | 14 +++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/ytdl.py b/app/ytdl.py index d4955f3..511ef6f 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -33,6 +33,7 @@ class DownloadQueueNotifier: class DownloadInfo: def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error): self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' + self.id = f'{id}.{format}' self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}' self.url = url self.quality = quality @@ -203,7 +204,7 @@ class PersistentQueue: return sorted(shelf.items(), key=lambda item: item[1].timestamp) def put(self, value): - key = value.info.url + key = value.info.id self.dict[key] = value with shelve.open(self.path, 'w') as shelf: shelf[key] = value.info @@ -285,10 +286,10 @@ class DownloadQueue: pass download.info.status = 'error' download.close() - if self.queue.exists(download.info.url): - self.queue.delete(download.info.url) + if self.queue.exists(download.info.id): + self.queue.delete(download.info.id) if download.canceled: - asyncio.create_task(self.notifier.canceled(download.info.url)) + asyncio.create_task(self.notifier.canceled(download.info.id)) else: self.done.put(download) asyncio.create_task(self.notifier.completed(download.info)) @@ -361,9 +362,9 @@ class DownloadQueue: 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) + url = entry.get('webpage_url') or entry['url'] + dl = DownloadInfo(entry['id'], entry.get('title') or entry['id'], url, quality, format, folder, custom_name_prefix, error) + if not self.queue.exists(dl.id): dldirectory, error_message = self.__calc_download_path(quality, format, folder) if error_message is not None: return error_message diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index a536154..1b3d9f1 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -290,7 +290,7 @@ - Video + Media Speed ETA @@ -336,7 +336,8 @@ - Video + Media + Format File Size @@ -358,6 +359,9 @@
Error: {{download.value.error}}
+ + {{ download.value.format }} + {{ download.value.size | fileSize }} diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index cf63a5e..ae1e599 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -59,21 +59,21 @@ export class DownloadsService { }); socket.fromEvent('added').subscribe((strdata: string) => { let data: Download = JSON.parse(strdata); - this.queue.set(data.url, data); + this.queue.set(data.id, data); this.queueChanged.next(null); }); socket.fromEvent('updated').subscribe((strdata: string) => { let data: Download = JSON.parse(strdata); - let dl: Download = this.queue.get(data.url); + let dl: Download = this.queue.get(data.id); data.checked = dl.checked; data.deleting = dl.deleting; - this.queue.set(data.url, data); + this.queue.set(data.id, data); this.updated.next(null); }); socket.fromEvent('completed').subscribe((strdata: string) => { let data: Download = JSON.parse(strdata); - this.queue.delete(data.url); - this.done.set(data.url, data); + this.queue.delete(data.id); + this.done.set(data.id, data); this.queueChanged.next(null); this.doneChanged.next(null); }); @@ -127,13 +127,13 @@ export class DownloadsService { public startByFilter(where: string, filter: (dl: Download) => boolean) { let ids: string[] = []; - this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.url) }); + this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.id) }); return this.startById(ids); } public delByFilter(where: string, filter: (dl: Download) => boolean) { let ids: string[] = []; - this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.url) }); + this[where].forEach((dl: Download) => { if (filter(dl)) ids.push(dl.id) }); return this.delById(where, ids); } public addDownloadByUrl(url: string): Promise { From c4f3468b6cee6d071bb094dc98d930cc1b413a01 Mon Sep 17 00:00:00 2001 From: xerdream Date: Tue, 19 Aug 2025 09:52:33 +0800 Subject: [PATCH 2/4] Fix incomplete cleanup of temporary files Fix mistakenly deleting previously saved intermediate files during post-processing cleanup --- app/ytdl.py | 52 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/app/ytdl.py b/app/ytdl.py index 511ef6f..421bda6 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -52,8 +52,8 @@ class Download: def __init__(self, download_dir, temp_dir, output_template, output_template_chapter, quality, format, ytdl_opts, info): self.download_dir = download_dir self.temp_dir = temp_dir - self.output_template = output_template - self.output_template_chapter = output_template_chapter + self.output_template = self._add_format_identifier(format, output_template) + self.output_template_chapter = self._add_format_identifier(format, output_template_chapter) self.format = get_format(format, quality) self.ytdl_opts = get_opts(format, quality, ytdl_opts) if "impersonate" in self.ytdl_opts: @@ -140,6 +140,8 @@ class Download: if self.status_queue is not None: self.status_queue.put(None) + self._delete_format_identifier() + def running(self): try: return self.proc is not None and self.proc.is_alive() @@ -175,6 +177,46 @@ class Download: self.info.eta = status.get('eta') log.info(f"Updating status for {self.info.title}: {status}") await self.notifier.updated(self.info) + + def _add_format_identifier(self, identifier, template): + # Preventing the post-processing of YT-DLP from deleting the intermediate file which was download before. + return f'{identifier}_{template}' + + def _delete_format_identifier(self): + # Delete the identifier in the file name after the post-processing is complete. + if self.canceled or self.info.status != 'finished' or not hasattr(self.info,'filename'): + return + + try: + filename = re.sub(r'^\w+_', '', self.info.filename) + filepath_idt = os.path.join(self.download_dir, self.info.filename) + filepath = os.path.join(self.download_dir, filename) + if os.path.exists(filepath): + os.remove(filepath) + os.rename(filepath_idt, filepath) + log.info(f"Renamed file '{filepath_idt}' to '{filepath}'") + except PermissionError as e: + log.warning(f"Error deleting old file '{filepath}': {e} ") + return + except Exception as e: + log.warning(f"Error renaming file '{filepath_idt}': {e} ") + return + + self.info.filename = filename + + def delete_tmpfile(self): + if not self.tmpfilename: + return + tmpfilename = self.tmpfilename.rsplit('.')[0] + def is_tmpfile(filename): + return filename.startswith(tmpfilename) + + tmpfiles = filter(is_tmpfile ,os.listdir(self.download_dir)) + try: + for tmpfile in tmpfiles: + os.remove(tmpfile) + except: + pass class PersistentQueue: def __init__(self, path): @@ -279,11 +321,7 @@ class DownloadQueue: def _post_download_cleanup(self, download): if download.info.status != 'finished': - if download.tmpfilename and os.path.isfile(download.tmpfilename): - try: - os.remove(download.tmpfilename) - except: - pass + download.delete_tmpfile() download.info.status = 'error' download.close() if self.queue.exists(download.info.id): From 2b2a2e69f3ca5f06783a04724d136bb4f4854db2 Mon Sep 17 00:00:00 2001 From: xerdream Date: Wed, 20 Aug 2025 08:19:41 +0800 Subject: [PATCH 3/4] change some code that may cause errors --- app/ytdl.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/ytdl.py b/app/ytdl.py index 421bda6..bd79b86 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -33,7 +33,7 @@ class DownloadQueueNotifier: class DownloadInfo: def __init__(self, id, title, url, quality, format, folder, custom_name_prefix, error): self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' - self.id = f'{id}.{format}' + self.id = f'{self.id}.{format}' self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}' self.url = url self.quality = quality @@ -205,18 +205,21 @@ class Download: self.info.filename = filename def delete_tmpfile(self): - if not self.tmpfilename: + if not self.tmpfilename or not self.download_dir: return + if not os.path.isdir(self.download_dir): + return + tmpfilename = self.tmpfilename.rsplit('.')[0] def is_tmpfile(filename): return filename.startswith(tmpfilename) - tmpfiles = filter(is_tmpfile ,os.listdir(self.download_dir)) try: + tmpfiles = filter(is_tmpfile, os.listdir(self.download_dir)) for tmpfile in tmpfiles: - os.remove(tmpfile) - except: - pass + os.remove(os.path.join(self.download_dir, tmpfile)) + except Exception as e: + log.warning(f"Error deleting temporary files: {e}") class PersistentQueue: def __init__(self, path): From 93f973df50af24b456ea09ca84897bfbff333f9e Mon Sep 17 00:00:00 2001 From: xerdream Date: Wed, 20 Aug 2025 14:29:01 +0800 Subject: [PATCH 4/4] Fixe temporary files were not deleted as expected --- app/ytdl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ytdl.py b/app/ytdl.py index bd79b86..5be7a29 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -210,7 +210,7 @@ class Download: if not os.path.isdir(self.download_dir): return - tmpfilename = self.tmpfilename.rsplit('.')[0] + tmpfilename = os.path.basename(self.tmpfilename) def is_tmpfile(filename): return filename.startswith(tmpfilename)