Misc. fixes #2.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
"""MediaHive CLI entrypoint."""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Hivescan CLI entrypoint."""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user