Playback moves to Kodi (standalone on GBM/KMS -- the smooth, hardware-decoded LibreELEC path). mediapi becomes a thin remote: it browses media and controls Kodi over its JSON-RPC HTTP API. This drops every mpv/DRM/X-mirror problem (DRM-master exclusivity, gpu-next "export failed" wedges, X-mirror A/V desync, software-decode choppiness) -- none of which had a working single config. - new mediapi/kodi.py (stdlib JSON-RPC client); player.py rewritten to poll and drive Kodi (Player.Open/PlayPause/Seek, Application.SetVolume, Player.GetProperties), same public interface + keep-playing auto-advance - config.py: KODI_* settings replace MPV_SOCKET; __init__ + api_routes updated - delete mpv_ipc.py, the mpv/X units, start-mpv/session scripts - new systemd/mediapi-kodi.service.template (standalone Kodi on tty1) + scripts/configure-kodi.py (headlessly enable Kodi's JSON-RPC web server) - install.sh now bootstraps a BARE Pi OS Lite end to end: apt base packages, installs uv, installs Kodi, enables its web API, configures the AP, installs + starts services (only git need be preinstalled). Auto-seeds .env from the example on first run. - README + .env.example updated for the Kodi architecture Dual HDMI is handled in hardware (external splitter off one port); no software mirror -- see git history for why that can't work smoothly on this Pi. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.1 KiB
Python
90 lines
3.1 KiB
Python
import logging
|
|
import os
|
|
from datetime import timedelta
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
INSTANCE_DIR = os.path.join(BASE_DIR, "instance")
|
|
SECRET_KEY_PATH = os.path.join(INSTANCE_DIR, "secret_key")
|
|
DOTENV_PATH = os.path.join(BASE_DIR, ".env")
|
|
|
|
|
|
def load_dotenv(path=DOTENV_PATH):
|
|
"""Minimal .env loader (no dependency). Populates os.environ for any key
|
|
not already set, so real environment variables always win over the file.
|
|
Works for both `uv run run.py` and the systemd-launched service."""
|
|
if not os.path.exists(path):
|
|
return
|
|
with open(path) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, val = line.partition("=")
|
|
key = key.strip()
|
|
val = val.strip().strip('"').strip("'")
|
|
if key:
|
|
os.environ.setdefault(key, val)
|
|
|
|
|
|
def require(name):
|
|
val = os.environ.get(name)
|
|
if not val:
|
|
raise RuntimeError(
|
|
f"{name} is not set -- copy .env.example to .env and fill it in"
|
|
)
|
|
return val
|
|
|
|
|
|
def load_secret_key():
|
|
"""Read the persisted session-signing key, generating it if missing.
|
|
|
|
On a read-only overlay filesystem the write will fail; in that case we
|
|
fall back to an ephemeral in-memory key so the app still starts (sessions
|
|
just won't survive a restart). install.sh generates this file while the
|
|
card is writable, so in normal operation the write path isn't hit."""
|
|
if os.path.exists(SECRET_KEY_PATH):
|
|
with open(SECRET_KEY_PATH) as f:
|
|
return f.read().strip()
|
|
|
|
key = os.urandom(32).hex()
|
|
try:
|
|
os.makedirs(INSTANCE_DIR, exist_ok=True)
|
|
with open(SECRET_KEY_PATH, "w") as f:
|
|
f.write(key)
|
|
except OSError:
|
|
log.warning(
|
|
"could not persist secret key (read-only filesystem?) -- using an "
|
|
"ephemeral key; logins won't survive an app restart"
|
|
)
|
|
return key
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
class Config:
|
|
SECRET_KEY = load_secret_key()
|
|
PERMANENT_SESSION_LIFETIME = timedelta(days=3650)
|
|
SESSION_COOKIE_SAMESITE = "Lax"
|
|
|
|
USERNAME = require("MEDIAPI_USERNAME")
|
|
PASSWORD = require("MEDIAPI_PASSWORD")
|
|
|
|
MEDIA_ROOTS = [
|
|
p for p in os.environ.get("MEDIAPI_MEDIA_ROOTS", "/localmedia").split(":") if p
|
|
]
|
|
# Kodi does the playback; we drive it over its JSON-RPC HTTP endpoint.
|
|
# Kodi's web server must be enabled (install.sh configures this) and should
|
|
# be on a different port than this app. Defaults match install.sh.
|
|
KODI_HOST = os.environ.get("MEDIAPI_KODI_HOST", "127.0.0.1")
|
|
KODI_PORT = int(os.environ.get("MEDIAPI_KODI_PORT", "8090"))
|
|
KODI_USER = os.environ.get("MEDIAPI_KODI_USER", "kodi")
|
|
KODI_PASSWORD = os.environ.get("MEDIAPI_KODI_PASSWORD", "kodi")
|
|
KODI_URL = f"http://{KODI_HOST}:{KODI_PORT}/jsonrpc"
|
|
PORT = int(os.environ.get("MEDIAPI_PORT", "8080"))
|
|
|
|
VIDEO_EXTENSIONS = {
|
|
".mp4", ".mkv", ".avi", ".mov", ".m4v", ".webm", ".mpg", ".mpeg", ".ts", ".flv", ".wmv",
|
|
}
|