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)**
|
**[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
|
## What It Does
|
||||||
|
|
||||||
- Scans your chosen media folder for all movies and series that can be found
|
- Scans your chosen media folder for all movies and series that can be found
|
||||||
- Produces preview video clips and downloads metadata
|
- 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
|
- 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.
|
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
|
# 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
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,30 @@ def main() -> None:
|
|||||||
action="append",
|
action="append",
|
||||||
help=(f"Endpoint (default: localhost:{DEFAULT_PORT})."),
|
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()
|
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:
|
if args.media_folders:
|
||||||
roots: dict[str, str] = {}
|
roots: dict[str, str] = {}
|
||||||
for path in args.media_folders:
|
for path in args.media_folders:
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ import msgspec
|
|||||||
import msgspec.toml
|
import msgspec.toml
|
||||||
|
|
||||||
|
|
||||||
class Config(msgspec.Struct):
|
class Config(msgspec.Struct, omit_defaults=True):
|
||||||
media_folder: str | None = None
|
media_folder: str | None = None
|
||||||
roots: dict[str, str] | None = None
|
roots: dict[str, str] | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from pathlib import Path
|
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 name (created at common root of scanned paths)
|
||||||
DEFAULT_OUTPUT_FOLDER = ".mediahive"
|
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 for quality sorting (higher = better)
|
||||||
RESOLUTION_PRIORITY = {
|
RESOLUTION_PRIORITY = {
|
||||||
"8K": 5,
|
"8K": 5,
|
||||||
@@ -108,10 +104,9 @@ def normalize_resolution_label(value: str | None) -> str | None:
|
|||||||
async def get_added_timestamp(path: Path) -> int | None:
|
async def get_added_timestamp(path: Path) -> int | None:
|
||||||
"""Get the timestamp when a torrent was added to the collection.
|
"""Get the timestamp when a torrent was added to the collection.
|
||||||
|
|
||||||
Heuristic:
|
Best-effort rule:
|
||||||
- For directories: use ctime (most accurate for torrent folder creation)
|
- On Windows: use ctime (creation-time semantics)
|
||||||
- For files: use atime unless it's too close to current time (suggesting
|
- On other OSes: use mtime (ctime is metadata-change time on Unix)
|
||||||
the filesystem updates atime on reads), otherwise use max(mtime, ctime)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Unix timestamp as int, or None if path doesn't exist
|
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()
|
stat_info = await ap.stat()
|
||||||
except OSError, PermissionError:
|
except OSError, PermissionError:
|
||||||
return None
|
return None
|
||||||
|
if os.name == "nt":
|
||||||
if await ap.is_dir():
|
|
||||||
return int(stat_info.st_ctime)
|
return int(stat_info.st_ctime)
|
||||||
|
return int(stat_info.st_mtime)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def get_directory_size(path: Path) -> int:
|
def get_directory_size(path: Path) -> int:
|
||||||
|
|||||||
@@ -893,7 +893,7 @@ def winmain() -> None:
|
|||||||
nargs="?",
|
nargs="?",
|
||||||
help="Path to the media folder (default: saved config or initial setup dialog)",
|
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()
|
_prepend_meipass_to_path()
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -38,6 +38,13 @@ only-packages = true
|
|||||||
[tool.hatch.build.targets.sdist.hooks.custom]
|
[tool.hatch.build.targets.sdist.hooks.custom]
|
||||||
path = "scripts/fastapi-vue/build-frontend.py"
|
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]
|
[tool.uv]
|
||||||
package = true
|
package = true
|
||||||
|
|
||||||
@@ -46,8 +53,8 @@ parse-torrent-title = { git = "https://github.com/platelminto/parse-torrent-titl
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
gui = [
|
gui = [
|
||||||
"pywebview>=6.2.1; platform_system != 'Darwin'",
|
"pywebview[qt]>=6.2.1; platform_system != 'Windows'",
|
||||||
"pywebview[qt5]>=6.2.1; platform_system == 'Darwin'",
|
"pywebview>=6.2.1; platform_system == 'Windows'",
|
||||||
"qtpy>=2.4.1; platform_system == 'Darwin'",
|
"qtpy>=2.4.1; platform_system == 'Darwin'",
|
||||||
"PyQt5>=5.15.11; platform_system == 'Darwin'",
|
"PyQt5>=5.15.11; platform_system == 'Darwin'",
|
||||||
"pythonnet>=3.1.0rc0; platform_system == 'Windows' and python_version >= '3.14'",
|
"pythonnet>=3.1.0rc0; platform_system == 'Windows' and python_version >= '3.14'",
|
||||||
|
|||||||
@@ -14,4 +14,21 @@ from buildutil import build
|
|||||||
class CustomBuildHook(BuildHookInterface):
|
class CustomBuildHook(BuildHookInterface):
|
||||||
def initialize(self, version, build_data) -> None:
|
def initialize(self, version, build_data) -> None:
|
||||||
super().initialize(version, build_data)
|
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