Misc. fixes #2.

This commit is contained in:
2026-05-25 02:37:51 +00:00
parent e86f1e22de
commit 17c5a47936
15 changed files with 46 additions and 29 deletions
+2
View File
@@ -1,3 +1,5 @@
"""MediaHive CLI entrypoint."""
import argparse
import asyncio
import json
+2
View File
@@ -1,3 +1,5 @@
"""Hivescan CLI entrypoint."""
import argparse
import asyncio
import json
+8 -4
View File
@@ -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]
+5 -4
View File
@@ -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
+5 -5
View File
@@ -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"],
+1 -1
View File
@@ -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:
+2 -1
View File
@@ -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()
+1 -1
View File
@@ -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:
+1 -2
View File
@@ -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
+2 -1
View File
@@ -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:
+1 -1
View File
@@ -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,
+3 -1
View File
@@ -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
+8 -3
View File
@@ -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)
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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,