diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py index a35ccd85..b1282f42 100644 --- a/app/library/ItemDTO.py +++ b/app/library/ItemDTO.py @@ -1,5 +1,6 @@ import json import logging +import re import time import uuid from dataclasses import dataclass, field @@ -115,6 +116,12 @@ class Item: msg = "url param is required." raise ValueError(msg) + url = url.strip() + + # If it's only a YouTube video ID, convert to a full URL. + if len(url) >= 11 and re.fullmatch(r"[A-Za-z0-9_-]{11}", url): + url = f"https://www.youtube.com/watch?v={url}" + data: dict[str, str] = {"url": url} preset: str | None = item.get("preset") diff --git a/app/library/Presets.py b/app/library/Presets.py index 08d582d5..425061cf 100644 --- a/app/library/Presets.py +++ b/app/library/Presets.py @@ -19,33 +19,34 @@ DEFAULT_PRESETS: list[dict[int, dict[str, str | bool]]] = [ "id": "3e163c6c-64eb-4448-924f-814b629b3810", "name": "default", "default": True, + "cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log", "description": "Default preset for yt-dlp. It will download whatever yt-dlp decides is the best quality for the video and audio.", }, { "id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48d", "name": "Mobile", - "cli": '-t mp4 --merge-output-format mp4 --add-chapters --remux-video mp4 \n--embed-metadata --embed-thumbnail \n--postprocessor-args "-movflags +faststart"', + "cli": '--socket-timeout 30 --download-archive %(config_path)s/archive.log\n-t mp4 --merge-output-format mp4 --add-chapters --remux-video mp4 \n--embed-metadata --embed-thumbnail \n--postprocessor-args "-movflags +faststart"', "default": True, "description": "This preset is designed for mobile devices. It will download the best quality video and audio in mp4 format, merge them, and add chapters, metadata, and thumbnail.", }, { "id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b", "name": "1080p", - "cli": "-S vcodec:h264 --format 'bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'", + "cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n-S vcodec:h264 --format 'bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'", "default": True, "description": "Download the best quality video and audio in mp4 format for 1080p resolution.", }, { "id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e", "name": "720p", - "cli": "-S vcodec:h264 --format 'bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'", + "cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n-S vcodec:h264 --format 'bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'", "default": True, "description": "Download the best quality video and audio in mp4 format for 720p resolution.", }, { "id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330", "name": "Audio Only", - "cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail --format 'bestaudio/best'", + "cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--extract-audio --add-chapters --embed-metadata --embed-thumbnail --format 'bestaudio/best'", "default": True, "description": "This preset is designed to download only the audio of the video. It will extract the audio, add chapters, metadata, and thumbnail.", }, @@ -55,7 +56,7 @@ DEFAULT_PRESETS: list[dict[int, dict[str, str | bool]]] = [ "description": 'This preset generate specific filename format and metadata to work with yt-dlp info reader plugins for jellyfin/emby and plex and to make play state sync work for WatchState.\n\nThere is one more step you need to do via Other > Terminal if you have it enabled or directly from container shell\n\nyt-dlp -I0 --write-info-json --write-thumbnail --convert-thumbnails jpg --paths /downloads/youtube -o "%(channel|Unknown_title)s [%(channel_id|Unknown_id)s]/%(title).180B [%(channel_id|Unknown_id)s].%(ext)s" -- https://www.youtube.com/channel/UCClfFsWcT3N2I7VTXXyt84A\n\nChange the url to the channel you want to download.\n\nFor more information please visit \nhttps://github.com/arabcoders/watchstate/blob/master/FAQ.md#how-to-get-watchstate-working-with-youtube-contentlibrary', "folder": "youtube", "template": "%(channel)s %(channel_id|Unknown_id)s/Season %(release_date>%Y,upload_date>%Y|Unknown)s/%(release_date>%Y%m%d,upload_date>%Y%m%d)s - %(title).180B [%(extractor)s-%(id)s].%(ext)s", - "cli": "--windows-filenames --write-info-json --embed-info-json \n--convert-thumbnails jpg --write-thumbnail --embed-metadata", + "cli": "--socket-timeout 30 --download-archive %(config_path)s/archive.log\n--windows-filenames --write-info-json --embed-info-json \n--convert-thumbnails jpg --write-thumbnail --embed-metadata", "default": True, }, ] @@ -92,6 +93,7 @@ class Preset: def json(self) -> str: from .encoder import Encoder + return Encoder().encode(self.serialize()) def get(self, key: str, default: Any = None) -> Any: diff --git a/app/library/Tasks.py b/app/library/Tasks.py index 7cc2af27..59c60a9c 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -504,6 +504,10 @@ class HandleTask: if not task.handler_enabled: continue + if not task.get_ytdlp_opts().get_all().get("download_archive"): + LOG.debug(f"Task '{task.name}' does not have an archive file configured.") + continue + try: handler = self._find_handler(task) if handler is None: @@ -565,6 +569,9 @@ class HandleTask: handlers: list[type] = [] for _, module_name, _ in pkgutil.iter_modules(handlers_pkg.__path__): + if module_name.startswith("_"): + continue + module = importlib.import_module(f"{handlers_pkg.__name__}.{module_name}") for _, cls in inspect.getmembers(module, inspect.isclass): if cls.__module__ != module.__name__: diff --git a/app/library/YTDLPOpts.py b/app/library/YTDLPOpts.py index ee421357..ab1b6239 100644 --- a/app/library/YTDLPOpts.py +++ b/app/library/YTDLPOpts.py @@ -22,7 +22,7 @@ class YTDLPOpts: """The command options for yt-dlp from preset.""" def __init__(self): - self._config = Config.get_instance() + self._config: Config = Config.get_instance() @staticmethod def get_instance() -> "YTDLPOpts": @@ -96,7 +96,7 @@ class YTDLPOpts: """ preset: Preset | None = Presets.get_instance().get(name) - if not preset or "default" == name: + if not preset: return self if preset.cli: @@ -169,7 +169,11 @@ class YTDLPOpts: if len(self._item_cli) > 0: try: - user_cli: dict = arg_converter(args="\n".join(self._item_cli), level=True) + user_cli = "\n".join(self._item_cli) + for k, v in self._config.get_replacers().items(): + user_cli = user_cli.replace(f"%({k})s", v if isinstance(v, str) else str(v)) + + user_cli: dict = arg_converter(args=user_cli, level=True) except Exception as e: msg = f"Invalid command options for yt-dlp were given. '{e!s}'." raise ValueError(msg) from e diff --git a/app/library/config.py b/app/library/config.py index a45fb16c..7331629b 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -152,9 +152,6 @@ class Config: file_logging: bool = True "Enable file logging." - sentry_dsn: str | None = None - "The Sentry DSN to use for error reporting." - secret_key: str "The secret key to use for the application." @@ -266,7 +263,6 @@ class Config: "basic_mode", "default_preset", "instance_title", - "sentry_dsn", "console_enabled", "browser_enabled", "browser_control_enabled", @@ -406,6 +402,9 @@ class Config: self._ytdlp_cli_mutable += f"\n--socket-timeout {self.socket_timeout}" if self.keep_archive: + LOG.warning( + "The global 'keep_archive' option is deprecated and will be removed in future releases. please use presets instead." + ) archive_file: Path = Path(self.archive_file) if not archive_file.exists(): LOG.info(f"Creating archive file '{archive_file}'.") @@ -541,6 +540,17 @@ class Config: data["ytdlp_version"] = Config._ytdlp_version() return data + def get_replacers(self) -> dict: + """ + Get the variables that can be used in Command options for yt-dlp. + + Returns: + dict: The replacer variables. + + """ + keys: tuple[str] = ("download_path", "temp_path", "config_path") + return {k: getattr(self, k) for k in keys} + @staticmethod def _ytdlp_version() -> str: try: diff --git a/app/library/task_handlers/_base_handler.py b/app/library/task_handlers/_base_handler.py new file mode 100644 index 00000000..5c79a6be --- /dev/null +++ b/app/library/task_handlers/_base_handler.py @@ -0,0 +1,105 @@ +# flake8: noqa: ARG004 +import logging +from typing import Any + +import httpx +from yt_dlp.utils.networking import random_user_agent + +from app.library.config import Config +from app.library.DownloadQueue import DownloadQueue +from app.library.Events import EventBus, Events +from app.library.ItemDTO import ItemDTO +from app.library.Tasks import Task + +LOG: logging.Logger = logging.getLogger(__name__) + + +class BaseHandler: + queued: set[str] = set() + failure_count: dict[str, int] = {} + + def __init_subclass__(cls, **kwargs): + """Ensure each subclass has its own state containers.""" + super().__init_subclass__(**kwargs) + if "queued" not in cls.__dict__: + cls.queued = set() + if "failure_count" not in cls.__dict__: + cls.failure_count = {} + + EventBus.get_instance().subscribe( + Events.ITEM_ERROR, + lambda data, _, **__: cls.on_error(data.data), + f"{cls.__name__}.on_error", + ) + + @staticmethod + def can_handle(task: Task) -> bool: + return False + + @staticmethod + async def handle(task: Task, notify: EventBus, config: Config, queue: DownloadQueue): + pass + + @staticmethod + def parse(url: str) -> Any | None: + return None + + @classmethod + async def on_error(cls, item: ItemDTO) -> None: + """ + Handle errors by logging them and removing the queued ID if it exists. + + Args: + item (ItemDTO): The error data containing the URL and other information. + + """ + if not item or not isinstance(item, ItemDTO): + return + + if not item.archive_id or not cls.failure_count.get(item.archive_id, None): + LOG.debug(f"Item '{item.name()}' not queued by the handler.") + return + + failCount: int = int(cls.failure_count.get(item.archive_id, 0)) + + LOG.info(f"Removing '{item.name()}' from queued IDs due to error. Failure count: '{failCount + 1}'.") + if item.archive_id in cls.queued: + cls.queued.remove(item.archive_id) + + cls.failure_count[item.archive_id] = 1 + failCount + + @staticmethod + def tests() -> list[tuple[str, bool]]: + return [] + + @staticmethod + async def request(url: str, headers: dict | None = None, ytdlp_opts: dict | None = None) -> httpx.Response: + headers = {} if not isinstance(headers, dict) else headers + ytdlp_opts = {} if not isinstance(ytdlp_opts, dict) else ytdlp_opts + + opts: dict[str, Any] = { + "headers": { + "User-Agent": random_user_agent(), + }, + } + + try: + from httpx_curl_cffi import AsyncCurlTransport, CurlOpt + + opts["transport"] = AsyncCurlTransport( + impersonate="chrome", + default_headers=True, + curl_options={CurlOpt.FRESH_CONNECT: True}, + ) + opts["headers"].pop("User-Agent", None) + except Exception: + pass + + for k, v in headers.items(): + opts["headers"][k] = v + + if proxy := ytdlp_opts.get("proxy", None): + opts["proxy"] = proxy + + async with httpx.AsyncClient(**opts) as client: + return await client.request(method="GET", url=url, timeout=ytdlp_opts.get("socket_timeout", 120)) diff --git a/app/library/task_handlers/twitch.py b/app/library/task_handlers/twitch.py new file mode 100644 index 00000000..f764041a --- /dev/null +++ b/app/library/task_handlers/twitch.py @@ -0,0 +1,189 @@ +import asyncio +import logging +import re +from typing import TYPE_CHECKING +from xml.etree.ElementTree import Element + +from app.library.DownloadQueue import DownloadQueue +from app.library.Events import EventBus, Events +from app.library.ItemDTO import Item +from app.library.Tasks import Task +from app.library.Utils import archive_read, get_archive_id + +from ._base_handler import BaseHandler + +if TYPE_CHECKING: + from xml.etree.ElementTree import Element + + from app.library.Download import Download + +LOG: logging.Logger = logging.getLogger(__name__) + + +class TwitchHandler(BaseHandler): + FEED = "https://twitchrss.appspot.com/vodonly/{handle}" + + RX: re.Pattern[str] = re.compile(r"^https?:\/\/(?:www\.|m\.)?twitch\.tv\/(?P[a-z0-9_]{3,25})(?:\/.*)?$") + + @staticmethod + def can_handle(task: Task) -> bool: + LOG.debug(f"Checking if task '{task.name}' is using parsable Twitch URL: {task.url}") + return TwitchHandler.parse(task.url) is not None + + @staticmethod + async def handle(task: Task, notify: EventBus, queue: DownloadQueue): + """ + Fetch the RSS feed for a Twitch channel VODs, parse entries, + and enqueue new items that are not in the archive/queue already. + + Args: + task (Task): The task containing the Twitch channel URL. + notify (EventBus): The event bus for notifications. + queue (DownloadQueue): The download queue instance. + + """ + from defusedxml.ElementTree import fromstring + + handleName: str | None = TwitchHandler.parse(task.url) + if not handleName: + LOG.error(f"Cannot parse '{task.name}' URL: {task.url}") + return + + params: dict = task.get_ytdlp_opts().get_all() + archive_file: str | None = params.get("download_archive") + if not archive_file: + LOG.error(f"Task '{task.name}' does not have an archive file.") + return + + feed_url: str = TwitchHandler.FEED.format(handle=handleName) + + LOG.debug(f"Fetching '{task.name}' feed.") + response = await TwitchHandler.request(url=feed_url, ytdlp_opts=params) + response.raise_for_status() + + items: list = [] + has_items = False + + root: Element[str] = fromstring(response.text) + for entry in root.findall("channel/item"): + link_elem: Element[str] | None = entry.find("link") + url: str = link_elem.text.strip() if link_elem is not None and link_elem.text else "" + if not url: + LOG.warning(f"Entry in '{task.name}' feed is missing URL. Skipping entry.") + continue + + m: re.Match[str] | None = re.search(r"^https?://(?:www\.)?twitch\.tv/videos/(?P\d+)(?:[/?].*)?$", url) + if not m: + LOG.warning(f"URL in '{task.name}' feed does not look like a VOD link: {url}") + continue + + vid: str = m.group("id") + + title_elem: Element[str] | None = entry.find("title") + title: str = title_elem.text.strip() if title_elem is not None and title_elem.text else "" + + has_items = True + + id_dict = get_archive_id(url) + archive_id: str | None = id_dict.get("archive_id") + if not archive_id: + LOG.warning(f"Could not compute archive ID for video '{vid}' in '{task.name}' feed. Skipping entry.") + continue + + if archive_id in TwitchHandler.queued: + continue + + items.append({"id": vid, "url": url, "title": title, "archive_id": archive_id}) + + if len(items) < 1: + if not has_items: + LOG.warning(f"No entries found in '{task.name}' feed. URL: {feed_url}") + else: + LOG.debug(f"No new items found in '{task.name}' feed.") + return + + filtered: list = [] + + downloaded: list[str] = archive_read(archive_file, [item["archive_id"] for item in items]) + + for item in items: + TwitchHandler.queued.add(item["archive_id"]) + + if item["archive_id"] in downloaded: + continue + + if queue.queue.exists(url=item["url"]): + continue + + try: + done: Download = queue.done.get(url=item["url"]) + if "error" != done.info.status: + continue + except KeyError: + pass + + if item["archive_id"] not in TwitchHandler.failure_count: + TwitchHandler.failure_count[item["archive_id"]] = 0 + + filtered.append(item) + + if len(filtered) < 1: + LOG.debug(f"No new items found in '{task.name}' feed.") + return + + LOG.info(f"Found '{len(filtered)}' new items from '{task.name}' feed.") + + rItem: Item = Item.format( + { + "url": feed_url, + "preset": task.preset, + "folder": task.folder if task.folder else "", + "template": task.template if task.template else "", + "cli": task.cli if task.cli else "", + "auto_start": task.auto_start, + "extras": {"source_task": task.id}, + } + ) + + try: + await asyncio.gather( + *[notify.emit(Events.ADD_URL, data=rItem.new_with(url=item["url"]).serialize()) for item in filtered] + ) + except Exception as e: + LOG.exception(e) + LOG.error(f"Error while adding items from '{task.name}'. {e!s}") + return + + @staticmethod + def parse(url: str) -> str | None: + """ + Parse twitch URL to extract the channel. + + Args: + url (str): The url to check. + + Returns: + str | None: The parsed ID if successful, None otherwise. + + """ + match: re.Match[str] | None = TwitchHandler.RX.match(url) + return match.group("id") if match else None + + @staticmethod + def tests() -> list[tuple[str, bool]]: + """ + Test cases for the URL parser. + + Returns: + list[tuple[str, bool]]: A list of tuples containing the URL and expected result. + + """ + return [ + ("https://www.twitch.tv/test_username", True), + ("https://twitch.tv/test_username", True), + ("http://m.twitch.tv/test_username,", True), + ("https://www.twitch.tv/test_username/", True), + ("https://twitch.tv/test_username/", True), + ("http://m.twitch.tv/test_username/,", True), + ("twitch.tv/test_username/", False), + ] diff --git a/app/library/task_handlers/youtube.py b/app/library/task_handlers/youtube.py index eae2b340..834bf83d 100644 --- a/app/library/task_handlers/youtube.py +++ b/app/library/task_handlers/youtube.py @@ -1,15 +1,16 @@ import asyncio import logging import re -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from xml.etree.ElementTree import Element -from app.library.config import Config from app.library.DownloadQueue import DownloadQueue from app.library.Events import EventBus, Events -from app.library.ItemDTO import Item, ItemDTO +from app.library.ItemDTO import Item from app.library.Tasks import Task -from app.library.Utils import archive_read +from app.library.Utils import archive_read, get_archive_id + +from ._base_handler import BaseHandler if TYPE_CHECKING: from xml.etree.ElementTree import Element @@ -18,22 +19,17 @@ if TYPE_CHECKING: LOG: logging.Logger = logging.getLogger(__name__) -EventBus.get_instance().subscribe( - Events.ITEM_ERROR, - lambda data, _, **__: YoutubeHandler.on_error(data.data), - f"{__name__}.on_error", -) - - -class YoutubeHandler: - queued: set[str] = set() - failure_count: dict[str, int] = {} +class YoutubeHandler(BaseHandler): FEED = "https://www.youtube.com/feeds/videos.xml?{type}={id}" - CHANNEL_REGEX = re.compile(r"^https?://(?:www\.)?youtube\.com/(?:channel/(?PUC[0-9A-Za-z_-]{22})|)/?$") + CHANNEL_REGEX: re.Pattern[str] = re.compile( + r"^https?://(?:www\.)?youtube\.com/(?:channel/(?PUC[0-9A-Za-z_-]{22})|)/?$" + ) - PLAYLIST_REGEX = re.compile(r"^https?://(?:www\.)?youtube\.com/(?:playlist\?list=(?P[A-Za-z0-9_-]+)|).*$") + PLAYLIST_REGEX: re.Pattern[str] = re.compile( + r"^https?://(?:www\.)?youtube\.com/(?:playlist\?list=(?P[A-Za-z0-9_-]+)|).*$" + ) @staticmethod def can_handle(task: Task) -> bool: @@ -45,7 +41,7 @@ class YoutubeHandler: return YoutubeHandler.parse(task.url) is not None @staticmethod - async def handle(task: Task, notify: EventBus, config: Config, queue: DownloadQueue): + async def handle(task: Task, notify: EventBus, queue: DownloadQueue): """ Fetch the Atom feed for a YouTube channel or playlist, parse entries, and return a list of videos with metadata. @@ -53,16 +49,9 @@ class YoutubeHandler: Args: task (Task): The task containing the YouTube URL. notify (EventBus): The event bus for notifications. - config (Config): The configuration instance. queue (DownloadQueue): The download queue instance. """ - params: dict = task.get_ytdlp_opts().get_all() - if not (archive_file := params.get("download_archive")): - LOG.error(f"Task '{task.name}' does not have an archive file.") - return - - import httpx from defusedxml.ElementTree import fromstring parsed: dict[str, str] | None = YoutubeHandler.parse(task.url) @@ -70,65 +59,49 @@ class YoutubeHandler: LOG.error(f"Cannot parse '{task.name}' URL: {task.url}") return + params: dict = task.get_ytdlp_opts().get_all() + feed_url: str = YoutubeHandler.FEED.format(type=parsed["type"], id=parsed["id"]) - LOG.debug(f"Fetching '{task.name}' feed.") - opts: dict[str, Any] = { - "proxy": params.get("proxy"), - "headers": { - "User-Agent": params.get( - "user_agent", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", - ) - }, - } - - try: - from httpx_curl_cffi import AsyncCurlTransport, CurlOpt - - opts["transport"] = AsyncCurlTransport( - impersonate="chrome", - default_headers=True, - curl_options={CurlOpt.FRESH_CONNECT: True}, - ) - opts.pop("headers", None) - except Exception: - pass items: list = [] has_items = False - async with httpx.AsyncClient(**opts) as client: - response: httpx.Response = await client.request(method="GET", url=feed_url, timeout=120) - response.raise_for_status() + response = await YoutubeHandler.request(url=feed_url, ytdlp_opts=params) + response.raise_for_status() - root: Element[str] = fromstring(response.text) - ns: dict[str, str] = { - "atom": "http://www.w3.org/2005/Atom", - "yt": "http://www.youtube.com/xml/schemas/2015", - } + root: Element[str] = fromstring(response.text) + ns: dict[str, str] = { + "atom": "http://www.w3.org/2005/Atom", + "yt": "http://www.youtube.com/xml/schemas/2015", + } - for entry in root.findall("atom:entry", ns): - vid_elem: Element[str] | None = entry.find("yt:videoId", ns) - vid: str | None = vid_elem.text if vid_elem is not None else "" - if not vid: - LOG.warning(f"Entry in '{task.name}' feed is missing a video ID. Skipping entry.") - continue + for entry in root.findall("atom:entry", ns): + vid_elem: Element[str] | None = entry.find("yt:videoId", ns) + vid: str | None = vid_elem.text if vid_elem is not None else "" + if not vid: + LOG.warning(f"Entry in '{task.name}' feed is missing a video ID. Skipping entry.") + continue - archive_id: str = f"youtube {vid}" - url: str = f"https://www.youtube.com/watch?v={vid}" + url: str = f"https://www.youtube.com/watch?v={vid}" - title_elem: Element[str] | None = entry.find("atom:title", ns) - title: str | None = title_elem.text if title_elem is not None else "" + id_dict: dict[str, str | None] = get_archive_id(url) + archive_id: str | None = id_dict.get("archive_id") + if not archive_id: + LOG.warning(f"Could not compute archive ID for video '{vid}' in '{task.name}' feed. Skipping entry.") + continue - pub_elem: Element[str] | None = entry.find("atom:published", ns) - published: str | None = pub_elem.text if pub_elem is not None else "" - has_items = True + title_elem: Element[str] | None = entry.find("atom:title", ns) + title: str | None = title_elem.text if title_elem is not None else "" - if archive_id in YoutubeHandler.queued: - continue + pub_elem: Element[str] | None = entry.find("atom:published", ns) + published: str | None = pub_elem.text if pub_elem is not None else "" + has_items = True - items.append({"id": vid, "url": url, "title": title, "published": published, "archive_id": archive_id}) + if archive_id in YoutubeHandler.queued: + continue + + items.append({"id": vid, "url": url, "title": title, "published": published, "archive_id": archive_id}) if len(items) < 1: if not has_items: @@ -139,7 +112,7 @@ class YoutubeHandler: filtered: list = [] - downloaded: list[str] = archive_read(archive_file, [item["archive_id"] for item in items]) + downloaded: list[str] = archive_read(params.get("download_archive"), [item["archive_id"] for item in items]) for item in items: YoutubeHandler.queued.add(item["archive_id"]) @@ -170,7 +143,7 @@ class YoutubeHandler: rItem: Item = Item.format( { "url": feed_url, - "preset": str(task.preset or config.default_preset), + "preset": task.preset, "folder": task.folder if task.folder else "", "template": task.template if task.template else "", "cli": task.cli if task.cli else "", @@ -210,31 +183,6 @@ class YoutubeHandler: return None - @staticmethod - async def on_error(item: ItemDTO) -> None: - """ - Handle errors by logging them and removing the queued ID if it exists. - - Args: - item (ItemDTO): The error data containing the URL and other information. - - """ - cls = YoutubeHandler - if not item or not isinstance(item, ItemDTO): - return - - if not item.archive_id or not cls.failure_count.get(item.archive_id, None): - LOG.debug(f"Item '{item.name()}' not queued by the handler.") - return - - failCount: int = int(cls.failure_count.get(item.archive_id, 0)) - - LOG.info(f"Removing '{item.name()}' from queued IDs due to error. Failure count: '{failCount + 1}'.") - if item.archive_id in cls.queued: - cls.queued.remove(item.archive_id) - - cls.failure_count[item.archive_id] = 1 + failCount - @staticmethod def tests() -> list[tuple[str, bool]]: """ diff --git a/ui/app/components/History.vue b/ui/app/components/History.vue index 129b6b90..b81367eb 100644 --- a/ui/app/components/History.vue +++ b/ui/app/components/History.vue @@ -13,7 +13,7 @@
-
+
-