Allow same URL downloads for different output types

This commit is contained in:
TonyBlu 2026-05-01 09:14:54 +08:00
parent 85b86cb47f
commit b0fa114663

View file

@ -666,14 +666,25 @@ class PersistentQueue:
self.dict[k] = Download(None, None, None, None, getattr(v, 'quality', 'best'), getattr(v, 'format', 'any'), {}, v) self.dict[k] = Download(None, None, None, None, getattr(v, 'quality', 'best'), getattr(v, 'format', 'any'), {}, v)
def exists(self, key): def exists(self, key):
return key in self.dict return self._resolve_key(key) is not None
def get(self, key): 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): def items(self):
return self.dict.items() 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): def saved_items(self):
items = [ items = [
(item["key"], _download_info_from_record(item["info"])) (item["key"], _download_info_from_record(item["info"]))
@ -739,7 +750,7 @@ class PersistentQueue:
return items return items
def put(self, value): def put(self, value):
key = value.info.url key = getattr(value.info, "key", value.info.url)
old = self.dict.get(key) old = self.dict.get(key)
self.dict[key] = value self.dict[key] = value
try: try:
@ -752,13 +763,14 @@ class PersistentQueue:
raise raise
def delete(self, key): def delete(self, key):
if key in self.dict: resolved = self._resolve_key(key)
old = self.dict[key] if resolved is not None:
del self.dict[key] old = self.dict[resolved]
del self.dict[resolved]
try: try:
self._save_dict() self._save_dict()
except Exception: except Exception:
self.dict[key] = old self.dict[resolved] = old
raise raise
def next(self): def next(self):
@ -781,6 +793,25 @@ class DownloadQueue:
self._add_generation = 0 self._add_generation = 0
self._canceled_urls = set() # URLs canceled during current playlist add 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): def cancel_add(self):
self._add_generation += 1 self._add_generation += 1
log.info('Playlist add operation canceled by user') log.info('Playlist add operation canceled by user')
@ -818,10 +849,11 @@ class DownloadQueue:
pass pass
download.info.status = 'error' download.info.status = 'error'
download.close() download.close()
if self.queue.exists(download.info.url): key = getattr(download.info, 'key', download.info.url)
self.queue.delete(download.info.url) if self.queue.exists(key):
self.queue.delete(key)
if download.canceled: if download.canceled:
asyncio.create_task(self.notifier.canceled(download.info.url)) asyncio.create_task(self.notifier.canceled(key))
else: else:
self.done.put(download) self.done.put(download)
asyncio.create_task(self.notifier.completed(download.info)) 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') 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 clear_after = 0
if 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) 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) await asyncio.sleep(delay_seconds)
if self.done.exists(url): if self.done.exists(key):
log.debug(f'Auto-clearing completed download: {url}') log.debug(f'Auto-clearing completed download: {key}')
await self.clear([url]) await self.clear([key])
def _build_ytdl_options(self, ytdl_options_presets=None, ytdl_options_overrides=None): def _build_ytdl_options(self, ytdl_options_presets=None, ytdl_options_overrides=None):
"""Merge global options, presets (in order), and per-download overrides.""" """Merge global options, presets (in order), and per-download overrides."""
@ -1037,29 +1069,30 @@ class DownloadQueue:
if key in self._canceled_urls: if key in self._canceled_urls:
log.info(f'Skipping canceled URL: {entry.get("title") or key}') log.info(f'Skipping canceled URL: {entry.get("title") or key}')
return {'status': 'ok'} return {'status': 'ok'}
if not self.queue.exists(key): dl = DownloadInfo(
dl = DownloadInfo( id=entry['id'],
id=entry['id'], title=entry.get('title') or entry['id'],
title=entry.get('title') or entry['id'], url=key,
url=key, quality=quality,
quality=quality, download_type=download_type,
download_type=download_type, codec=codec,
codec=codec, format=format,
format=format, folder=folder,
folder=folder, custom_name_prefix=custom_name_prefix,
custom_name_prefix=custom_name_prefix, error=error,
error=error, entry=entry,
entry=entry, playlist_item_limit=playlist_item_limit,
playlist_item_limit=playlist_item_limit, split_by_chapters=split_by_chapters,
split_by_chapters=split_by_chapters, chapter_template=chapter_template,
chapter_template=chapter_template, subtitle_language=subtitle_language,
subtitle_language=subtitle_language, subtitle_mode=subtitle_mode,
subtitle_mode=subtitle_mode, ytdl_options_presets=ytdl_options_presets,
ytdl_options_presets=ytdl_options_presets, ytdl_options_overrides=ytdl_options_overrides,
ytdl_options_overrides=ytdl_options_overrides, clip_start=clip_start,
clip_start=clip_start, clip_end=clip_end,
clip_end=clip_end, )
) dl.key = self._download_key(dl)
if not self.queue.exists(dl.key):
await self.__add_download(dl, auto_start) await self.__add_download(dl, auto_start)
return {'status': 'ok'} return {'status': 'ok'}
return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'} return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}