From f65c4ff3deea4e4cb2681531f3cdd6618e401eed Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 9 Feb 2026 03:29:03 +0000 Subject: [PATCH] Restructure to mediahive.hivescan. --- hivescan/__main__.py | 61 ---------- mediahive/__init__.py | 1 + mediahive/__main__.py | 104 +++++++++++++----- {hivescan => mediahive/hivescan}/__init__.py | 21 ++-- {hivescan => mediahive/hivescan}/images.py | 3 +- .../hivescan}/index_store.py | 3 +- {hivescan => mediahive/hivescan}/indexer.py | 15 +-- {hivescan => mediahive/hivescan}/models.py | 1 + {hivescan => mediahive/hivescan}/parsing.py | 3 +- {hivescan => mediahive/hivescan}/scanning.py | 7 +- {hivescan => mediahive/hivescan}/server.py | 23 ++-- {hivescan => mediahive/hivescan}/showreel.py | 1 + {hivescan => mediahive/hivescan}/structs.py | 1 + .../hivescan}/tmdb_client.py | 5 +- {hivescan => mediahive/hivescan}/utils.py | 1 + mediahive/server.py | 3 +- pyproject.toml | 3 +- 17 files changed, 131 insertions(+), 125 deletions(-) delete mode 100644 hivescan/__main__.py rename {hivescan => mediahive/hivescan}/__init__.py (70%) rename {hivescan => mediahive/hivescan}/images.py (98%) rename {hivescan => mediahive/hivescan}/index_store.py (99%) rename {hivescan => mediahive/hivescan}/indexer.py (98%) rename {hivescan => mediahive/hivescan}/models.py (99%) rename {hivescan => mediahive/hivescan}/parsing.py (96%) rename {hivescan => mediahive/hivescan}/scanning.py (96%) rename {hivescan => mediahive/hivescan}/server.py (95%) rename {hivescan => mediahive/hivescan}/showreel.py (99%) rename {hivescan => mediahive/hivescan}/structs.py (99%) rename {hivescan => mediahive/hivescan}/tmdb_client.py (99%) rename {hivescan => mediahive/hivescan}/utils.py (99%) diff --git a/hivescan/__main__.py b/hivescan/__main__.py deleted file mode 100644 index 2ae85f3..0000000 --- a/hivescan/__main__.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Entry point for hivescan — launches the scanning FastAPI server.""" - -import argparse -import os - - -def main(): - parser = argparse.ArgumentParser( - description="Hivescan server — continuous media scanning with live WS updates.", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - %(prog)s /path/to/torrents/* # Scan paths, auto-detect common root - %(prog)s /mnt/disk1/* /mnt/disk2/* # Scan multiple locations - %(prog)s /torrents/* -o /srv/media # Override output directory - %(prog)s /torrents/* --port 9000 # Custom port - -The server exposes: - WS /ws Live index updates & task progress - POST /api/scan Trigger a new scan - GET /api/status Current server status - GET /api/index Full index as JSON (HTTP fallback) - """, - ) - - parser.add_argument( - "paths", - nargs="+", - help="Folders or glob patterns to scan for downloads", - ) - parser.add_argument( - "-o", - "--output-dir", - metavar="DIR", - help="Output directory for index and covers (default: .mediahive at common root)", - ) - parser.add_argument( - "--host", - default="0.0.0.0", - help="Host to bind to (default: 0.0.0.0)", - ) - parser.add_argument( - "--port", - type=int, - default=8421, - help="Port to listen on (default: 8421)", - ) - args = parser.parse_args() - - # Pass configuration via environment variables (read by server.py lifespan) - os.environ["HIVESCAN_PATHS"] = os.pathsep.join(args.paths) - if args.output_dir: - os.environ["HIVESCAN_OUTPUT"] = args.output_dir - - from hivescan.server import run - - run(host=args.host, port=args.port) - - -if __name__ == "__main__": - main() diff --git a/mediahive/__init__.py b/mediahive/__init__.py index 6ea254b..4a3914f 100644 --- a/mediahive/__init__.py +++ b/mediahive/__init__.py @@ -1 +1,2 @@ """MediaHive - Media Browser Server""" + diff --git a/mediahive/__main__.py b/mediahive/__main__.py index 39c277c..dfe3dfa 100644 --- a/mediahive/__main__.py +++ b/mediahive/__main__.py @@ -9,42 +9,96 @@ DEVMODE = bool(os.getenv("MEDIAHIVE_FRONTEND_URL")) def main(): - parser = argparse.ArgumentParser(description="Run the mediahive server.") - parser.add_argument( + parser = argparse.ArgumentParser(description="MediaHive - Media scanning, indexing, and streaming") + subparsers = parser.add_subparsers(dest='command', required=True, help='Available commands') + + # Server subcommand + server_parser = subparsers.add_parser('server', help='Run the MediaHive streaming server') + server_parser.add_argument( "media_folder", nargs="?", help="Path to the media folder (default: MEDIAHIVE_PATH or current directory)", ) - parser.add_argument( + server_parser.add_argument( "-l", "--listen", action="append", help=(f"Endpoint (default: localhost:{DEFAULT_PORT})."), ) + + # Scan subcommand + scan_parser = subparsers.add_parser('scan', help='Run the Hivescan media scanning server', + description="Hivescan server — continuous media scanning with live WS updates.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + mediahive scan /path/to/torrents/* # Scan paths, auto-detect common root + mediahive scan /mnt/disk1/* /mnt/disk2/* # Scan multiple locations + mediahive scan /torrents/* -o /srv/media # Override output directory + mediahive scan /torrents/* --port 9000 # Custom port + +The server exposes: + WS /ws Live index updates & task progress + POST /api/scan Trigger a new scan + GET /api/status Current server status + GET /api/index Full index as JSON (HTTP fallback) + """) + scan_parser.add_argument( + "paths", + nargs="+", + help="Folders or glob patterns to scan for downloads", + ) + scan_parser.add_argument( + "-o", + "--output-dir", + metavar="DIR", + help="Output directory for index and covers (default: .mediahive at common root)", + ) + scan_parser.add_argument( + "--host", + default="0.0.0.0", + help="Host to bind to (default: 0.0.0.0)", + ) + scan_parser.add_argument( + "--port", + type=int, + default=8421, + help="Port to listen on (default: 8421)", + ) + args = parser.parse_args() - # Determine media folder - match Path( - args.media_folder or os.environ.get("MEDIAHIVE_PATH") or Path.cwd() - ).parts: - case (*rest, ".mediahive", "index.json"): - ... - case (*rest, ".mediahive"): - ... - case rest: - ... - mediaroot = Path(*rest).resolve() - if not mediaroot.exists() or not mediaroot.is_dir(): - print(f"Error: Folder does not exist: {mediaroot}") - exit(1) - os.environ["MEDIAHIVE_PATH"] = mediaroot.as_posix() - dev = {"reload": True, "reload_dirs": ["mediahive"]} - server.run( - "mediahive.server:app", - listen=args.listen, - default_port=DEFAULT_PORT, - **(dev if DEVMODE else {}), - ) + if args.command == 'server': + # Determine media folder + match Path( + args.media_folder or os.environ.get("MEDIAHIVE_PATH") or Path.cwd() + ).parts: + case (*rest, ".mediahive", "index.json"): + ... + case (*rest, ".mediahive"): + ... + case rest: + ... + mediaroot = Path(*rest).resolve() + if not mediaroot.exists() or not mediaroot.is_dir(): + print(f"Error: Folder does not exist: {mediaroot}") + exit(1) + os.environ["MEDIAHIVE_PATH"] = mediaroot.as_posix() + dev = {"reload": True, "reload_dirs": ["mediahive"]} + server.run( + "mediahive.server:app", + listen=args.listen, + default_port=DEFAULT_PORT, + **(dev if DEVMODE else {}), + ) + elif args.command == 'scan': + # Pass configuration via environment variables (read by server.py lifespan) + os.environ["HIVESCAN_PATHS"] = os.pathsep.join(args.paths) + if args.output_dir: + os.environ["HIVESCAN_OUTPUT"] = args.output_dir + + from mediahive.hivescan.server import run + run(host=args.host, port=args.port) if __name__ == "__main__": diff --git a/hivescan/__init__.py b/mediahive/hivescan/__init__.py similarity index 70% rename from hivescan/__init__.py rename to mediahive/hivescan/__init__.py index 37534e1..04c380e 100644 --- a/hivescan/__init__.py +++ b/mediahive/hivescan/__init__.py @@ -6,22 +6,22 @@ Usage: hivescan /path/* --port 9000 # Custom port Or as a library: - from hivescan.index_store import IndexStore - from hivescan.structs import Movie, Series, TaskInfo - from hivescan.server import app + from mediahive.hivescan.index_store import IndexStore + from mediahive.hivescan.structs import Movie, Series, TaskInfo + from mediahive.hivescan.server import app """ -from hivescan.models import ContentType, ContentHash, ParsedContent -from hivescan.scanning import ( +from mediahive.hivescan.models import ContentType, ContentHash, ParsedContent +from mediahive.hivescan.scanning import ( scan_downloads, categorize_downloads, find_playable_file, find_episode_files, ) -from hivescan.index_store import IndexStore -from hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root -from hivescan.showreel import generate_showreel_images, generate_episode_reel -from hivescan.structs import ( +from mediahive.hivescan.index_store import IndexStore +from mediahive.hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root +from mediahive.hivescan.showreel import generate_showreel_images, generate_episode_reel +from mediahive.hivescan.structs import ( CastMember, Episode, EpisodeRelease, @@ -37,7 +37,7 @@ from hivescan.structs import ( TMDbInfo, TMDbSeasonInfo, ) -from hivescan.tmdb_client import ( +from mediahive.hivescan.tmdb_client import ( fetch_movie_info, fetch_series_info, fetch_season_details, @@ -83,3 +83,4 @@ __all__ = [ "DEFAULT_OUTPUT_FOLDER", "find_common_root", ] + diff --git a/hivescan/images.py b/mediahive/hivescan/images.py similarity index 98% rename from hivescan/images.py rename to mediahive/hivescan/images.py index 66a68a8..81f8ffa 100644 --- a/hivescan/images.py +++ b/mediahive/hivescan/images.py @@ -6,7 +6,7 @@ from typing import Optional from aiopathlib import AsyncPath -from hivescan.utils import get_media_folder_path +from mediahive.hivescan.utils import get_media_folder_path # TMDb image configuration @@ -113,3 +113,4 @@ async def download_season_poster( await AsyncPath(media_folder).mkdir(parents=True, exist_ok=True) url = f"{TMDB_IMAGE_BASE}/{DEFAULT_POSTER_SIZE}{poster_path}" return await _download_image(url, output_path, f"season {season_num} poster") + diff --git a/hivescan/index_store.py b/mediahive/hivescan/index_store.py similarity index 99% rename from hivescan/index_store.py rename to mediahive/hivescan/index_store.py index b25613a..06af586 100644 --- a/hivescan/index_store.py +++ b/mediahive/hivescan/index_store.py @@ -18,7 +18,7 @@ import msgspec from aiopathlib import AsyncPath from fastapi import WebSocket -from hivescan.structs import ( +from mediahive.hivescan.structs import ( IndexSnapshot, MediaStats, Movie, @@ -250,3 +250,4 @@ class IndexStore: movies=movies_list, series=series_list, ) + diff --git a/hivescan/indexer.py b/mediahive/hivescan/indexer.py similarity index 98% rename from hivescan/indexer.py rename to mediahive/hivescan/indexer.py index debfae2..5646270 100644 --- a/hivescan/indexer.py +++ b/mediahive/hivescan/indexer.py @@ -6,11 +6,11 @@ import logging from pathlib import Path from typing import AsyncIterator, Dict, List, Optional, Tuple -from hivescan.showreel import ( +from mediahive.hivescan.showreel import ( get_expected_episode_reel_path, get_expected_showreel_paths, ) -from hivescan.structs import ( +from mediahive.hivescan.structs import ( Episode, EpisodeRelease, Movie, @@ -21,20 +21,20 @@ from hivescan.structs import ( TMDbInfo, TMDbSeasonInfo, ) -from hivescan.tmdb_client import ( +from mediahive.hivescan.tmdb_client import ( fetch_movie_info, fetch_series_info, fetch_season_details, ) -from hivescan.models import ContentType, ParsedContent -from hivescan.scanning import find_cover_image, find_episode_files, find_playable_file -from hivescan.images import ( +from mediahive.hivescan.models import ContentType, ParsedContent +from mediahive.hivescan.scanning import find_cover_image, find_episode_files, find_playable_file +from mediahive.hivescan.images import ( download_cover_image, download_backdrop_image, download_season_poster, ) -from hivescan.utils import ( +from mediahive.hivescan.utils import ( get_added_timestamp, get_directory_size, get_media_folder_path, @@ -739,3 +739,4 @@ async def _process_series( seasons=seasons_data, ) yield series, ep_reel_tasks + diff --git a/hivescan/models.py b/mediahive/hivescan/models.py similarity index 99% rename from hivescan/models.py rename to mediahive/hivescan/models.py index ced825c..b313c84 100644 --- a/hivescan/models.py +++ b/mediahive/hivescan/models.py @@ -51,3 +51,4 @@ class ParsedContent: is_directory: bool = False raw_parsed: dict = field(default_factory=dict) content_hash: Optional[ContentHash] = None + diff --git a/hivescan/parsing.py b/mediahive/hivescan/parsing.py similarity index 96% rename from hivescan/parsing.py rename to mediahive/hivescan/parsing.py index 7621769..9fcec83 100644 --- a/hivescan/parsing.py +++ b/mediahive/hivescan/parsing.py @@ -7,7 +7,7 @@ from typing import Optional, Tuple import PTN from aiopathlib import AsyncPath -from hivescan.models import ContentHash, ContentType, ParsedContent +from mediahive.hivescan.models import ContentHash, ContentType, ParsedContent def determine_content_type(parsed: dict) -> ContentType: @@ -78,3 +78,4 @@ def parse_episode_from_filename(filename: str) -> Optional[Tuple[int, int]]: return int(match.group(1)), int(match.group(2)) return None + diff --git a/hivescan/scanning.py b/mediahive/hivescan/scanning.py similarity index 96% rename from hivescan/scanning.py rename to mediahive/hivescan/scanning.py index 9b34226..7979f9f 100644 --- a/hivescan/scanning.py +++ b/mediahive/hivescan/scanning.py @@ -7,9 +7,9 @@ from typing import Dict, List, Optional, Tuple from aiopathlib import AsyncPath -from hivescan.models import ContentType, ParsedContent -from hivescan.parsing import parse_download, parse_episode_from_filename -from hivescan.utils import get_media_folder_path, sanitize_filename +from mediahive.hivescan.models import ContentType, ParsedContent +from mediahive.hivescan.parsing import parse_download, parse_episode_from_filename +from mediahive.hivescan.utils import get_media_folder_path, sanitize_filename # Video file extensions @@ -205,3 +205,4 @@ async def find_cover_image( return str(legacy_path) return None + diff --git a/hivescan/server.py b/mediahive/hivescan/server.py similarity index 95% rename from hivescan/server.py rename to mediahive/hivescan/server.py index 3890b91..c4dc349 100644 --- a/hivescan/server.py +++ b/mediahive/hivescan/server.py @@ -24,20 +24,20 @@ from fastapi.middleware.cors import CORSMiddleware import msgspec from aiopathlib import AsyncPath -from hivescan.index_store import IndexStore -from hivescan.indexer import _process_movies, _process_series -from hivescan.structs import MsgspecResponse, ScanRequest, StatusResponse, TaskInfo -from hivescan.models import ContentType, ParsedContent -from hivescan.parsing import parse_download -from hivescan.scanning import categorize_downloads -from hivescan.showreel import ( +from mediahive.hivescan.index_store import IndexStore +from mediahive.hivescan.indexer import _process_movies, _process_series +from mediahive.hivescan.structs import MsgspecResponse, ScanRequest, StatusResponse, TaskInfo +from mediahive.hivescan.models import ContentType, ParsedContent +from mediahive.hivescan.parsing import parse_download +from mediahive.hivescan.scanning import categorize_downloads +from mediahive.hivescan.showreel import ( generate_episode_reel, generate_showreel_images, episode_reel_exists, movie_showreels_exist, ) -from hivescan.tmdb_client import set_cache_dir -from hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root, make_relative_path +from mediahive.hivescan.tmdb_client import set_cache_dir +from mediahive.hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root, make_relative_path logger = logging.getLogger("hivescan.server") @@ -45,7 +45,7 @@ logger = logging.getLogger("hivescan.server") # Configuration (from environment) # --------------------------------------------------------------------------- -SCAN_PATHS: List[str] = [] # set in lifespan from HIVESCAN_PATHS +SCAN_PATHS: List[str] = [] # set in lifespan from mediahive.hivescan_PATHS OUTPUT_DIR: Optional[Path] = None # .mediahive folder MEDIA_ROOT: Optional[Path] = None # parent of OUTPUT_DIR @@ -409,7 +409,7 @@ async def _showreel_worker(): # Update the movie item with generated showreel paths if item_id in store.movies: movie = store.movies[item_id] - from hivescan.showreel import get_expected_showreel_paths + from mediahive.hivescan.showreel import get_expected_showreel_paths media_root_path = ( Path(store.media_root) if store.media_root else None @@ -494,3 +494,4 @@ def run(host: str = "0.0.0.0", port: int = 8421): datefmt="%H:%M:%S", ) uvicorn.run(app, host=host, port=port, log_level="info") + diff --git a/hivescan/showreel.py b/mediahive/hivescan/showreel.py similarity index 99% rename from hivescan/showreel.py rename to mediahive/hivescan/showreel.py index 3210a6a..871570c 100644 --- a/hivescan/showreel.py +++ b/mediahive/hivescan/showreel.py @@ -811,3 +811,4 @@ async def generate_episode_reel( episode_code = f"S{season_num:02d}E{episode_num:02d}" logger.error("Error generating episode reel for %s: %s", episode_code, e) return None + diff --git a/hivescan/structs.py b/mediahive/hivescan/structs.py similarity index 99% rename from hivescan/structs.py rename to mediahive/hivescan/structs.py index bede7d0..778f70b 100644 --- a/hivescan/structs.py +++ b/mediahive/hivescan/structs.py @@ -334,3 +334,4 @@ class MsgspecResponse(Response): def render(self, content: object) -> bytes: return msgspec.json.encode(content) + diff --git a/hivescan/tmdb_client.py b/mediahive/hivescan/tmdb_client.py similarity index 99% rename from hivescan/tmdb_client.py rename to mediahive/hivescan/tmdb_client.py index 4aac91d..fdd8cec 100644 --- a/hivescan/tmdb_client.py +++ b/mediahive/hivescan/tmdb_client.py @@ -15,7 +15,7 @@ from typing import Dict, Optional import httpx from aiopathlib import AsyncPath -from hivescan.structs import ( +from mediahive.hivescan.structs import ( CastMember, SimilarMedia, TMDbEpisodeInfo, @@ -101,7 +101,7 @@ async def _save_to_cache(cache_path: Path, data: Optional[Dict]): pass # Cache write failures are not critical -# TMDbEpisodeInfo, TMDbSeasonInfo, TMDbInfo imported from hivescan.structs +# TMDbEpisodeInfo, TMDbSeasonInfo, TMDbInfo imported from mediahive.hivescan.structs async def tmdb_api_request( @@ -562,3 +562,4 @@ async def fetch_series_info(title: str) -> Optional[TMDbInfo]: number_of_episodes=details.get("number_of_episodes"), networks=networks if networks else None, ) + diff --git a/hivescan/utils.py b/mediahive/hivescan/utils.py similarity index 99% rename from hivescan/utils.py rename to mediahive/hivescan/utils.py index d78750a..b668da4 100644 --- a/hivescan/utils.py +++ b/mediahive/hivescan/utils.py @@ -194,3 +194,4 @@ def sort_by_quality(items: list, reverse: bool = True) -> None: ), reverse=reverse, ) + diff --git a/mediahive/server.py b/mediahive/server.py index 458998c..b25eba5 100644 --- a/mediahive/server.py +++ b/mediahive/server.py @@ -18,7 +18,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, StreamingResponse from fastapi_vue import Frontend -from hivescan.structs import PlayMediaRequest, OpenFolderRequest +from mediahive.hivescan.structs import PlayMediaRequest, OpenFolderRequest from mediahive.__main__ import DEVMODE @@ -262,3 +262,4 @@ async def serve_media_file(file_path: str): # Serve the Vue frontend (needs to be last if SPA catch-all is used) frontend.route(app, "/") + diff --git a/pyproject.toml b/pyproject.toml index 93e319f..4a88007 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,14 +18,13 @@ dependencies = [ [project.scripts] mediahive = "mediahive.__main__:main" -hivescan = "hivescan.__main__:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build] -packages = ["mediahive", "hivescan"] +packages = ["mediahive"] artifacts = ["mediahive/frontend-build"] only-packages = true