Add FastAPI backend and React frontend subprojects
Backend: - FastAPI app with 17 REST endpoints covering dashboard, monitoring, media index, file browser, and jobs - Reuses existing clients/domain/services unchanged - pydantic-settings config, dependency injection, CORS setup - Auto-generated OpenAPI docs at /docs Frontend: - Vite + React + TypeScript SPA - @tanstack/react-query for data fetching with polling - ag-grid-react for media table and file browser - recharts for monitoring charts - Tailwind CSS styling - 4 pages: Dashboard, Monitoring, Media, File Browser - Typed API client matching all backend endpoints Also: - docs/MIGRATION_PLAN.md with full architecture plan - Updated .gitignore for both subprojects - Streamlit app preserved for now (can coexist)
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""Path resolution utilities for Jellyfin → SSH path mapping."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import posixpath
|
||||
|
||||
|
||||
def apply_remote_path_prefix(path: str, prefix: str) -> str:
|
||||
"""Apply an optional fallback prefix for Jellyfin->SSH path handoff."""
|
||||
if not path:
|
||||
return path
|
||||
normalized_prefix = (prefix or "").strip()
|
||||
if not normalized_prefix:
|
||||
return path
|
||||
normalized_prefix = normalized_prefix.rstrip("/")
|
||||
if path == normalized_prefix or path.startswith(normalized_prefix + "/"):
|
||||
return posixpath.normpath(path)
|
||||
if path.startswith("/"):
|
||||
return posixpath.normpath(normalized_prefix + path)
|
||||
return posixpath.normpath(posixpath.join(normalized_prefix, path))
|
||||
|
||||
|
||||
def map_path_to_media_root(path: str, media_root: str) -> str:
|
||||
"""Map a Jellyfin path to the configured SSH media root when possible.
|
||||
|
||||
If the final segment of media_root (e.g. 'media') appears in the Jellyfin path,
|
||||
the prefix up to that segment is replaced by media_root.
|
||||
"""
|
||||
if not path:
|
||||
return path
|
||||
normalized_root = (media_root or "").strip()
|
||||
if not normalized_root:
|
||||
return path
|
||||
normalized_root = posixpath.normpath(normalized_root)
|
||||
|
||||
raw_parts = [part for part in str(path).split("/") if part]
|
||||
if not raw_parts:
|
||||
return path
|
||||
|
||||
path_absolute = "/" + "/".join(raw_parts)
|
||||
if path_absolute == normalized_root or path_absolute.startswith(normalized_root + "/"):
|
||||
return path_absolute
|
||||
|
||||
root_anchor = posixpath.basename(normalized_root)
|
||||
if not root_anchor:
|
||||
return path
|
||||
|
||||
if root_anchor in raw_parts:
|
||||
anchor_index = raw_parts.index(root_anchor)
|
||||
remainder_parts = raw_parts[anchor_index + 1:]
|
||||
return posixpath.join(normalized_root, *remainder_parts) if remainder_parts else normalized_root
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def resolve_remote_media_path(path: str, media_root: str, fallback_prefix: str) -> str:
|
||||
"""Resolve Jellyfin paths to SSH-visible paths.
|
||||
|
||||
Strategy:
|
||||
1. Prefer mapping to media_root when it can anchor on the root basename.
|
||||
2. If no mapping happened, apply optional fallback prefix.
|
||||
"""
|
||||
if not path:
|
||||
return path
|
||||
mapped = map_path_to_media_root(path, media_root)
|
||||
if mapped and mapped != path:
|
||||
return mapped
|
||||
return apply_remote_path_prefix(mapped or path, fallback_prefix)
|
||||
Reference in New Issue
Block a user