Restructure to mediahive.hivescan.
This commit is contained in:
@@ -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 +1,2 @@
|
||||
"""MediaHive - Media Browser Server"""
|
||||
|
||||
|
||||
+79
-25
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user