Restructure into backend/ and frontend/ subprojects
- backend/ uses proper Python src layout (src/media_library_viewer_api/) with pyproject.toml, hatchling build, and PYTHONPATH=src convention - frontend/ is a Vite + React + TypeScript SPA - archive/ preserves the original Streamlit prototype for reference - Cleaned up root to only contain docs, license, and subproject dirs - Updated README for the new dual-subproject architecture
This commit is contained in:
+41
-8
@@ -2,6 +2,38 @@
|
||||
|
||||
FastAPI backend serving the REST API for Jellyfin media browsing, SSH file inspection, and server monitoring.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── src/
|
||||
│ └── media_library_viewer_api/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI app entrypoint
|
||||
│ ├── config.py # pydantic-settings config
|
||||
│ ├── dependencies.py # Dependency injection
|
||||
│ ├── path_utils.py # Jellyfin→SSH path resolution
|
||||
│ ├── jobs.py # Job templates
|
||||
│ ├── utils.py # Formatting helpers
|
||||
│ ├── routers/
|
||||
│ │ ├── dashboard.py
|
||||
│ │ ├── monitoring.py
|
||||
│ │ ├── media.py
|
||||
│ │ ├── files.py
|
||||
│ │ └── jobs.py
|
||||
│ ├── clients/
|
||||
│ │ ├── jellyfin.py
|
||||
│ │ ├── resources.py
|
||||
│ │ └── ssh.py
|
||||
│ ├── domain/
|
||||
│ │ └── media.py
|
||||
│ └── services/
|
||||
│ └── media_index.py
|
||||
└── tests/
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
@@ -11,12 +43,6 @@ source .venv/bin/activate
|
||||
pip install -e '.[dev]'
|
||||
```
|
||||
|
||||
Or install dependencies directly:
|
||||
|
||||
```bash
|
||||
pip install fastapi uvicorn[standard] pydantic-settings paramiko requests python-dotenv pandas
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `.env` file in the project root (or set environment variables):
|
||||
@@ -40,7 +66,13 @@ REMOTE_PATH_PREFIX=
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uvicorn main:app --reload --port 8000
|
||||
uvicorn media_library_viewer_api.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
Or with PYTHONPATH if not installed:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src uvicorn media_library_viewer_api.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
API docs available at: http://localhost:8000/docs
|
||||
@@ -51,9 +83,10 @@ API docs available at: http://localhost:8000/docs
|
||||
- `GET /api/dashboard/libraries` — Per-library breakdown
|
||||
- `GET /api/dashboard/now-playing` — Active playback sessions
|
||||
- `GET /api/monitoring/status` — Collector status
|
||||
- `GET /api/monitoring/metrics` — Resource samples
|
||||
- `GET /api/monitoring/metrics` — Resource samples (last hour)
|
||||
- `GET /api/monitoring/disk` — Disk space
|
||||
- `POST /api/monitoring/start|stop|restart` — Collector controls
|
||||
- `GET /api/monitoring/diagnostics` — Collector debug info
|
||||
- `GET /api/media/status` — Index status
|
||||
- `POST /api/media/build` — Rebuild index
|
||||
- `GET /api/media/query` — Query with filters/sort/pagination
|
||||
|
||||
@@ -16,9 +16,21 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = ["httpx", "pytest", "pytest-asyncio", "ruff"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/media_library_viewer_api"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py311"
|
||||
src = ["src"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "W"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import shlex
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from clients.ssh import RemoteSSHClient
|
||||
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||
|
||||
# POSIX shell script copied to the remote server by start_resource_collector().
|
||||
# Keep this script bash-free because many NAS/media servers have minimal shells.
|
||||
@@ -8,9 +8,9 @@ from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from clients.jellyfin import JellyfinClient
|
||||
from clients.ssh import RemoteSSHClient
|
||||
from config import get_settings
|
||||
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||
from media_library_viewer_api.config import get_settings
|
||||
|
||||
|
||||
@lru_cache
|
||||
@@ -11,7 +11,7 @@ from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from utils import human_size, ticks_to_minutes
|
||||
from media_library_viewer_api.utils import human_size, ticks_to_minutes
|
||||
|
||||
|
||||
def first_media_source(item: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -11,7 +11,7 @@ import shlex
|
||||
from dataclasses import dataclass
|
||||
from typing import Mapping
|
||||
|
||||
from clients.ssh import CommandResult, RemoteSSHClient
|
||||
from media_library_viewer_api.clients.ssh import CommandResult, RemoteSSHClient
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -7,7 +7,7 @@ from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from routers import dashboard, monitoring, media, files, jobs
|
||||
from media_library_viewer_api.routers import dashboard, monitoring, media, files, jobs
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
+2
-2
@@ -6,8 +6,8 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from dependencies import get_jellyfin_client, get_user_id
|
||||
from clients.jellyfin import JellyfinClient
|
||||
from media_library_viewer_api.dependencies import get_jellyfin_client, get_user_id
|
||||
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||
|
||||
@@ -7,10 +7,10 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
|
||||
from dependencies import get_ssh_client
|
||||
from clients.ssh import RemoteSSHClient
|
||||
from config import get_settings
|
||||
from path_utils import resolve_remote_media_path
|
||||
from media_library_viewer_api.dependencies import get_ssh_client
|
||||
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||
from media_library_viewer_api.config import get_settings
|
||||
from media_library_viewer_api.path_utils import resolve_remote_media_path
|
||||
|
||||
router = APIRouter(prefix="/api/files", tags=["files"])
|
||||
|
||||
@@ -7,9 +7,9 @@ from typing import Any
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from dependencies import get_ssh_client
|
||||
from clients.ssh import RemoteSSHClient
|
||||
from jobs import JOB_TEMPLATES, run_job
|
||||
from media_library_viewer_api.dependencies import get_ssh_client
|
||||
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||
from media_library_viewer_api.jobs import JOB_TEMPLATES, run_job
|
||||
|
||||
router = APIRouter(prefix="/api/jobs", tags=["jobs"])
|
||||
|
||||
@@ -6,9 +6,9 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from dependencies import get_jellyfin_client, get_user_id
|
||||
from clients.jellyfin import JellyfinClient
|
||||
from services.media_index import MediaIndex, build_media_index
|
||||
from media_library_viewer_api.dependencies import get_jellyfin_client, get_user_id
|
||||
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||
from media_library_viewer_api.services.media_index import MediaIndex, build_media_index
|
||||
|
||||
router = APIRouter(prefix="/api/media", tags=["media"])
|
||||
|
||||
+4
-4
@@ -7,9 +7,9 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from dependencies import get_ssh_client
|
||||
from clients.ssh import RemoteSSHClient
|
||||
from clients.resources import (
|
||||
from media_library_viewer_api.dependencies import get_ssh_client
|
||||
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||
from media_library_viewer_api.clients.resources import (
|
||||
disk_space,
|
||||
read_resource_metrics,
|
||||
resource_collector_debug_info,
|
||||
@@ -18,7 +18,7 @@ from clients.resources import (
|
||||
start_resource_collector,
|
||||
stop_resource_collector,
|
||||
)
|
||||
from config import get_settings
|
||||
from media_library_viewer_api.config import get_settings
|
||||
|
||||
router = APIRouter(prefix="/api/monitoring", tags=["monitoring"])
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from clients.jellyfin import JellyfinClient
|
||||
from domain.media import display_media_row, normalize_media_item
|
||||
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||
from media_library_viewer_api.domain.media import display_media_row, normalize_media_item
|
||||
|
||||
# Local generated database. It is ignored by git and can be rebuilt from
|
||||
# Jellyfin metadata whenever needed.
|
||||
Reference in New Issue
Block a user