Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22454f2d29 | ||
|
|
589c789d4d | ||
|
|
5f2454d8e8 | ||
|
|
2fa15132fb | ||
|
|
76cd0224de | ||
|
|
b0d13a67a0 |
@@ -6,15 +6,19 @@ Netflix style browsing of your local media archive. Supports keyboard, mouse and
|
||||
|
||||
**[Windows and Mac portable ZIP downloads](https://git.zi.fi/LeoVasanko/mediahive/releases)**
|
||||
|
||||
## Getting Started
|
||||
|
||||
- Windows and macOS: Download the portable ZIP from the releases page, extract it anywhere, and run `MediaHive`.
|
||||
- Linux and other platforms: Install [UV](https://docs.astral.sh/uv/getting-started/installation/) and run directly with `uvx --from mediahive[gui] mediahive`.
|
||||
|
||||
## What It Does
|
||||
|
||||
- Scans your chosen media folder for all movies and series that can be found
|
||||
- Produces preview video clips and downloads metadata
|
||||
- Search on cast and character names, not just titles
|
||||
- Search on names and other metadata, not just titles
|
||||
- Hand off playback to your preferred system player
|
||||
- Implement gamepad controls for MPC-BE on Windows (where needed)
|
||||
|
||||
Extract the ZIP in some place and run MediaHive.exe to start the app. Currently we have no installer, but you can pin to start/taskbar for easier access. On the first startup the app asks for your media folder, that can later be changed by clicking in-app folder icon.
|
||||
Windows and macOS builds are currently portable-only (no installer). On first startup, the app asks for your media folder, which you can later change using the in-app folder icon.
|
||||
|
||||
Note that `.mediahive` folder is created in your media folder to hold all the metadata and preview clips, avoiding the lengthy processing that you will see on initial startup.
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Development
|
||||
|
||||
This document covers the developer-facing ways to run MediaHive locally. The main [README.md](../README.md) is aimed at Windows end users.
|
||||
This document covers the developer-facing ways to run MediaHive locally. The main [README.md](../README.md) covers end-user startup across platforms (portable ZIPs on Windows/macOS, `uvx --from mediahive[gui] mediahive` on Linux/other).
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -54,9 +54,30 @@ def main() -> None:
|
||||
action="append",
|
||||
help=(f"Endpoint (default: localhost:{DEFAULT_PORT})."),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gui",
|
||||
action="store_true",
|
||||
help="Run with GUI (fails if GUI dependencies are not installed)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# --listen implies server-only mode; use --gui to force GUI even with --listen.
|
||||
use_gui = args.gui or not args.listen
|
||||
|
||||
if use_gui:
|
||||
try:
|
||||
from mediahive.winmain import winmain
|
||||
except ImportError as exc:
|
||||
if args.gui:
|
||||
raise RuntimeError(
|
||||
"GUI dependencies are not installed. "
|
||||
"Install with: uv pip install mediahive[gui]"
|
||||
) from exc
|
||||
else:
|
||||
winmain()
|
||||
return
|
||||
|
||||
if args.media_folders:
|
||||
roots: dict[str, str] = {}
|
||||
for path in args.media_folders:
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import msgspec
|
||||
import msgspec.toml
|
||||
|
||||
|
||||
class Config(msgspec.Struct):
|
||||
class Config(msgspec.Struct, omit_defaults=True):
|
||||
media_folder: str | None = None
|
||||
roots: dict[str, str] | None = None
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
@@ -11,9 +10,6 @@ from aiopathlib import AsyncPath
|
||||
# Default output folder name (created at common root of scanned paths)
|
||||
DEFAULT_OUTPUT_FOLDER = ".mediahive"
|
||||
|
||||
# Threshold for considering atime "too close" to current time (1 hour)
|
||||
_ATIME_FRESHNESS_THRESHOLD = 3600
|
||||
|
||||
# Resolution priority for quality sorting (higher = better)
|
||||
RESOLUTION_PRIORITY = {
|
||||
"8K": 5,
|
||||
@@ -108,10 +104,9 @@ def normalize_resolution_label(value: str | None) -> str | None:
|
||||
async def get_added_timestamp(path: Path) -> int | None:
|
||||
"""Get the timestamp when a torrent was added to the collection.
|
||||
|
||||
Heuristic:
|
||||
- For directories: use ctime (most accurate for torrent folder creation)
|
||||
- For files: use atime unless it's too close to current time (suggesting
|
||||
the filesystem updates atime on reads), otherwise use max(mtime, ctime)
|
||||
Best-effort rule:
|
||||
- On Windows: use ctime (creation-time semantics)
|
||||
- On other OSes: use mtime (ctime is metadata-change time on Unix)
|
||||
|
||||
Returns:
|
||||
Unix timestamp as int, or None if path doesn't exist
|
||||
@@ -122,17 +117,9 @@ async def get_added_timestamp(path: Path) -> int | None:
|
||||
stat_info = await ap.stat()
|
||||
except OSError, PermissionError:
|
||||
return None
|
||||
|
||||
if await ap.is_dir():
|
||||
if os.name == "nt":
|
||||
return int(stat_info.st_ctime)
|
||||
|
||||
now = time.time()
|
||||
atime = stat_info.st_atime
|
||||
|
||||
if now - atime < _ATIME_FRESHNESS_THRESHOLD:
|
||||
return int(max(stat_info.st_mtime, stat_info.st_ctime))
|
||||
|
||||
return int(atime)
|
||||
return int(stat_info.st_mtime)
|
||||
|
||||
|
||||
def get_directory_size(path: Path) -> int:
|
||||
|
||||
@@ -893,7 +893,7 @@ def winmain() -> None:
|
||||
nargs="?",
|
||||
help="Path to the media folder (default: saved config or initial setup dialog)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
args, _unknown = parser.parse_known_args()
|
||||
|
||||
_prepend_meipass_to_path()
|
||||
|
||||
|
||||
+9
-2
@@ -38,6 +38,13 @@ only-packages = true
|
||||
[tool.hatch.build.targets.sdist.hooks.custom]
|
||||
path = "scripts/fastapi-vue/build-frontend.py"
|
||||
|
||||
[tool.hatch.build.targets.sdist.force-include]
|
||||
"scripts/fastapi-vue/build-frontend.py" = "scripts/fastapi-vue/build-frontend.py"
|
||||
"scripts/fastapi-vue/buildutil.py" = "scripts/fastapi-vue/buildutil.py"
|
||||
|
||||
[tool.hatch.build.targets.wheel.hooks.custom]
|
||||
path = "scripts/fastapi-vue/build-frontend.py"
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
|
||||
@@ -46,8 +53,8 @@ parse-torrent-title = { git = "https://github.com/platelminto/parse-torrent-titl
|
||||
|
||||
[project.optional-dependencies]
|
||||
gui = [
|
||||
"pywebview>=6.2.1; platform_system != 'Darwin'",
|
||||
"pywebview[qt5]>=6.2.1; platform_system == 'Darwin'",
|
||||
"pywebview[qt]>=6.2.1; platform_system != 'Windows'",
|
||||
"pywebview>=6.2.1; platform_system == 'Windows'",
|
||||
"qtpy>=2.4.1; platform_system == 'Darwin'",
|
||||
"PyQt5>=5.15.11; platform_system == 'Darwin'",
|
||||
"pythonnet>=3.1.0rc0; platform_system == 'Windows' and python_version >= '3.14'",
|
||||
|
||||
@@ -14,4 +14,21 @@ from buildutil import build
|
||||
class CustomBuildHook(BuildHookInterface):
|
||||
def initialize(self, version, build_data) -> None:
|
||||
super().initialize(version, build_data)
|
||||
build("frontend")
|
||||
root = Path(self.root)
|
||||
frontend_src = root / "frontend"
|
||||
frontend_build = root / "mediahive" / "frontend-build"
|
||||
|
||||
# When building a wheel from sdist, frontend sources may be omitted
|
||||
# while prebuilt assets are already present in mediahive/frontend-build.
|
||||
if frontend_src.exists():
|
||||
build(str(frontend_src))
|
||||
return
|
||||
|
||||
if frontend_build.exists():
|
||||
return
|
||||
|
||||
msg = (
|
||||
"Frontend build is missing. Expected either source directory "
|
||||
f"'{frontend_src}' or prebuilt assets in '{frontend_build}'."
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
Reference in New Issue
Block a user