diff --git a/mediahive/__main__.py b/mediahive/__main__.py index 77915fe..88b5e8c 100644 --- a/mediahive/__main__.py +++ b/mediahive/__main__.py @@ -1,3 +1,5 @@ +"""MediaHive CLI entrypoint.""" + import argparse import asyncio import json diff --git a/mediahive/hivescan/__main__.py b/mediahive/hivescan/__main__.py index b841a46..2dd0d1b 100644 --- a/mediahive/hivescan/__main__.py +++ b/mediahive/hivescan/__main__.py @@ -1,3 +1,5 @@ +"""Hivescan CLI entrypoint.""" + import argparse import asyncio import json diff --git a/mediahive/hivescan/indexer.py b/mediahive/hivescan/indexer.py index 222032e..4fe4df6 100644 --- a/mediahive/hivescan/indexer.py +++ b/mediahive/hivescan/indexer.py @@ -383,7 +383,9 @@ async def _process_movies( ) -> AsyncIterator[tuple[Movie, tuple[str, Path, str] | None]]: """Async generator that processes all movies. - Yields (Movie, showreel_task_or_None) for each movie as it is processed. + Yields: + Tuples of ``(Movie, showreel_task_or_None)`` as each movie is processed. + """ # In-memory cache for TMDb lookups movie_tmdb_cache: dict[str, Info | None] = {} @@ -555,7 +557,7 @@ async def _process_movies( yield movie, showreel_task # Process movies without TMDb info - for key, group_data in no_tmdb_movie_groups.items(): + for group_data in no_tmdb_movie_groups.values(): items = group_data["items"] title = group_data["title"] year = group_data["year"] @@ -637,7 +639,9 @@ async def _process_series( ) -> AsyncIterator[tuple[Series, list[tuple[str, Path, int, int, str]]]]: """Async generator that processes all series. - Yields (Series, episode_reel_tasks) for each series as it is processed. + Yields: + Tuples of ``(Series, episode_reel_tasks)`` as each series is processed. + """ # In-memory cache for TMDb lookups series_tmdb_cache: dict[str, Info | None] = {} @@ -787,7 +791,7 @@ async def _process_series( yield series, ep_reel_tasks # Process series without TMDb info - for key, group_data in no_tmdb_groups.items(): + for group_data in no_tmdb_groups.values(): items = group_data["items"] title = group_data["title"] content_hash = hashlib.md5(f"series:{title}".encode()).hexdigest()[:12] diff --git a/mediahive/hivescan/scanner.py b/mediahive/hivescan/scanner.py index 0dda546..79db2ca 100644 --- a/mediahive/hivescan/scanner.py +++ b/mediahive/hivescan/scanner.py @@ -162,8 +162,8 @@ class RootScanner: ) ) - MEDIA_CONTAINER_DIRS = {"BDMV", "VIDEO_TS", "HVDVD_TS"} - VIDEO_EXTENSIONS = { + media_container_dirs = {"BDMV", "VIDEO_TS", "HVDVD_TS"} + video_extensions = { ".mkv", ".mp4", ".avi", @@ -196,7 +196,7 @@ class RootScanner: continue if await AsyncPath(item).is_dir(): - if item.name.upper() in MEDIA_CONTAINER_DIRS: + if item.name.upper() in media_container_dirs: is_media_container = True child_dirs.append(item) else: @@ -232,7 +232,7 @@ class RootScanner: await _walk(child) await asyncio.sleep(0) for child_file in child_files: - if child_file.suffix.lower() in VIDEO_EXTENSIONS: + if child_file.suffix.lower() in video_extensions: relpath = make_relative_path(str(child_file), media_root_str) try: stat_info = await AsyncPath(child_file).stat() @@ -298,6 +298,7 @@ class RootScanner: async def _run_scan(self) -> None: """Full scan pipeline: + 1. Discover downloads 2. Categorise → movies / series 3. Iterate async generators, send each item as Upsert diff --git a/mediahive/hivescan/tmdb_client.py b/mediahive/hivescan/tmdb_client.py index c31a933..9deb8af 100644 --- a/mediahive/hivescan/tmdb_client.py +++ b/mediahive/hivescan/tmdb_client.py @@ -411,8 +411,8 @@ async def fetch_movie_info(title: str, year: int | None = None) -> Info | None: alternative_titles = sorted(alt_titles_set) if alt_titles_set else None # Extract full cast - credits = details.get("credits", {}) - cast_data = credits.get("cast", []) + credits_data = details.get("credits", {}) + cast_data = credits_data.get("cast", []) cast = [ CastMember( name=c["name"], @@ -424,7 +424,7 @@ async def fetch_movie_info(title: str, year: int | None = None) -> Info | None: ] # Extract director from crew - crew = credits.get("crew", []) + crew = credits_data.get("crew", []) directors = [c["name"] for c in crew if c.get("job") == "Director"] director = directors[0] if directors else None @@ -512,8 +512,8 @@ async def fetch_series_info(title: str) -> Info | None: keywords = [k["name"] for k in keywords_data] # Extract full cast - credits = details.get("credits", {}) - cast_data = credits.get("cast", []) + credits_data = details.get("credits", {}) + cast_data = credits_data.get("cast", []) cast = [ CastMember( name=c["name"], diff --git a/mediahive/index_store.py b/mediahive/index_store.py index fb9c99d..6494613 100644 --- a/mediahive/index_store.py +++ b/mediahive/index_store.py @@ -96,7 +96,7 @@ class IndexStore: logger.exception("Failed to load snapshot from %s", self.snapshot_path) def _load_snapshot_sync(self, raw: bytes) -> None: - """Synchronous snapshot parsing (runs in thread pool).""" + """Parse snapshot bytes in a thread-pool context.""" data = msgspec.json.decode(raw, type=IndexSnapshot) for m in data.movies: if m.showreel_source_sets: diff --git a/mediahive/models/data.py b/mediahive/models/data.py index e3a8cb3..4e6536a 100644 --- a/mediahive/models/data.py +++ b/mediahive/models/data.py @@ -116,7 +116,8 @@ class IndexSnapshot(msgspec.Struct): movies: list[Movie] = [] series: list[Series] = [] - def __post_init__(self): + def __post_init__(self) -> None: + """Populate default stats when omitted from decoded payload.""" if self.stats is msgspec.UNSET: self.stats = MediaStats() diff --git a/mediahive/root_registry.py b/mediahive/root_registry.py index 7a55d56..dc232f1 100644 --- a/mediahive/root_registry.py +++ b/mediahive/root_registry.py @@ -144,7 +144,7 @@ class RootContext: logger.exception("Error flushing snapshot for root %s", self.root_id) async def send_event(self, event: ScanEvent) -> None: - """Called by the scanner to push an event into this root's queue.""" + """Push a scanner event into this root's queue.""" await self._events.put(event) async def _consume_events(self) -> None: diff --git a/mediahive/winmain.py b/mediahive/winmain.py index 71b63a1..d49bdd2 100644 --- a/mediahive/winmain.py +++ b/mediahive/winmain.py @@ -578,8 +578,7 @@ def _start_gamepad_remote( def _setup_logging() -> Path: - """Redirect stdout/stderr and configure logging to a file in - %APPDATA%/mediahive/. + """Redirect stdout/stderr and configure logging to a file in %APPDATA%/mediahive/. In a PyInstaller --windowed build there is no console, so any print() or unhandled exception traceback would be lost. This ensures everything ends diff --git a/rtorrent_client.py b/rtorrent_client.py index 4deab00..0380c29 100644 --- a/rtorrent_client.py +++ b/rtorrent_client.py @@ -13,7 +13,7 @@ class SCGITransport(xmlrpc.client.Transport): super().__init__() self.socket_path = socket_path - def single_request(self, host, handler, request_body, verbose=False): + def single_request(self, _host, _handler, request_body, _verbose=False): # Create SCGI request headers = f"CONTENT_LENGTH\x00{len(request_body)}\x00SCGI\x001\x00" request = f"{len(headers)}:{headers},{request_body.decode('utf-8')}" @@ -68,6 +68,7 @@ class RTorrentClient: def load_torrent(self, torrent_path: Path, download_dir: Path) -> bool: """Load a torrent file and set its download directory. + Uses load.start_verbose to load and immediately start/hash-check. Args: diff --git a/scripts/devserver.py b/scripts/devserver.py index 85c74fd..3afd2da 100644 --- a/scripts/devserver.py +++ b/scripts/devserver.py @@ -11,7 +11,7 @@ from pathlib import Path # Import util.py from scripts/fastapi-vue (not a package, so we adjust sys.path) sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue"))) -from devutil import ( # type: ignore +from devutil import ( # type: ignore[import-not-found] ProcessGroup, check_ports_free, logger, diff --git a/scripts/fastapi-vue/build-frontend.py b/scripts/fastapi-vue/build-frontend.py index 46036db..2f3d60f 100644 --- a/scripts/fastapi-vue/build-frontend.py +++ b/scripts/fastapi-vue/build-frontend.py @@ -3,7 +3,9 @@ import sys from pathlib import Path -from hatchling.builders.hooks.plugin.interface import BuildHookInterface # type: ignore +from hatchling.builders.hooks.plugin.interface import ( # type: ignore[import-not-found] + BuildHookInterface, +) sys.path.insert(0, str(Path(__file__).parent)) from buildutil import build diff --git a/scripts/fastapi-vue/devutil.py b/scripts/fastapi-vue/devutil.py index ac02bd0..c4b05b7 100644 --- a/scripts/fastapi-vue/devutil.py +++ b/scripts/fastapi-vue/devutil.py @@ -9,7 +9,7 @@ import sys from collections.abc import Coroutine from contextlib import suppress from pathlib import Path -from typing import Any +from typing import Any, Self import httpx from buildutil import find_dev_tool, find_install_tool, logger @@ -58,10 +58,15 @@ class ProcessGroup: logger.warning("%s failed with exit status %d", e.cmd, e.returncode) raise SystemExit(1) from None - async def __aenter__(self): + async def __aenter__(self) -> Self: + """Return this process group context manager.""" return self - async def __aexit__(self, exc_type, *_): + async def __aexit__( + self, + exc_type: type[BaseException] | None, + *_: object, + ) -> None: """Wait for one process to exit, terminate others, then wait for all.""" await self._cleanup(immediate=exc_type is not None) diff --git a/scripts/guibuild.py b/scripts/guibuild.py index 9934157..9de9451 100644 --- a/scripts/guibuild.py +++ b/scripts/guibuild.py @@ -74,8 +74,8 @@ def fetch_ffmpeg() -> Path: ffmpeg_entry = next( name for name in zf.namelist() if name.endswith("/bin/ffmpeg.exe") ) - with zf.open(ffmpeg_entry) as src, Path(dest).open("wb") as out: - out.write(src.read()) + with zf.open(ffmpeg_entry) as src: + Path(dest).write_bytes(src.read()) print(f"ffmpeg staged at {dest} ({dest.stat().st_size // 1024 // 1024} MB)") return dest @@ -109,8 +109,8 @@ def fetch_macos_arm64_binaries() -> dict[str, Path]: for name in zf.namelist() if Path(name).name == tool_name and not name.endswith("/") ) - with zf.open(entry_name) as src, Path(dest).open("wb") as out: - out.write(src.read()) + with zf.open(entry_name) as src: + Path(dest).write_bytes(src.read()) dest.chmod(dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) diff --git a/scripts/rtorrent-manager.py b/scripts/rtorrent-manager.py index fafc81e..8ccb9cf 100644 --- a/scripts/rtorrent-manager.py +++ b/scripts/rtorrent-manager.py @@ -202,7 +202,7 @@ def format_size(size_bytes: int | None) -> str: def main() -> None: - """Main entry point for the torrent scanner.""" + """Run the torrent scanner command-line workflow.""" parser = argparse.ArgumentParser( description="Scan and manage torrent files", formatter_class=argparse.RawDescriptionHelpFormatter,