diff --git a/.github/workflows/update-yt-dlp.yml b/.github/workflows/update-yt-dlp.yml index 2a145751..860bfd35 100644 --- a/.github/workflows/update-yt-dlp.yml +++ b/.github/workflows/update-yt-dlp.yml @@ -42,7 +42,7 @@ jobs: VER=$(uv pip list --outdated | awk '$1 == "yt-dlp" {print $3}') if [ -n "$VER" ]; then echo "YTLDLP_VER=${VER}" >> "$GITHUB_OUTPUT" - uv pip install --upgrade yt-dlp + uv pip install --upgrade yt-dlp[default] uv sync --upgrade UPDATED=true else diff --git a/FAQ.md b/FAQ.md index 42b56e70..48812cda 100644 --- a/FAQ.md +++ b/FAQ.md @@ -317,22 +317,13 @@ Then simply create a new preset, and in the `Command options for yt-dlp` field s ```bash --extractor-args "youtubepot-bgutilhttp:base_url=http://bgutil_provider:4416" ---extractor-args "youtube:player-client=default,tv,mweb;formats=incomplete" ``` -you and also enable the fallback by using the follow extractor args - -```bash ---extractor-args "youtubepot-bgutilhttp:base_url=http://bgutil_provider:4416;disable_innertube=1" ---extractor-args "youtube:player-client=default,tv,mweb;formats=incomplete" -``` - -Use this alternative extractor args in case the extractor fails to get the pot tokens from the bgutil provider server. For more information please visit [bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider) project. # Troubleshooting and submitting issues -Before asking a question or submitting an issue for YTPTube, please remember that YTPTube is only a thin wrapper for +Before asking a question or submitting an issue for YTPTube, please remember that YTPTube is only a wrapper for [yt-dlp](https://github.com/yt-dlp/yt-dlp). Any issues you might be experiencing with authentication to video websites, postprocessing, permissions, other `yt-dlp options` configurations which seem not to work, or anything else that concerns the workings of the underlying yt-dlp library, need not be opened on the YTPTube project. @@ -343,7 +334,8 @@ and once that is working, importing the options that worked for you into a new ` ## Via HTTP If you have enabled the web terminal via `YTP_CONSOLE_ENABLED` environment variable, simply go to `Other > Terminal` use - the yt-dlp command, the interface is jailed to the `yt-dlp` binary you can't access anything else. + the yt-dlp command, the interface is jailed to the `yt-dlp` binary you can't access anything else. Or from download form + by clicking `advanced options` button than the yellow terminal icon `Run directly in console`. ## Via CLI @@ -503,7 +495,7 @@ as we only test for GPU encoding once on first video stream. # Allowing internal URLs requests -By default, YTPTube prevents requests to internal resources, for security reasons. However, if you want to allow requests to internal URLs, you can set the `YTP_ALLOW_INTERNAL_URLS` environment variable to `true`. This will allow requests to internal URLs. +By default, YTPTube prevents requests to internal resources. However, if you want to allow requests to internal URLs, you can set the `YTP_ALLOW_INTERNAL_URLS` environment variable to `true`. This will allow requests to internal URLs. We do not recommend enabling this option unless you know what you are doing, as it can expose your internal network to potential security risks. This should only be used if it's truly needed. @@ -557,7 +549,6 @@ services: After making the changes, restart your container. This should resolve the "No space left on device" error during download. - # How to prevent loading screen during YouTube premieres? Depending on how you look at it, YTPTube live download implementation is rather great and fast. However, during YouTube @@ -566,7 +557,6 @@ playing. By default we wait for 5min + the duration of the video before starting the loading screen. However, you can override the behavior by setting the following environment variable: ```env -YTP_PREVENT_LIVE_PREMIERE=true YTP_LIVE_PREMIERE_BUFFER=10 ``` diff --git a/app/features/streaming/library/segments.py b/app/features/streaming/library/segments.py index 62c1890f..5789ed01 100644 --- a/app/features/streaming/library/segments.py +++ b/app/features/streaming/library/segments.py @@ -94,7 +94,7 @@ class Segments: } caps: dict[str, dict[str, bool]] = detect_qsv_capabilities() - base_codec: str = s_codec.split("_")[0] + base_codec: str = s_codec.split("_", maxsplit=1)[0] codec_caps: dict[str, bool] = caps.get(base_codec, {"full": False, "lp": False}) ctx["qsv"] = codec_caps diff --git a/app/features/streaming/router.py b/app/features/streaming/router.py index 687d8e5c..84b4970a 100644 --- a/app/features/streaming/router.py +++ b/app/features/streaming/router.py @@ -213,7 +213,7 @@ async def segments_stream(request: Request, config: Config, app: web.Application await Segments( download_path=config.download_path, index=int(segment), - duration=float(f"{float(sd if sd else M3u8.duration):.6f}"), + duration=float(f"{float(sd or M3u8.duration):.6f}"), vconvert=vc == 1, aconvert=ac == 1, ).stream(realFile, resp) diff --git a/app/features/tasks/definitions/service.py b/app/features/tasks/definitions/service.py index 4fa245fb..eca88441 100644 --- a/app/features/tasks/definitions/service.py +++ b/app/features/tasks/definitions/service.py @@ -397,7 +397,7 @@ class TaskHandle: return TaskFailure( message=extraction.message, - error=extraction.error if extraction.error else extraction.message, + error=extraction.error or extraction.message, metadata=combined_failure_metadata, ) diff --git a/app/features/ytdlp/extractor.py b/app/features/ytdlp/extractor.py index 74295500..5fed3b31 100644 --- a/app/features/ytdlp/extractor.py +++ b/app/features/ytdlp/extractor.py @@ -235,7 +235,7 @@ def extract_info_sync( archive_id: str | None = f".{id_dict['id']}" if id_dict.get("id") else None log_wrapper.add_target( - target=logging.getLogger(f"yt-dlp{archive_id if archive_id else '.extract_info'}"), + target=logging.getLogger(f"yt-dlp{archive_id or '.extract_info'}"), level=logging.DEBUG if debug else logging.WARNING, ) diff --git a/app/features/ytdlp/router.py b/app/features/ytdlp/router.py index 9d09f3c8..1054e3fb 100644 --- a/app/features/ytdlp/router.py +++ b/app/features/ytdlp/router.py @@ -394,7 +394,7 @@ async def get_info(request: Request, cache: Cache, config: Config) -> Response: data["is_archived"] = False archive_file: str | None = ytdlp_opts.get("download_archive") - data["archive_file"] = archive_file if archive_file else None + data["archive_file"] = archive_file or None if archive_file and (archive_id := get_archive_id(url=url).get("archive_id")): data["archive_id"] = archive_id diff --git a/app/features/ytdlp/ytdlp_opts.py b/app/features/ytdlp/ytdlp_opts.py index d30028a0..4b8340eb 100644 --- a/app/features/ytdlp/ytdlp_opts.py +++ b/app/features/ytdlp/ytdlp_opts.py @@ -130,7 +130,7 @@ class YTDLPCli: raise ValueError(msg) self.item = item - preset_name = item.preset if item.preset else self._config.default_preset + preset_name = item.preset or self._config.default_preset self.preset: Preset | None = YTDLPCli._get_presets().get(preset_name) self._config: Config = config or Config.get_instance() diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py index d5b2cfa4..f4ef3d85 100644 --- a/app/library/ItemDTO.py +++ b/app/library/ItemDTO.py @@ -227,7 +227,7 @@ class Item: """ from app.features.presets.service import Presets - return Presets.get_instance().get(self.preset if self.preset else self._default_preset()) + return Presets.get_instance().get(self.preset or self._default_preset()) def get_archive_id(self) -> str | None: """ @@ -553,7 +553,7 @@ class ItemDTO: """ from app.features.presets.service import Presets - return Presets.get_instance().get(self.preset if self.preset else "default") + return Presets.get_instance().get(self.preset or "default") def archive_status(self, force: bool = False) -> None: """ diff --git a/app/library/PackageInstaller.py b/app/library/PackageInstaller.py index b2ed929e..30c27bf0 100644 --- a/app/library/PackageInstaller.py +++ b/app/library/PackageInstaller.py @@ -113,15 +113,16 @@ class PackageInstaller: if "yt_dlp" == pkg: pkg = "yt-dlp" + pkg_name: str = "yt-dlp[default]" if pkg == "yt-dlp" else pkg if version: - if "nightly" == version and pkg == "yt-dlp": - cmd.extend(["--pre", "yt-dlp[default]"]) - elif "master" == version and pkg == "yt-dlp": + if "nightly" == version and "yt-dlp" == pkg: + cmd.extend(["--pre", pkg_name]) + elif "master" == version and "yt-dlp" == pkg: cmd.append("git+https://github.com/yt-dlp/yt-dlp.git@master") else: - cmd.append(version if str(version).startswith("git+") else f"{pkg}=={version}") + cmd.append(version if str(version).startswith("git+") else f"{pkg_name}=={version}") else: - cmd.extend(["--disable-pip-version-check", pkg]) + cmd.extend(["--disable-pip-version-check", pkg_name]) try: proc: subprocess.CompletedProcess[bytes] = subprocess.run( diff --git a/app/library/UpdateChecker.py b/app/library/UpdateChecker.py index 4d6eae2d..db430982 100644 --- a/app/library/UpdateChecker.py +++ b/app/library/UpdateChecker.py @@ -131,8 +131,8 @@ class UpdateChecker(metaclass=Singleton): if app_cached and ytdlp_cached: return (app_cached, ytdlp_cached) - app_result = app_cached if app_cached else await self._check_app_version() - ytdlp_result = ytdlp_cached if ytdlp_cached else await self._check_ytdlp_version() + app_result = app_cached or await self._check_app_version() + ytdlp_result = ytdlp_cached or await self._check_ytdlp_version() return (app_result, ytdlp_result) diff --git a/app/library/config.py b/app/library/config.py index 3074e410..b153bcf7 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -201,7 +201,7 @@ class Config(metaclass=Singleton): is_native: bool = False "Is the application running in natively." - prevent_live_premiere: bool = False + prevent_live_premiere: bool = True """Prevent downloading of the initial premiere live broadcast.""" live_premiere_buffer: int = 5 diff --git a/app/library/downloads/core.py b/app/library/downloads/core.py index ef14b6a7..2732c814 100644 --- a/app/library/downloads/core.py +++ b/app/library/downloads/core.py @@ -62,10 +62,10 @@ class Download: self.max_workers = int(config.max_workers) self.is_live: bool = bool(info.is_live) or info.live_in is not None self.info_dict: dict | None = info_dict - self.logger: logging.Logger = logging.getLogger(f"Download.{info.id if info.id else info._id}") + self.logger: logging.Logger = logging.getLogger(f"Download.{info.id or info._id}") self.started_time = 0 self.queue_time: datetime = datetime.now(tz=UTC) - self.logs: list[str] = logs if logs else [] + self.logs: list[str] = logs or [] self._process_manager = ProcessManager( download_id=self.id, @@ -131,10 +131,7 @@ class Download: if self.info.cookies: try: - cookie_file = ( - Path(self._temp_manager.temp_path if self._temp_manager.temp_path else self.temp_dir) - / f"cookie_{self.info._id}.txt" - ) + cookie_file = Path(self._temp_manager.temp_path or self.temp_dir) / f"cookie_{self.info._id}.txt" self.logger.debug( f"Creating cookie file for '{self.info.id}: {self.info.title}' - '{cookie_file}'." ) @@ -372,7 +369,7 @@ class Download: self.logger.debug(f"Download task '{self.info.title}: {self.info.id}' started for '{elapsed}' seconds.") return False - status: str = self.info.status if self.info.status else "unknown" + status: str = self.info.status or "unknown" is_stale = is_download_stale(self.started_time, status, self.running(), self.info.auto_start, min_elapsed=300) if is_stale: diff --git a/app/library/downloads/status_tracker.py b/app/library/downloads/status_tracker.py index 0a403ba1..62e4d083 100644 --- a/app/library/downloads/status_tracker.py +++ b/app/library/downloads/status_tracker.py @@ -185,8 +185,7 @@ class StatusTracker: if next_status is None or isinstance(next_status, Terminator): continue await self.process_status_update(next_status) - except Exception as e: - self.logger.warning(f"Error processing status update during drain: {e}") + except Exception: # noqa: S112 continue def cancel_update_task(self) -> None: diff --git a/app/library/downloads/video_processor.py b/app/library/downloads/video_processor.py index f0329065..b01d538e 100644 --- a/app/library/downloads/video_processor.py +++ b/app/library/downloads/video_processor.py @@ -112,12 +112,12 @@ async def add_video(queue: "DownloadQueue", entry: dict, item: "Item", logs: lis download_dir=download_dir, temp_dir=queue.config.temp_path, cookies=item.cookies, - template=item.template if item.template else queue.config.output_template, + template=item.template or queue.config.output_template, template_chapter=queue.config.output_template_chapter, datetime=formatdate(time.time()), error=error, is_live=is_live, - live_in=live_in if live_in else item.extras.get("live_in", None), + live_in=live_in or item.extras.get("live_in", None), options=options, cli=item.cli, auto_start=item.auto_start, diff --git a/app/library/migrate.py b/app/library/migrate.py index 17cda134..decc9c3a 100644 --- a/app/library/migrate.py +++ b/app/library/migrate.py @@ -133,7 +133,7 @@ class Database: else: self.conn: AsyncConnection = db_url - async def __aenter__(self) -> "Database": + async def __aenter__(self): if self._owns_connection and self.conn is None: # Create connection from string URL from sqlalchemy.ext.asyncio import create_async_engine @@ -284,7 +284,7 @@ def create_migration(name: str, directory: str | None = None) -> str: Create a migration with the given name. If no directory is specified, the current working directory will be used. """ - directory = directory if directory else "." + directory = directory or "." if not os.path.exists(directory) or not os.path.isdir(directory): msg: str = f"{directory} is not a directory." raise Error(msg) diff --git a/app/library/router.py b/app/library/router.py index 22e356b9..c9cee5b4 100644 --- a/app/library/router.py +++ b/app/library/router.py @@ -83,7 +83,7 @@ def route( return await func(*args, **kwargs) for m in methods: - route_name = name if name else make_route_name(m, path) + route_name = name or make_route_name(m, path) route_type: str = RouteType.SOCKET if RouteType.SOCKET == m else RouteType.HTTP if route_type not in ROUTES: @@ -124,7 +124,7 @@ def add_route( methods = [method] if isinstance(method, (str, RouteType)) else method for m in methods: - route_name = name if name else make_route_name(m, path) + route_name = name or make_route_name(m, path) route_type: str = RouteType.SOCKET if RouteType.SOCKET == m else RouteType.HTTP if route_type not in ROUTES: diff --git a/app/library/sqlite_store.py b/app/library/sqlite_store.py index f56f09c3..47ad0d90 100644 --- a/app/library/sqlite_store.py +++ b/app/library/sqlite_store.py @@ -75,7 +75,7 @@ class SqliteStore(metaclass=ThreadSafe): self._flush_interval: float = flush_interval self._max_pending: int = max_pending - async def __aenter__(self) -> "SqliteStore": + async def __aenter__(self): await self.get_connection() return self diff --git a/app/routes/api/history.py b/app/routes/api/history.py index 44337f38..056e9db3 100644 --- a/app/routes/api/history.py +++ b/app/routes/api/history.py @@ -425,14 +425,12 @@ async def items_add(request: Request, queue: DownloadQueue, encoder: Encoder) -> if not item.extras: item.extras = {} - item.extras["batch_id"] = batch_id - item.extras["batch_index"] = idx - item.extras["batch_total"] = len(items) + if len(items) > 1: + item.extras["batch_id"] = batch_id + item.extras["batch_index"] = idx + item.extras["batch_total"] = len(items) - task = asyncio.create_task( - queue.add(item=item), - name=f"bulk_add_{batch_id}_{idx}", - ) + task = asyncio.create_task(queue.add(item=item), name=f"bulk_add_{batch_id}_{idx}") task.add_done_callback(lambda t: handle_task_exception(t, LOG)) return web.json_response( diff --git a/app/routes/api/system.py b/app/routes/api/system.py index 2c4345f4..e1744a4e 100644 --- a/app/routes/api/system.py +++ b/app/routes/api/system.py @@ -199,12 +199,12 @@ async def check_updates(config: Config, encoder: Encoder, update_checker: Update "app": { "status": "update_available" if config.new_version else "up_to_date", "current_version": config.app_version, - "new_version": config.new_version if config.new_version else None, + "new_version": config.new_version or None, }, "ytdlp": { "status": "update_available" if config.yt_new_version else "up_to_date", "current_version": config._ytdlp_version(), - "new_version": config.yt_new_version if config.yt_new_version else None, + "new_version": config.yt_new_version or None, }, }, status=web.HTTPOk.status_code, diff --git a/app/tests/test_update_checker.py b/app/tests/test_update_checker.py index 33cf3c79..a390ec3a 100644 --- a/app/tests/test_update_checker.py +++ b/app/tests/test_update_checker.py @@ -170,171 +170,7 @@ class TestUpdateChecker: mock_ytdlp_response = MagicMock() mock_ytdlp_response.status_code = 200 - mock_ytdlp_response.json.return_value = {"tag_name": "9999.12.31"} - - mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response]) - mock_http = MagicMock() - mock_http.get = mock_get - mock_client.return_value = mock_http - - with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"): - checker = UpdateChecker.get_instance(config=config) - (app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates() - - assert "update_available" == app_status, "Should return update_available status for app" - assert "v99.0.0" == app_version, "Should return new app version tag" - assert "v99.0.0" == config.new_version, "Should store new app version tag" - assert "update_available" == ytdlp_status, "Should return update_available status for yt-dlp" - assert "9999.12.31" == ytdlp_version, "Should return new yt-dlp version tag" - assert "9999.12.31" == config.yt_new_version, "Should store new yt-dlp version tag" - assert checker._job_id is None, "Should stop scheduled task after finding app update" - - @pytest.mark.asyncio - @patch("app.library.UpdateChecker.get_async_client") - async def test_check_for_updates_no_update_available(self, mock_client): - """Test that check_for_updates correctly handles when no update is available.""" - from app.library.config import Config - from app.library.UpdateChecker import UpdateChecker - - config = Config.get_instance() - config.check_for_updates = True - config.new_version = "" - config.yt_new_version = "" - - mock_app_response = MagicMock() - mock_app_response.status_code = 200 - mock_app_response.json.return_value = {"tag_name": "v1.0.0"} - - mock_ytdlp_response = MagicMock() - mock_ytdlp_response.status_code = 200 - mock_ytdlp_response.json.return_value = {"tag_name": "2020.01.01"} - - mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response]) - mock_http = MagicMock() - mock_http.get = mock_get - mock_client.return_value = mock_http - - checker = UpdateChecker.get_instance(config=config) - checker._job_id = "test-job" - - (app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates() - - assert "up_to_date" == app_status, "Should return up_to_date status for app" - assert app_version is None, "Should return None for app new_version" - assert "" == config.new_version, "Should clear new_version when no app update available" - assert "up_to_date" == ytdlp_status, "Should return up_to_date status for yt-dlp" - assert ytdlp_version is None, "Should return None for yt-dlp new_version" - assert "" == config.yt_new_version, "Should clear yt_new_version when no yt-dlp update available" - assert "test-job" == checker._job_id, "Should keep scheduled task running" - - @pytest.mark.asyncio - @patch("app.library.UpdateChecker.get_async_client") - async def test_check_for_updates_handles_http_error(self, mock_client): - """Test that check_for_updates handles HTTP errors gracefully.""" - from app.library.config import Config - from app.library.UpdateChecker import UpdateChecker - - config = Config.get_instance() - config.check_for_updates = True - config.new_version = "" - config.yt_new_version = "" - - mock_response = MagicMock() - mock_response.status_code = 404 - - mock_http = MagicMock() - mock_http.get = AsyncMock(return_value=mock_response) - mock_client.return_value = mock_http - - checker = UpdateChecker.get_instance(config=config) - - (app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates() - - assert "error" == app_status, "Should return error status for app" - assert app_version is None, "Should return None for app new_version on error" - assert "" == config.new_version, "Should not set new_version on HTTP error" - assert "error" == ytdlp_status, "Should return error status for yt-dlp" - assert ytdlp_version is None, "Should return None for yt-dlp new_version on error" - assert "" == config.yt_new_version, "Should not set yt_new_version on HTTP error" - - @pytest.mark.asyncio - @patch("app.library.UpdateChecker.get_async_client") - async def test_check_for_updates_handles_exception(self, mock_client): - """Test that check_for_updates handles exceptions gracefully.""" - from app.library.config import Config - from app.library.UpdateChecker import UpdateChecker - - config = Config.get_instance() - config.check_for_updates = True - config.new_version = "" - config.yt_new_version = "" - - mock_http = MagicMock() - mock_http.get = AsyncMock(side_effect=Exception("Network error")) - mock_client.return_value = mock_http - - checker = UpdateChecker.get_instance(config=config) - - (app_status, app_version), (ytdlp_status, ytdlp_version) = await checker.check_for_updates() - - assert "error" == app_status, "Should return error status on exception for app" - assert app_version is None, "Should return None for app new_version on exception" - assert "" == config.new_version, "Should not set new_version on exception" - assert "error" == ytdlp_status, "Should return error status on exception for yt-dlp" - assert ytdlp_version is None, "Should return None for yt-dlp new_version on exception" - assert "" == config.yt_new_version, "Should not set yt_new_version on exception" - - def test_compare_versions_newer_available(self): - """Test version comparison detects newer version.""" - from app.library.UpdateChecker import UpdateChecker - - checker = UpdateChecker.get_instance() - - assert checker._compare_versions("1.0.0", "2.0.0") is True, "Should detect 2.0.0 > 1.0.0" - assert checker._compare_versions("1.0.0", "1.1.0") is True, "Should detect 1.1.0 > 1.0.0" - assert checker._compare_versions("1.0.0", "1.0.1") is True, "Should detect 1.0.1 > 1.0.0" - - def test_compare_versions_same_version(self): - """Test version comparison with same version.""" - from app.library.UpdateChecker import UpdateChecker - - checker = UpdateChecker.get_instance() - - assert checker._compare_versions("1.0.0", "1.0.0") is False, "Should detect versions are equal" - - def test_compare_versions_older_version(self): - """Test version comparison with older version.""" - from app.library.UpdateChecker import UpdateChecker - - checker = UpdateChecker.get_instance() - - assert checker._compare_versions("2.0.0", "1.0.0") is False, "Should detect 1.0.0 is not newer than 2.0.0" - - def test_github_api_url_constant(self): - """Test that GitHub API URL is correctly defined.""" - from app.library.UpdateChecker import UpdateChecker - - expected_url = "https://api.github.com/repos/arabcoders/ytptube/releases/latest" - assert UpdateChecker.GITHUB_API_URL == expected_url, "GitHub API URL should be correct" - - @pytest.mark.asyncio - async def test_check_for_updates_stores_tag_name(self): - """Test that check_for_updates stores the tag_name when update found.""" - from app.library.config import Config - from app.library.UpdateChecker import UpdateChecker - - config = Config.get_instance() - config.check_for_updates = True - config.new_version = "" - config.yt_new_version = "" - - mock_app_response = MagicMock() - mock_app_response.status_code = 200 - mock_app_response.json.return_value = {"tag_name": "v2.0.0"} - - mock_ytdlp_response = MagicMock() - mock_ytdlp_response.status_code = 200 - mock_ytdlp_response.json.return_value = {"tag_name": "2026.01.01"} + mock_ytdlp_response.json.return_value = {"tag_name": "2026.12.31"} mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response]) mock_http = MagicMock() @@ -342,11 +178,12 @@ class TestUpdateChecker: with patch("app.library.UpdateChecker.get_async_client", return_value=mock_http): with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"): - checker = UpdateChecker.get_instance(config=config) - await checker.check_for_updates() + with patch("app.library.config.Config._ytdlp_version", return_value="2026.01.01"): + checker = UpdateChecker.get_instance(config=config) + await checker.check_for_updates() - assert "v2.0.0" == config.new_version, "Should store full tag_name including 'v' prefix" - assert "2026.01.01" == config.yt_new_version, "Should store yt-dlp tag_name" + assert "v99.0.0" == config.new_version, "Should store full tag_name including 'v' prefix" + assert "2026.12.31" == config.yt_new_version, "Should store yt-dlp tag_name" def test_subscribe_to_started_event(self): """Test that attach subscribes to Events.STARTED.""" @@ -401,8 +238,9 @@ class TestUpdateChecker: mock_http.get = AsyncMock(return_value=mock_response) mock_client.return_value = mock_http - checker = UpdateChecker.get_instance(config=config) - status, new_version = await checker._check_ytdlp_version() + with patch("app.library.config.Config._ytdlp_version", return_value="2024.01.01"): + checker = UpdateChecker.get_instance(config=config) + status, new_version = await checker._check_ytdlp_version() assert "update_available" == status, "Should return update_available status for yt-dlp" assert "9999.12.31" == new_version, "Should return new yt-dlp version tag" @@ -427,8 +265,9 @@ class TestUpdateChecker: mock_http.get = AsyncMock(return_value=mock_response) mock_client.return_value = mock_http - checker = UpdateChecker.get_instance(config=config) - status, new_version = await checker._check_ytdlp_version() + with patch("app.library.config.Config._ytdlp_version", return_value="2025.01.01"): + checker = UpdateChecker.get_instance(config=config) + status, new_version = await checker._check_ytdlp_version() assert "up_to_date" == status, "Should return up_to_date status for yt-dlp" assert new_version is None, "Should return None for yt-dlp new_version" @@ -477,7 +316,7 @@ class TestUpdateChecker: mock_ytdlp_response = MagicMock() mock_ytdlp_response.status_code = 200 - mock_ytdlp_response.json.return_value = {"tag_name": "2026.01.01"} + mock_ytdlp_response.json.return_value = {"tag_name": "2026.12.31"} mock_get = AsyncMock(side_effect=[mock_app_response, mock_ytdlp_response]) mock_http = MagicMock() @@ -485,8 +324,9 @@ class TestUpdateChecker: mock_client.return_value = mock_http with patch("app.library.UpdateChecker.APP_VERSION", "v1.0.0"): - checker = UpdateChecker.get_instance(config=config) - (app_status1, app_version1), (ytdlp_status1, ytdlp_version1) = await checker.check_for_updates() + with patch("app.library.config.Config._ytdlp_version", return_value="2026.01.01"): + checker = UpdateChecker.get_instance(config=config) + (app_status1, app_version1), (ytdlp_status1, ytdlp_version1) = await checker.check_for_updates() assert "update_available" == app_status1, "Should find app update on first call" assert "update_available" == ytdlp_status1, "Should find yt-dlp update on first call" diff --git a/app/yt_dlp_plugins/postprocessor/nfo_maker.py b/app/yt_dlp_plugins/postprocessor/nfo_maker.py index dfc78b0d..85d75708 100644 --- a/app/yt_dlp_plugins/postprocessor/nfo_maker.py +++ b/app/yt_dlp_plugins/postprocessor/nfo_maker.py @@ -157,7 +157,7 @@ class NFOMakerPP(PostProcessor): try: first_date: str = next((str(nfo_data[k]) for k in self._DATE_FIELDS if nfo_data.get(k)), "") if first_date: - nfo_data["year"] = first_date.split("-")[0] + nfo_data["year"] = first_date.split("-", maxsplit=1)[0] except Exception as e: self.report_warning(f"Error extracting year from date: {e}") diff --git a/pyproject.toml b/pyproject.toml index 398d8c23..1a78fbdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,13 @@ ignore = [ "PT011", "RUF001", "S311", - "TC001" + "TC001", + "ASYNC240", + # will be fixed in the future + "UP042", + "FURB171", + "FURB110", + "PLW0108", ] fixable = ["ALL"] @@ -186,7 +192,7 @@ pythonpath = ["."] testpaths = ["app/tests", "app/features"] addopts = "-v --tb=short" filterwarnings = [ - "ignore:Parsing dates involving a day of month without a year:DeprecationWarning" + "ignore:Parsing dates involving a day of month without a year:DeprecationWarning", ] [dependency-groups] @@ -195,4 +201,5 @@ dev = [ "pytest>=8.4.2", "pytest-asyncio>=1.1.0", "ruff>=0.13.0", + "pip>=26.0.0", ] diff --git a/ui/app/components/History.vue b/ui/app/components/History.vue index 4b258a02..dc4e5883 100644 --- a/ui/app/components/History.vue +++ b/ui/app/components/History.vue @@ -474,7 +474,7 @@ @@ -499,10 +499,11 @@ import { useStorage, useIntersectionObserver } from '@vueuse/core' import type { StoreItem } from '~/types/store' import { useConfirm } from '~/composables/useConfirm' import { deepIncludes, getPath, getImage } from '~/utils' +import type { item_request } from '~/types/item' const emitter = defineEmits<{ (e: 'getInfo', url: string, preset: string, cli: string): void - (e: 'add_new', item: Partial): void + (e: 'add_new', item: item_request): void (e: 'getItemInfo', id: string): void (e: 'clear_search'): void }>() @@ -850,7 +851,7 @@ const removeItem = async (item: StoreItem) => { } const retryItem = async (item: StoreItem, re_add: boolean = false, remove_file: boolean = false) => { - const item_req: Partial = { + const item_req: item_request = { url: item.url, preset: item.preset, folder: item.folder, diff --git a/ui/app/components/Queue.vue b/ui/app/components/Queue.vue index 74f3c5f8..57db52c9 100644 --- a/ui/app/components/Queue.vue +++ b/ui/app/components/Queue.vue @@ -289,7 +289,7 @@ Pause -
+