Unsafe fixes.

This commit is contained in:
2026-05-25 02:12:31 +00:00
parent 95081ca50e
commit 54a4b57230
20 changed files with 55 additions and 81 deletions
+1 -1
View File
@@ -1 +1 @@
"""MediaHive - Media Browser Server""" """MediaHive - Media Browser Server."""
+1 -1
View File
@@ -31,7 +31,7 @@ def _derive_name(path: str) -> str:
return p.name or p.anchor.strip("/\\").lower() or "media" return p.name or p.anchor.strip("/\\").lower() or "media"
def main(): def main() -> None:
_configure_windows_event_loop_policy() _configure_windows_event_loop_policy()
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
+2 -2
View File
@@ -1,7 +1,7 @@
"""Platform-appropriate config persistence for MediaHive. r"""Platform-appropriate config persistence for MediaHive.
Config file location: Config file location:
Windows: %APPDATA%\\mediahive\\config.toml Windows: %APPDATA%\mediahive\config.toml
macOS: ~/Library/Application Support/mediahive/config.toml macOS: ~/Library/Application Support/mediahive/config.toml
Linux: $XDG_CONFIG_HOME/mediahive/config.toml (~/.config/mediahive/config.toml) Linux: $XDG_CONFIG_HOME/mediahive/config.toml (~/.config/mediahive/config.toml)
""" """
+1 -1
View File
@@ -17,7 +17,7 @@ def _configure_windows_event_loop_policy() -> None:
asyncio.set_event_loop_policy(policy_cls()) asyncio.set_event_loop_policy(policy_cls())
def main(): def main() -> None:
_configure_windows_event_loop_policy() _configure_windows_event_loop_policy()
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
+4 -10
View File
@@ -400,10 +400,7 @@ async def _process_movies(
return await find_playable_file(item.path) is not None return await find_playable_file(item.path) is not None
# Filter movies with playable files # Filter movies with playable files
valid_movies = [] valid_movies = [item for item in categories[ContentType.MOVIE] if await has_playable(item)]
for item in categories[ContentType.MOVIE]:
if await has_playable(item):
valid_movies.append(item)
skipped = len(categories[ContentType.MOVIE]) - len(valid_movies) skipped = len(categories[ContentType.MOVIE]) - len(valid_movies)
if skipped > 0: if skipped > 0:
logger.debug( logger.debug(
@@ -429,7 +426,7 @@ async def _process_movies(
len(categories[ContentType.MOVIE]), len(categories[ContentType.MOVIE]),
) if movie_groups else None ) if movie_groups else None
for idx, (movie_key, items) in enumerate(movie_groups.items(), 1): for idx, (_movie_key, items) in enumerate(movie_groups.items(), 1):
first_item = items[0] first_item = items[0]
logger.debug( logger.debug(
" [%d/%d] %s (%s)", " [%d/%d] %s (%s)",
@@ -658,10 +655,7 @@ async def _process_series(
return len(await find_episode_files(item.path)) > 0 return len(await find_episode_files(item.path)) > 0
# Filter series with video content # Filter series with video content
valid_series = [] valid_series = [item for item in categories[ContentType.SERIES] if await has_video_content(item)]
for item in categories[ContentType.SERIES]:
if await has_video_content(item):
valid_series.append(item)
skipped = len(categories[ContentType.SERIES]) - len(valid_series) skipped = len(categories[ContentType.SERIES]) - len(valid_series)
if skipped > 0: if skipped > 0:
logger.info( logger.info(
@@ -687,7 +681,7 @@ async def _process_series(
len(categories[ContentType.SERIES]), len(categories[ContentType.SERIES]),
) )
for idx, (series_key, items) in enumerate(series_groups.items(), 1): for idx, (_series_key, items) in enumerate(series_groups.items(), 1):
first_item = items[0] first_item = items[0]
logger.debug(" [%d/%d] %s", idx, len(series_groups), first_item.title) logger.debug(" [%d/%d] %s", idx, len(series_groups), first_item.title)
+6 -9
View File
@@ -12,6 +12,7 @@ defined in ``.mediahive/scanignore`` (gitignore-style syntax).
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import contextlib
import logging import logging
import uuid import uuid
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
@@ -59,7 +60,7 @@ class RootScanner:
root_id: str, root_id: str,
media_root: Path, media_root: Path,
send: Send, send: Send,
): ) -> None:
self.root_id = root_id self.root_id = root_id
self.media_root = media_root self.media_root = media_root
self._send = send self._send = send
@@ -108,10 +109,8 @@ class RootScanner:
self._rescan_worker_task, self._rescan_worker_task,
): ):
if task and not task.done(): if task and not task.done():
try: with contextlib.suppress(TimeoutError, asyncio.CancelledError):
await asyncio.wait_for(task, timeout=2.0) await asyncio.wait_for(task, timeout=2.0)
except TimeoutError, asyncio.CancelledError:
pass
def is_scanning(self) -> bool: def is_scanning(self) -> bool:
return self._scan_task is not None and not self._scan_task.done() return self._scan_task is not None and not self._scan_task.done()
@@ -266,7 +265,7 @@ class RootScanner:
try: try:
root_children = await asyncio.to_thread(lambda: list(root_ap.iterdir())) root_children = await asyncio.to_thread(lambda: list(root_ap.iterdir()))
except OSError, PermissionError: except OSError, PermissionError:
logger.error("Cannot list media root: %s", self.media_root) logger.exception("Cannot list media root: %s", self.media_root)
return downloads return downloads
for item_async in root_children: for item_async in root_children:
@@ -302,7 +301,7 @@ class RootScanner:
1. Discover downloads 1. Discover downloads
2. Categorise → movies / series 2. Categorise → movies / series
3. Iterate async generators, send each item as Upsert 3. Iterate async generators, send each item as Upsert
4. Queue showreel tasks 4. Queue showreel tasks.
""" """
task_id = f"scan-{uuid.uuid4().hex[:8]}" task_id = f"scan-{uuid.uuid4().hex[:8]}"
media_root_str = self.media_root.as_posix() media_root_str = self.media_root.as_posix()
@@ -674,10 +673,8 @@ class RootScanner:
"Showreel worker error (queue size=%d)", "Showreel worker error (queue size=%d)",
self._showreel_queue.qsize(), self._showreel_queue.qsize(),
) )
try: with contextlib.suppress(ValueError):
self._showreel_queue.task_done() self._showreel_queue.task_done()
except ValueError:
pass
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+3 -2
View File
@@ -2,6 +2,7 @@
import asyncio import asyncio
import glob import glob
import operator
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
@@ -216,7 +217,7 @@ async def find_playable_file(path: Path) -> str | None:
_playable_file_cache[cache_key] = None _playable_file_cache[cache_key] = None
return None return None
video_files.sort(key=lambda x: x[1], reverse=True) video_files.sort(key=operator.itemgetter(1), reverse=True)
result = video_files[0][0] result = video_files[0][0]
_playable_file_cache[cache_key] = result _playable_file_cache[cache_key] = result
return result return result
@@ -312,7 +313,7 @@ async def find_metadata_probe_file(playable_path: str | None) -> str | None:
_bluray_probe_file_cache[cache_key] = None _bluray_probe_file_cache[cache_key] = None
return None return None
candidates.sort(key=lambda x: x[1], reverse=True) candidates.sort(key=operator.itemgetter(1), reverse=True)
result = candidates[0][0] result = candidates[0][0]
_bluray_probe_file_cache[cache_key] = result _bluray_probe_file_cache[cache_key] = result
return result return result
+5 -6
View File
@@ -6,6 +6,7 @@ and HDR passthrough.
""" """
import asyncio import asyncio
import contextlib
import logging import logging
import re import re
import shlex import shlex
@@ -75,7 +76,7 @@ async def _run_ffmpeg(
stdout, stderr = await proc.communicate() stdout, stderr = await proc.communicate()
except Exception: except Exception:
stdout, stderr = b"", b"" stdout, stderr = b"", b""
logger.error( logger.exception(
"ffmpeg command timed out. cmd=%s stderr=%s", "ffmpeg command timed out. cmd=%s stderr=%s",
shlex.join(cmd), shlex.join(cmd),
_decode_stderr(stderr), _decode_stderr(stderr),
@@ -113,10 +114,8 @@ async def _kill_proc(proc: asyncio.subprocess.Process | None) -> None:
"""Kill a subprocess immediately if it is still running.""" """Kill a subprocess immediately if it is still running."""
if proc is not None and proc.returncode is None: if proc is not None and proc.returncode is None:
proc.kill() proc.kill()
try: with contextlib.suppress(Exception):
await asyncio.wait_for(proc.wait(), timeout=2) await asyncio.wait_for(proc.wait(), timeout=2)
except Exception:
pass
# Showreel timestamp positions in seconds (5, 10, 15, 20, 25 minutes) # Showreel timestamp positions in seconds (5, 10, 15, 20, 25 minutes)
@@ -763,7 +762,7 @@ async def generate_showreel_images(
# Detect Dolby Vision profile for tonemapping (profiles 5/7 need conversion) # Detect Dolby Vision profile for tonemapping (profiles 5/7 need conversion)
dovi_profile = await detect_dovi_profile(ffmpeg_input) dovi_profile = await detect_dovi_profile(ffmpeg_input)
needs_tonemap = dovi_profile is not None and dovi_profile in (5, 7) needs_tonemap = dovi_profile is not None and dovi_profile in {5, 7}
if needs_tonemap: if needs_tonemap:
logger.info(" DoVi profile %d detected, will convert to HDR10", dovi_profile) logger.info(" DoVi profile %d detected, will convert to HDR10", dovi_profile)
@@ -924,7 +923,7 @@ async def generate_episode_reel(
# Detect Dolby Vision profile for tonemapping (profiles 5/7 need conversion) # Detect Dolby Vision profile for tonemapping (profiles 5/7 need conversion)
dovi_profile = await detect_dovi_profile(ffmpeg_input) dovi_profile = await detect_dovi_profile(ffmpeg_input)
needs_tonemap = dovi_profile is not None and dovi_profile in (5, 7) needs_tonemap = dovi_profile is not None and dovi_profile in {5, 7}
if needs_tonemap: if needs_tonemap:
logger.info(" DoVi profile %d detected, will convert to HDR10", dovi_profile) logger.info(" DoVi profile %d detected, will convert to HDR10", dovi_profile)
+6 -13
View File
@@ -85,14 +85,11 @@ async def _load_from_cache(cache_path: Path):
return _NOT_FOUND return _NOT_FOUND
async def _save_to_cache(cache_path: Path, data: dict | None): async def _save_to_cache(cache_path: Path, data: dict | None) -> None:
"""Save response to cache.""" """Save response to cache."""
try: try:
await AsyncPath(_get_cache_dir()).mkdir(parents=True, exist_ok=True) await AsyncPath(_get_cache_dir()).mkdir(parents=True, exist_ok=True)
if data is None: text = json.dumps({"_cached_none": True}) if data is None else json.dumps(data)
text = json.dumps({"_cached_none": True})
else:
text = json.dumps(data)
await AsyncPath(cache_path).write_text(text, encoding="utf-8") await AsyncPath(cache_path).write_text(text, encoding="utf-8")
except Exception: except Exception:
pass # Cache write failures are not critical pass # Cache write failures are not critical
@@ -144,20 +141,18 @@ async def tmdb_api_request(
async def fetch_movie_details(movie_id: int) -> dict | None: async def fetch_movie_details(movie_id: int) -> dict | None:
"""Fetch detailed movie info including credits, similar, keywords, and alternative titles.""" """Fetch detailed movie info including credits, similar, keywords, and alternative titles."""
# Use append_to_response to get multiple data in one request # Use append_to_response to get multiple data in one request
data = await tmdb_api_request( return await tmdb_api_request(
f"/movie/{movie_id}", f"/movie/{movie_id}",
{"append_to_response": "credits,similar,keywords,alternative_titles"}, {"append_to_response": "credits,similar,keywords,alternative_titles"},
) )
return data
async def fetch_series_details(series_id: int) -> dict | None: async def fetch_series_details(series_id: int) -> dict | None:
"""Fetch detailed TV series info including credits, similar, and keywords.""" """Fetch detailed TV series info including credits, similar, and keywords."""
# Use append_to_response to get multiple data in one request # Use append_to_response to get multiple data in one request
data = await tmdb_api_request( return await tmdb_api_request(
f"/tv/{series_id}", {"append_to_response": "credits,similar,keywords"} f"/tv/{series_id}", {"append_to_response": "credits,similar,keywords"}
) )
return data
async def fetch_season_details(series_id: int, season_number: int) -> SeasonInfo | None: async def fetch_season_details(series_id: int, season_number: int) -> SeasonInfo | None:
@@ -238,12 +233,10 @@ def _generate_title_variants(words: list[str], min_words: int = 2) -> list[str]:
variants.append(" ".join(words)) variants.append(" ".join(words))
# Then try removing from end (most common: edition names at end) # Then try removing from end (most common: edition names at end)
for num_words in range(len(words) - 1, min_words - 1, -1): variants.extend(" ".join(words[:num_words]) for num_words in range(len(words) - 1, min_words - 1, -1))
variants.append(" ".join(words[:num_words]))
# Then try removing from start (garbage at beginning) # Then try removing from start (garbage at beginning)
for start in range(1, len(words) - min_words + 1): variants.extend(" ".join(words[start:]) for start in range(1, len(words) - min_words + 1))
variants.append(" ".join(words[start:]))
# Finally try middle portions (remove from both ends) # Finally try middle portions (remove from both ends)
for start in range(1, len(words) - min_words): for start in range(1, len(words) - min_words):
+2 -3
View File
@@ -184,7 +184,7 @@ async def find_common_root(paths: list[Path]) -> Path | None:
# Find common prefix # Find common prefix
common_parts = [] common_parts = []
for parts in zip(*all_parts): for parts in zip(*all_parts, strict=False):
if len(set(parts)) == 1: if len(set(parts)) == 1:
common_parts.append(parts[0]) common_parts.append(parts[0])
else: else:
@@ -216,8 +216,7 @@ def sanitize_filename(name: str) -> str:
"""Sanitize a string for use as a filename.""" """Sanitize a string for use as a filename."""
for char in ["/", "\\", ":", "*", "?", '"', "<", ">", "|"]: for char in ["/", "\\", ":", "*", "?", '"', "<", ">", "|"]:
name = name.replace(char, "_") name = name.replace(char, "_")
name = name.strip(". ") return name.strip(". ")
return name
def get_media_folder_name(title: str, year: int | None, media_type: str) -> str: def get_media_folder_name(title: str, year: int | None, media_type: str) -> str:
+3 -4
View File
@@ -7,6 +7,7 @@ debounced background task.
""" """
import asyncio import asyncio
import contextlib
import logging import logging
import os import os
from datetime import datetime from datetime import datetime
@@ -51,7 +52,7 @@ class IndexStore:
snapshot_path: Path, snapshot_path: Path,
media_root: str | None = None, media_root: str | None = None,
root_id: str | None = None, root_id: str | None = None,
): ) -> None:
self.snapshot_path = snapshot_path self.snapshot_path = snapshot_path
self.media_root = media_root self.media_root = media_root
self.root_id = root_id self.root_id = root_id
@@ -205,10 +206,8 @@ class IndexStore:
"""Force-write a snapshot immediately (e.g. on shutdown).""" """Force-write a snapshot immediately (e.g. on shutdown)."""
if self._snapshot_task and not self._snapshot_task.done(): if self._snapshot_task and not self._snapshot_task.done():
self._snapshot_task.cancel() self._snapshot_task.cancel()
try: with contextlib.suppress(asyncio.CancelledError):
await self._snapshot_task await self._snapshot_task
except asyncio.CancelledError:
pass
await self._write_snapshot() await self._write_snapshot()
# ------------------------------------------------------------------ # ------------------------------------------------------------------
+5 -6
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import contextlib
import hashlib import hashlib
import logging import logging
from pathlib import Path from pathlib import Path
@@ -86,7 +87,7 @@ class RootEntry(msgspec.Struct):
class RootContext: class RootContext:
"""Runtime container for a single media root.""" """Runtime container for a single media root."""
def __init__(self, root_id: str, root_path: Path, name: str | None = None): def __init__(self, root_id: str, root_path: Path, name: str | None = None) -> None:
self.root_id = root_id self.root_id = root_id
self.name = name or root_id self.name = name or root_id
self.root_path = root_path self.root_path = root_path
@@ -134,10 +135,8 @@ class RootContext:
if self._consumer_task and not self._consumer_task.done(): if self._consumer_task and not self._consumer_task.done():
self._consumer_task.cancel() self._consumer_task.cancel()
try: with contextlib.suppress(asyncio.CancelledError):
await self._consumer_task await self._consumer_task
except asyncio.CancelledError:
pass
try: try:
await self.store.flush_snapshot() await self.store.flush_snapshot()
@@ -175,7 +174,7 @@ class RootContext:
class Supervisor: class Supervisor:
"""Manages the active set of RootContexts and handles atomic replacement.""" """Manages the active set of RootContexts and handles atomic replacement."""
def __init__(self): def __init__(self) -> None:
# Active contexts keyed by root_id # Active contexts keyed by root_id
self._contexts: dict[str, RootContext] = {} self._contexts: dict[str, RootContext] = {}
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
@@ -211,7 +210,7 @@ class Supervisor:
total_movie_versions = 0 total_movie_versions = 0
total_series_episodes = 0 total_series_episodes = 0
for ctx in self._contexts.values(): for ctx in self._contexts.values():
if ctx.status != "ready" and ctx.status != "scanning": if ctx.status not in {"ready", "scanning"}:
continue continue
movies.extend(ctx.store.movies.values()) movies.extend(ctx.store.movies.values())
series.extend(ctx.store.series.values()) series.extend(ctx.store.series.values())
+5 -10
View File
@@ -18,7 +18,7 @@ import subprocess
import sys import sys
import urllib.error import urllib.error
import urllib.request import urllib.request
from contextlib import asynccontextmanager from contextlib import asynccontextmanager, suppress
from pathlib import Path from pathlib import Path
import aiofiles import aiofiles
@@ -248,10 +248,7 @@ def _etag_matches_if_none_match(if_none_match: str | None, etag: str) -> bool:
return v return v
wanted = normalize(etag) wanted = normalize(etag)
for candidate in if_none_match.split(","): return any(normalize(candidate) == wanted for candidate in if_none_match.split(","))
if normalize(candidate) == wanted:
return True
return False
def _validate_root_paths(roots: dict[str, str]) -> dict[str, str]: def _validate_root_paths(roots: dict[str, str]) -> dict[str, str]:
@@ -283,7 +280,7 @@ def _validate_root_paths(roots: dict[str, str]) -> dict[str, str]:
async def _attach_scanners() -> None: async def _attach_scanners() -> None:
"""Ensure every active root context has a running scanner.""" """Ensure every active root context has a running scanner."""
for ctx in supervisor.all_contexts().values(): for ctx in supervisor.all_contexts().values():
if ctx.scanner is None and ctx.status in ("ready", "loading"): if ctx.scanner is None and ctx.status in {"ready", "loading"}:
try: try:
scanner = RootScanner(ctx.root_id, ctx.root_path, ctx.send_event) scanner = RootScanner(ctx.root_id, ctx.root_path, ctx.send_event)
await scanner.start() await scanner.start()
@@ -361,10 +358,8 @@ async def lifespan(app: FastAPI):
yield yield
activation_task.cancel() activation_task.cancel()
try: with suppress(asyncio.CancelledError):
await activation_task await activation_task
except asyncio.CancelledError:
pass
await supervisor.shutdown() await supervisor.shutdown()
@@ -464,7 +459,7 @@ async def trigger_root_scan(root_id: str):
@app.websocket("/api/roots/{root_id}/ws") @app.websocket("/api/roots/{root_id}/ws")
async def ws_endpoint(ws: WebSocket, root_id: str): async def ws_endpoint(ws: WebSocket, root_id: str) -> None:
"""Live index updates and task progress for a single root.""" """Live index updates and task progress for a single root."""
ctx = supervisor.get(root_id) ctx = supervisor.get(root_id)
if ctx is None: if ctx is None:
+3 -3
View File
@@ -9,7 +9,7 @@ from pathlib import Path
class SCGITransport(xmlrpc.client.Transport): class SCGITransport(xmlrpc.client.Transport):
"""SCGI transport for communicating with rtorrent via Unix socket.""" """SCGI transport for communicating with rtorrent via Unix socket."""
def __init__(self, socket_path: str): def __init__(self, socket_path: str) -> None:
super().__init__() super().__init__()
self.socket_path = socket_path self.socket_path = socket_path
@@ -48,7 +48,7 @@ class SCGITransport(xmlrpc.client.Transport):
class RTorrentClient: class RTorrentClient:
"""Client for communicating with rtorrent via XMLRPC over SCGI socket.""" """Client for communicating with rtorrent via XMLRPC over SCGI socket."""
def __init__(self, socket_path: str = "/home/user/rtorrent/.session/rpc.socket"): def __init__(self, socket_path: str = "/home/user/rtorrent/.session/rpc.socket") -> None:
self.socket_path = socket_path self.socket_path = socket_path
transport = SCGITransport(socket_path) transport = SCGITransport(socket_path)
self.proxy = xmlrpc.client.ServerProxy( self.proxy = xmlrpc.client.ServerProxy(
@@ -59,7 +59,7 @@ class RTorrentClient:
"""Get set of info hashes for all currently loaded torrents.""" """Get set of info hashes for all currently loaded torrents."""
try: try:
downloads = self.proxy.download_list("") downloads = self.proxy.download_list("")
return set(h.upper() for h in downloads) return {h.upper() for h in downloads}
except Exception as e: except Exception as e:
print(f"Error getting loaded torrents: {e}") print(f"Error getting loaded torrents: {e}")
return set() return set()
+1 -1
View File
@@ -49,7 +49,7 @@ async def run_devserver(
await pg.spawn(*vite, cwd=front) await pg.spawn(*vite, cwd=front)
def main(): def main() -> None:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Run Vite and FastAPI development servers", description="Run Vite and FastAPI development servers",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
+1 -1
View File
@@ -10,6 +10,6 @@ from buildutil import build
class CustomBuildHook(BuildHookInterface): class CustomBuildHook(BuildHookInterface):
def initialize(self, version, build_data): def initialize(self, version, build_data) -> None:
super().initialize(version, build_data) super().initialize(version, build_data)
build("frontend") build("frontend")
+1 -1
View File
@@ -178,7 +178,7 @@ def build(folder: str = "frontend") -> None:
logger.warning(e) logger.warning(e)
raise SystemExit(1) raise SystemExit(1)
def run(cmd): def run(cmd) -> None:
display_cmd = [Path(cmd[0]).stem, *cmd[1:]] display_cmd = [Path(cmd[0]).stem, *cmd[1:]]
logger.info("### %s", " ".join(display_cmd)) logger.info("### %s", " ".join(display_cmd))
subprocess.run(cmd, check=True, cwd=folder) subprocess.run(cmd, check=True, cwd=folder)
+2 -2
View File
@@ -16,7 +16,7 @@ from fastapi_vue.hostutil import parse_endpoint
class ProcessGroup: class ProcessGroup:
"""Manage async subprocesses with automatic cleanup, like TaskGroup for processes.""" """Manage async subprocesses with automatic cleanup, like TaskGroup for processes."""
def __init__(self): def __init__(self) -> None:
self._procs: list[asyncio.subprocess.Process] = [] self._procs: list[asyncio.subprocess.Process] = []
self._cmds: dict[int, str] = {} # pid -> command name self._cmds: dict[int, str] = {} # pid -> command name
@@ -59,7 +59,7 @@ class ProcessGroup:
"""Wait for one process to exit, terminate others, then wait for all.""" """Wait for one process to exit, terminate others, then wait for all."""
await self._cleanup(immediate=exc_type is not None) await self._cleanup(immediate=exc_type is not None)
async def _cleanup(self, immediate: bool = False): async def _cleanup(self, immediate: bool = False) -> None:
running = [p for p in self._procs if p.returncode is None] running = [p for p in self._procs if p.returncode is None]
if not running: if not running:
return return
+1 -2
View File
@@ -84,13 +84,12 @@ def find_dist_files(version: str) -> list[Path]:
Raises FileNotFoundError listing every missing file if any are absent. Raises FileNotFoundError listing every missing file if any are absent.
""" """
dist_dir = REPO_ROOT / "dist" dist_dir = REPO_ROOT / "dist"
ver = re.escape(version)
wheel = next((p for p in dist_dir.glob(f"mediahive-{version}-*.whl")), None) wheel = next((p for p in dist_dir.glob(f"mediahive-{version}-*.whl")), None)
sdist = next( sdist = next(
( (
p p
for p in dist_dir.glob(f"mediahive-{version}.*") for p in dist_dir.glob(f"mediahive-{version}.*")
if p.suffix in (".gz", ".zip") and p.name != f"mediahive-{version}.zip" if p.suffix in {".gz", ".zip"} and p.name != f"mediahive-{version}.zip"
), ),
None, None,
) )
+2 -3
View File
@@ -164,8 +164,7 @@ def scan_torrent_directories(paths: list[str]) -> Iterator[Path]:
for dir_path in glob.glob(pattern): for dir_path in glob.glob(pattern):
torrent_dir = Path(dir_path) torrent_dir = Path(dir_path)
if torrent_dir.is_dir(): if torrent_dir.is_dir():
for torrent_file in torrent_dir.glob("*.torrent"): yield from torrent_dir.glob("*.torrent")
yield torrent_file
def find_torrents_with_tracker( def find_torrents_with_tracker(
@@ -203,7 +202,7 @@ def format_size(size_bytes: int | None) -> str:
return f"{size_bytes:.2f} PB" return f"{size_bytes:.2f} PB"
def main(): def main() -> None:
"""Main entry point for the torrent scanner.""" """Main entry point for the torrent scanner."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Scan and manage torrent files", description="Scan and manage torrent files",