From b0fa1146638d24ccd52d52000adaa54a5ef982a9 Mon Sep 17 00:00:00 2001 From: TonyBlu <101348912+TonyBlur@users.noreply.github.com> Date: Fri, 1 May 2026 09:14:54 +0800 Subject: [PATCH] Allow same URL downloads for different output types --- app/ytdl.py | 109 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 38 deletions(-) diff --git a/app/ytdl.py b/app/ytdl.py index 940fca3..07d7f0c 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -666,14 +666,25 @@ class PersistentQueue: self.dict[k] = Download(None, None, None, None, getattr(v, 'quality', 'best'), getattr(v, 'format', 'any'), {}, v) def exists(self, key): - return key in self.dict + return self._resolve_key(key) is not None def get(self, key): - return self.dict[key] + resolved = self._resolve_key(key) + if resolved is None: + raise KeyError(key) + return self.dict[resolved] def items(self): return self.dict.items() + def _resolve_key(self, key): + if key in self.dict: + return key + for existing_key, download in self.dict.items(): + if getattr(download.info, "url", None) == key: + return existing_key + return None + def saved_items(self): items = [ (item["key"], _download_info_from_record(item["info"])) @@ -739,7 +750,7 @@ class PersistentQueue: return items def put(self, value): - key = value.info.url + key = getattr(value.info, "key", value.info.url) old = self.dict.get(key) self.dict[key] = value try: @@ -752,13 +763,14 @@ class PersistentQueue: raise def delete(self, key): - if key in self.dict: - old = self.dict[key] - del self.dict[key] + resolved = self._resolve_key(key) + if resolved is not None: + old = self.dict[resolved] + del self.dict[resolved] try: self._save_dict() except Exception: - self.dict[key] = old + self.dict[resolved] = old raise def next(self): @@ -781,6 +793,25 @@ class DownloadQueue: self._add_generation = 0 self._canceled_urls = set() # URLs canceled during current playlist add + @staticmethod + def _download_key(dl: DownloadInfo) -> str: + return "|".join([ + dl.url, + dl.download_type, + dl.codec, + dl.format, + dl.quality, + dl.folder or "", + dl.custom_name_prefix or "", + str(dl.playlist_item_limit or 0), + "1" if dl.split_by_chapters else "0", + dl.chapter_template or "", + dl.subtitle_language or "", + dl.subtitle_mode or "", + str(dl.clip_start if dl.clip_start is not None else ""), + str(dl.clip_end if dl.clip_end is not None else ""), + ]) + def cancel_add(self): self._add_generation += 1 log.info('Playlist add operation canceled by user') @@ -818,10 +849,11 @@ class DownloadQueue: pass download.info.status = 'error' download.close() - if self.queue.exists(download.info.url): - self.queue.delete(download.info.url) + key = getattr(download.info, 'key', download.info.url) + if self.queue.exists(key): + self.queue.delete(key) if download.canceled: - asyncio.create_task(self.notifier.canceled(download.info.url)) + asyncio.create_task(self.notifier.canceled(key)) else: self.done.put(download) asyncio.create_task(self.notifier.completed(download.info)) @@ -831,14 +863,14 @@ class DownloadQueue: log.error(f'CLEAR_COMPLETED_AFTER is set to an invalid value "{self.config.CLEAR_COMPLETED_AFTER}", expected an integer number of seconds') clear_after = 0 if clear_after > 0: - task = asyncio.create_task(self.__auto_clear_after_delay(download.info.url, clear_after)) + task = asyncio.create_task(self.__auto_clear_after_delay(key, clear_after)) task.add_done_callback(lambda t: log.error(f'Auto-clear task failed: {t.exception()}') if not t.cancelled() and t.exception() else None) - async def __auto_clear_after_delay(self, url, delay_seconds): + async def __auto_clear_after_delay(self, key, delay_seconds): await asyncio.sleep(delay_seconds) - if self.done.exists(url): - log.debug(f'Auto-clearing completed download: {url}') - await self.clear([url]) + if self.done.exists(key): + log.debug(f'Auto-clearing completed download: {key}') + await self.clear([key]) def _build_ytdl_options(self, ytdl_options_presets=None, ytdl_options_overrides=None): """Merge global options, presets (in order), and per-download overrides.""" @@ -1037,29 +1069,30 @@ class DownloadQueue: if key in self._canceled_urls: log.info(f'Skipping canceled URL: {entry.get("title") or key}') return {'status': 'ok'} - if not self.queue.exists(key): - dl = DownloadInfo( - id=entry['id'], - title=entry.get('title') or entry['id'], - url=key, - quality=quality, - download_type=download_type, - codec=codec, - format=format, - folder=folder, - custom_name_prefix=custom_name_prefix, - error=error, - entry=entry, - playlist_item_limit=playlist_item_limit, - split_by_chapters=split_by_chapters, - chapter_template=chapter_template, - subtitle_language=subtitle_language, - subtitle_mode=subtitle_mode, - ytdl_options_presets=ytdl_options_presets, - ytdl_options_overrides=ytdl_options_overrides, - clip_start=clip_start, - clip_end=clip_end, - ) + dl = DownloadInfo( + id=entry['id'], + title=entry.get('title') or entry['id'], + url=key, + quality=quality, + download_type=download_type, + codec=codec, + format=format, + folder=folder, + custom_name_prefix=custom_name_prefix, + error=error, + entry=entry, + playlist_item_limit=playlist_item_limit, + split_by_chapters=split_by_chapters, + chapter_template=chapter_template, + subtitle_language=subtitle_language, + subtitle_mode=subtitle_mode, + ytdl_options_presets=ytdl_options_presets, + ytdl_options_overrides=ytdl_options_overrides, + clip_start=clip_start, + clip_end=clip_end, + ) + dl.key = self._download_key(dl) + if not self.queue.exists(dl.key): await self.__add_download(dl, auto_start) return {'status': 'ok'} return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}