Restructure to mediahive.hivescan.

This commit is contained in:
2026-02-09 03:29:03 +00:00
parent e897dfcf9c
commit f65c4ff3de
17 changed files with 131 additions and 125 deletions
-61
View File
@@ -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()
+1
View File
@@ -1 +1,2 @@
"""MediaHive - Media Browser Server""" """MediaHive - Media Browser Server"""
+79 -25
View File
@@ -9,42 +9,96 @@ DEVMODE = bool(os.getenv("MEDIAHIVE_FRONTEND_URL"))
def main(): def main():
parser = argparse.ArgumentParser(description="Run the mediahive server.") parser = argparse.ArgumentParser(description="MediaHive - Media scanning, indexing, and streaming")
parser.add_argument( 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", "media_folder",
nargs="?", nargs="?",
help="Path to the media folder (default: MEDIAHIVE_PATH or current directory)", help="Path to the media folder (default: MEDIAHIVE_PATH or current directory)",
) )
parser.add_argument( server_parser.add_argument(
"-l", "-l",
"--listen", "--listen",
action="append", action="append",
help=(f"Endpoint (default: localhost:{DEFAULT_PORT})."), 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() args = parser.parse_args()
# Determine media folder if args.command == 'server':
match Path( # Determine media folder
args.media_folder or os.environ.get("MEDIAHIVE_PATH") or Path.cwd() match Path(
).parts: args.media_folder or os.environ.get("MEDIAHIVE_PATH") or Path.cwd()
case (*rest, ".mediahive", "index.json"): ).parts:
... case (*rest, ".mediahive", "index.json"):
case (*rest, ".mediahive"): ...
... case (*rest, ".mediahive"):
case rest: ...
... case rest:
mediaroot = Path(*rest).resolve() ...
if not mediaroot.exists() or not mediaroot.is_dir(): mediaroot = Path(*rest).resolve()
print(f"Error: Folder does not exist: {mediaroot}") if not mediaroot.exists() or not mediaroot.is_dir():
exit(1) print(f"Error: Folder does not exist: {mediaroot}")
os.environ["MEDIAHIVE_PATH"] = mediaroot.as_posix() exit(1)
dev = {"reload": True, "reload_dirs": ["mediahive"]} os.environ["MEDIAHIVE_PATH"] = mediaroot.as_posix()
server.run( dev = {"reload": True, "reload_dirs": ["mediahive"]}
"mediahive.server:app", server.run(
listen=args.listen, "mediahive.server:app",
default_port=DEFAULT_PORT, listen=args.listen,
**(dev if DEVMODE else {}), 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__": if __name__ == "__main__":
@@ -6,22 +6,22 @@ Usage:
hivescan /path/* --port 9000 # Custom port hivescan /path/* --port 9000 # Custom port
Or as a library: Or as a library:
from hivescan.index_store import IndexStore from mediahive.hivescan.index_store import IndexStore
from hivescan.structs import Movie, Series, TaskInfo from mediahive.hivescan.structs import Movie, Series, TaskInfo
from hivescan.server import app from mediahive.hivescan.server import app
""" """
from hivescan.models import ContentType, ContentHash, ParsedContent from mediahive.hivescan.models import ContentType, ContentHash, ParsedContent
from hivescan.scanning import ( from mediahive.hivescan.scanning import (
scan_downloads, scan_downloads,
categorize_downloads, categorize_downloads,
find_playable_file, find_playable_file,
find_episode_files, find_episode_files,
) )
from hivescan.index_store import IndexStore from mediahive.hivescan.index_store import IndexStore
from hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root from mediahive.hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root
from hivescan.showreel import generate_showreel_images, generate_episode_reel from mediahive.hivescan.showreel import generate_showreel_images, generate_episode_reel
from hivescan.structs import ( from mediahive.hivescan.structs import (
CastMember, CastMember,
Episode, Episode,
EpisodeRelease, EpisodeRelease,
@@ -37,7 +37,7 @@ from hivescan.structs import (
TMDbInfo, TMDbInfo,
TMDbSeasonInfo, TMDbSeasonInfo,
) )
from hivescan.tmdb_client import ( from mediahive.hivescan.tmdb_client import (
fetch_movie_info, fetch_movie_info,
fetch_series_info, fetch_series_info,
fetch_season_details, fetch_season_details,
@@ -83,3 +83,4 @@ __all__ = [
"DEFAULT_OUTPUT_FOLDER", "DEFAULT_OUTPUT_FOLDER",
"find_common_root", "find_common_root",
] ]
@@ -6,7 +6,7 @@ from typing import Optional
from aiopathlib import AsyncPath from aiopathlib import AsyncPath
from hivescan.utils import get_media_folder_path from mediahive.hivescan.utils import get_media_folder_path
# TMDb image configuration # TMDb image configuration
@@ -113,3 +113,4 @@ async def download_season_poster(
await AsyncPath(media_folder).mkdir(parents=True, exist_ok=True) await AsyncPath(media_folder).mkdir(parents=True, exist_ok=True)
url = f"{TMDB_IMAGE_BASE}/{DEFAULT_POSTER_SIZE}{poster_path}" url = f"{TMDB_IMAGE_BASE}/{DEFAULT_POSTER_SIZE}{poster_path}"
return await _download_image(url, output_path, f"season {season_num} poster") return await _download_image(url, output_path, f"season {season_num} poster")
@@ -18,7 +18,7 @@ import msgspec
from aiopathlib import AsyncPath from aiopathlib import AsyncPath
from fastapi import WebSocket from fastapi import WebSocket
from hivescan.structs import ( from mediahive.hivescan.structs import (
IndexSnapshot, IndexSnapshot,
MediaStats, MediaStats,
Movie, Movie,
@@ -250,3 +250,4 @@ class IndexStore:
movies=movies_list, movies=movies_list,
series=series_list, series=series_list,
) )
@@ -6,11 +6,11 @@ import logging
from pathlib import Path from pathlib import Path
from typing import AsyncIterator, Dict, List, Optional, Tuple from typing import AsyncIterator, Dict, List, Optional, Tuple
from hivescan.showreel import ( from mediahive.hivescan.showreel import (
get_expected_episode_reel_path, get_expected_episode_reel_path,
get_expected_showreel_paths, get_expected_showreel_paths,
) )
from hivescan.structs import ( from mediahive.hivescan.structs import (
Episode, Episode,
EpisodeRelease, EpisodeRelease,
Movie, Movie,
@@ -21,20 +21,20 @@ from hivescan.structs import (
TMDbInfo, TMDbInfo,
TMDbSeasonInfo, TMDbSeasonInfo,
) )
from hivescan.tmdb_client import ( from mediahive.hivescan.tmdb_client import (
fetch_movie_info, fetch_movie_info,
fetch_series_info, fetch_series_info,
fetch_season_details, fetch_season_details,
) )
from hivescan.models import ContentType, ParsedContent from mediahive.hivescan.models import ContentType, ParsedContent
from hivescan.scanning import find_cover_image, find_episode_files, find_playable_file from mediahive.hivescan.scanning import find_cover_image, find_episode_files, find_playable_file
from hivescan.images import ( from mediahive.hivescan.images import (
download_cover_image, download_cover_image,
download_backdrop_image, download_backdrop_image,
download_season_poster, download_season_poster,
) )
from hivescan.utils import ( from mediahive.hivescan.utils import (
get_added_timestamp, get_added_timestamp,
get_directory_size, get_directory_size,
get_media_folder_path, get_media_folder_path,
@@ -739,3 +739,4 @@ async def _process_series(
seasons=seasons_data, seasons=seasons_data,
) )
yield series, ep_reel_tasks yield series, ep_reel_tasks
@@ -51,3 +51,4 @@ class ParsedContent:
is_directory: bool = False is_directory: bool = False
raw_parsed: dict = field(default_factory=dict) raw_parsed: dict = field(default_factory=dict)
content_hash: Optional[ContentHash] = None content_hash: Optional[ContentHash] = None
@@ -7,7 +7,7 @@ from typing import Optional, Tuple
import PTN import PTN
from aiopathlib import AsyncPath 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: 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 int(match.group(1)), int(match.group(2))
return None return None
@@ -7,9 +7,9 @@ from typing import Dict, List, Optional, Tuple
from aiopathlib import AsyncPath from aiopathlib import AsyncPath
from hivescan.models import ContentType, ParsedContent from mediahive.hivescan.models import ContentType, ParsedContent
from hivescan.parsing import parse_download, parse_episode_from_filename from mediahive.hivescan.parsing import parse_download, parse_episode_from_filename
from hivescan.utils import get_media_folder_path, sanitize_filename from mediahive.hivescan.utils import get_media_folder_path, sanitize_filename
# Video file extensions # Video file extensions
@@ -205,3 +205,4 @@ async def find_cover_image(
return str(legacy_path) return str(legacy_path)
return None return None
@@ -24,20 +24,20 @@ from fastapi.middleware.cors import CORSMiddleware
import msgspec import msgspec
from aiopathlib import AsyncPath from aiopathlib import AsyncPath
from hivescan.index_store import IndexStore from mediahive.hivescan.index_store import IndexStore
from hivescan.indexer import _process_movies, _process_series from mediahive.hivescan.indexer import _process_movies, _process_series
from hivescan.structs import MsgspecResponse, ScanRequest, StatusResponse, TaskInfo from mediahive.hivescan.structs import MsgspecResponse, ScanRequest, StatusResponse, TaskInfo
from hivescan.models import ContentType, ParsedContent from mediahive.hivescan.models import ContentType, ParsedContent
from hivescan.parsing import parse_download from mediahive.hivescan.parsing import parse_download
from hivescan.scanning import categorize_downloads from mediahive.hivescan.scanning import categorize_downloads
from hivescan.showreel import ( from mediahive.hivescan.showreel import (
generate_episode_reel, generate_episode_reel,
generate_showreel_images, generate_showreel_images,
episode_reel_exists, episode_reel_exists,
movie_showreels_exist, movie_showreels_exist,
) )
from hivescan.tmdb_client import set_cache_dir from mediahive.hivescan.tmdb_client import set_cache_dir
from hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root, make_relative_path from mediahive.hivescan.utils import DEFAULT_OUTPUT_FOLDER, find_common_root, make_relative_path
logger = logging.getLogger("hivescan.server") logger = logging.getLogger("hivescan.server")
@@ -45,7 +45,7 @@ logger = logging.getLogger("hivescan.server")
# Configuration (from environment) # 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 OUTPUT_DIR: Optional[Path] = None # .mediahive folder
MEDIA_ROOT: Optional[Path] = None # parent of OUTPUT_DIR 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 # Update the movie item with generated showreel paths
if item_id in store.movies: if item_id in store.movies:
movie = store.movies[item_id] 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 = ( media_root_path = (
Path(store.media_root) if store.media_root else None 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", datefmt="%H:%M:%S",
) )
uvicorn.run(app, host=host, port=port, log_level="info") uvicorn.run(app, host=host, port=port, log_level="info")
@@ -811,3 +811,4 @@ async def generate_episode_reel(
episode_code = f"S{season_num:02d}E{episode_num:02d}" episode_code = f"S{season_num:02d}E{episode_num:02d}"
logger.error("Error generating episode reel for %s: %s", episode_code, e) logger.error("Error generating episode reel for %s: %s", episode_code, e)
return None return None
@@ -334,3 +334,4 @@ class MsgspecResponse(Response):
def render(self, content: object) -> bytes: def render(self, content: object) -> bytes:
return msgspec.json.encode(content) return msgspec.json.encode(content)
@@ -15,7 +15,7 @@ from typing import Dict, Optional
import httpx import httpx
from aiopathlib import AsyncPath from aiopathlib import AsyncPath
from hivescan.structs import ( from mediahive.hivescan.structs import (
CastMember, CastMember,
SimilarMedia, SimilarMedia,
TMDbEpisodeInfo, TMDbEpisodeInfo,
@@ -101,7 +101,7 @@ async def _save_to_cache(cache_path: Path, data: Optional[Dict]):
pass # Cache write failures are not critical 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( 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"), number_of_episodes=details.get("number_of_episodes"),
networks=networks if networks else None, networks=networks if networks else None,
) )
@@ -194,3 +194,4 @@ def sort_by_quality(items: list, reverse: bool = True) -> None:
), ),
reverse=reverse, reverse=reverse,
) )
+2 -1
View File
@@ -18,7 +18,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from fastapi_vue import Frontend from fastapi_vue import Frontend
from hivescan.structs import PlayMediaRequest, OpenFolderRequest from mediahive.hivescan.structs import PlayMediaRequest, OpenFolderRequest
from mediahive.__main__ import DEVMODE 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) # Serve the Vue frontend (needs to be last if SPA catch-all is used)
frontend.route(app, "/") frontend.route(app, "/")
+1 -2
View File
@@ -18,14 +18,13 @@ dependencies = [
[project.scripts] [project.scripts]
mediahive = "mediahive.__main__:main" mediahive = "mediahive.__main__:main"
hivescan = "hivescan.__main__:main"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build] [tool.hatch.build]
packages = ["mediahive", "hivescan"] packages = ["mediahive"]
artifacts = ["mediahive/frontend-build"] artifacts = ["mediahive/frontend-build"]
only-packages = true only-packages = true