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"""
+79 -25
View File
@@ -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__":
@@ -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",
]
@@ -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")
@@ -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,
)
@@ -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
@@ -51,3 +51,4 @@ class ParsedContent:
is_directory: bool = False
raw_parsed: dict = field(default_factory=dict)
content_hash: Optional[ContentHash] = None
@@ -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
@@ -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
@@ -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")
@@ -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
@@ -334,3 +334,4 @@ class MsgspecResponse(Response):
def render(self, content: object) -> bytes:
return msgspec.json.encode(content)
@@ -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,
)
@@ -194,3 +194,4 @@ def sort_by_quality(items: list, reverse: bool = True) -> None:
),
reverse=reverse,
)
+2 -1
View File
@@ -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, "/")
+1 -2
View File
@@ -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