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:
@@ -1,148 +1,100 @@
|
|||||||
# Media Library Viewer
|
# Media Library Viewer
|
||||||
|
|
||||||
Small Streamlit app for browsing a remote Jellyfin library, inspecting files on disk over SSH, previewing media metadata with `ffprobe`, and running safe remote job templates.
|
Media library management tool with Jellyfin integration, SSH file inspection, server monitoring, and safe remote job templates.
|
||||||
|
|
||||||
See `docs/REQUIREMENTS.md` for the living requirements, decisions, and planning history.
|
See `docs/REQUIREMENTS.md` for the living requirements, decisions, and planning history.
|
||||||
|
See `docs/MIGRATION_PLAN.md` for the FastAPI + React architecture plan.
|
||||||
|
|
||||||
Project policy/docs:
|
Project policy/docs:
|
||||||
|
|
||||||
- License: `LICENSE` (MIT)
|
- License: `LICENSE` (MIT)
|
||||||
- Contributing guide: `CONTRIBUTING.md`
|
- Contributing guide: `CONTRIBUTING.md`
|
||||||
|
|
||||||
## Phase 1 features
|
## Architecture
|
||||||
|
|
||||||
- Dashboard media counts for movies, series, and episodes, plus now-playing sessions (user, title, playback state, transcoding)
|
The project consists of two subprojects:
|
||||||
- SQLite-indexed Media tab with full-library sort/filter for runtime, size, bitrate, explicit HDR yes/no flag, date added, codec, resolution, series, season, episode, path, and row-based selection that automatically syncs File browser to the selected item's folder
|
|
||||||
- Server monitoring dashboard plus detailed Monitoring tab for CPU, IO wait, RAM, network, disk I/O, and disk space
|
|
||||||
- Jellyfin API-key connection using `GET /Users` plus user-scoped library endpoints
|
|
||||||
- Compact SSH remote directory browser with clickable rows, search, filters, sorting, pagination, and `[UP] ..` navigation
|
|
||||||
- Blocking selected-file `ffprobe` preview for known video files
|
|
||||||
- Separate container, video, audio, and subtitle metadata sections
|
|
||||||
- `stat` inspection
|
|
||||||
- Safe/read-only job templates, designed to extend later
|
|
||||||
|
|
||||||
## Project structure
|
- **`backend/`** — FastAPI Python API (see `backend/README.md`)
|
||||||
|
- **`frontend/`** — React + TypeScript SPA (see `frontend/README.md`)
|
||||||
|
- **`archive/`** — Original Streamlit prototype (preserved for reference)
|
||||||
|
|
||||||
```text
|
## Features
|
||||||
.
|
|
||||||
├── app.py # Thin Streamlit entrypoint
|
|
||||||
├── pyproject.toml # Package metadata, dependencies, tool config
|
|
||||||
├── requirements.txt # Convenience install file (-e .)
|
|
||||||
├── docs/
|
|
||||||
│ └── REQUIREMENTS.md # Living requirements and decision log
|
|
||||||
├── src/
|
|
||||||
│ └── media_library_viewer/
|
|
||||||
│ ├── app.py # Thin Streamlit orchestration layer
|
|
||||||
│ ├── config.py # Environment/.env config
|
|
||||||
│ ├── jobs.py # Remote job templates
|
|
||||||
│ ├── utils.py # Formatting and ffprobe summaries
|
|
||||||
│ ├── domain/ # UI-independent normalization/domain helpers
|
|
||||||
│ ├── services/ # UI-independent application services/indexes
|
|
||||||
│ ├── ui/ # Streamlit UI modules split by feature area
|
|
||||||
│ └── clients/
|
|
||||||
│ ├── jellyfin.py # Jellyfin API client
|
|
||||||
│ └── ssh.py # SSH/ffprobe client
|
|
||||||
└── tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup
|
- Dashboard with now-playing sessions, server monitoring overview, and per-library media counts
|
||||||
|
- Server monitoring with CPU, IO wait, RAM, network, and disk I/O charts
|
||||||
|
- SQLite-indexed media table with full-library sort/filter
|
||||||
|
- Remote file browser with ffprobe preview and job execution
|
||||||
|
- Jellyfin API integration for library metadata
|
||||||
|
- SSH-based file inspection and remote job templates
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd backend
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -e '.[dev]'
|
||||||
|
uvicorn media_library_viewer_api.main:app --reload --port 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
For development tools:
|
### Frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -e '.[dev]'
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Create `.env` or use Streamlit secrets/env vars:
|
Frontend runs on http://localhost:5173 and proxies API requests to http://localhost:8000.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.env` file in the project root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
JELLYFIN_URL=https://jellyfin.example.com
|
JELLYFIN_URL=https://jellyfin.example.com
|
||||||
JELLYFIN_API_KEY=your-api-key
|
JELLYFIN_API_KEY=your-api-key
|
||||||
# Optional fallback if user selection via GET /Users does not work.
|
|
||||||
JELLYFIN_USER_ID=
|
JELLYFIN_USER_ID=
|
||||||
|
|
||||||
SSH_HOST=media-server.example.com
|
SSH_HOST=media-server.example.com
|
||||||
SSH_USERNAME=username
|
SSH_USERNAME=username
|
||||||
SSH_PORT=22
|
SSH_PORT=22
|
||||||
SSH_KEY_FILENAME=/home/username/.ssh/id_rsa
|
SSH_KEY_FILENAME=/home/username/.ssh/id_rsa
|
||||||
# SSH_PASSWORD=optional-password-or-key-passphrase
|
SSH_PASSWORD=
|
||||||
REMOTE_MEDIA_ROOT=/mnt/media
|
|
||||||
# Optional fallback prefix when REMOTE_MEDIA_ROOT mapping is not enough.
|
REMOTE_MEDIA_ROOT=/srv/media
|
||||||
# Example fallback: Jellyfin /media/... -> SSH /srv/media/...
|
REMOTE_PATH_PREFIX=
|
||||||
REMOTE_PATH_PREFIX=/srv
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
streamlit run app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Before publishing / committing
|
|
||||||
|
|
||||||
Keep these out of git:
|
|
||||||
|
|
||||||
- `.env` and any real secrets/tokens/passwords
|
|
||||||
- `.streamlit/secrets.toml`
|
|
||||||
- `.venv/`, local IDE files (`.idea/`, `.vscode/`)
|
|
||||||
- local cache/index files under `.cache/`
|
|
||||||
- logs and temporary files
|
|
||||||
|
|
||||||
The included `.gitignore` already excludes these.
|
|
||||||
|
|
||||||
## Remote server requirements
|
## Remote server requirements
|
||||||
|
|
||||||
For file browsing, resource metrics, and media metadata:
|
The remote server needs:
|
||||||
|
|
||||||
```bash
|
- Linux `/proc` and `/sys/block` for monitoring
|
||||||
ffprobe -version
|
- `/bin/sh` (POSIX shell)
|
||||||
python3 --version
|
- `python3`, `ffprobe`, `find`, `stat`, `df`, `awk`
|
||||||
find --version
|
|
||||||
stat --version
|
|
||||||
df --version
|
|
||||||
awk --version || true
|
|
||||||
```
|
|
||||||
|
|
||||||
The resource dashboard uses Linux `/proc`, `/sys/block`, `/bin/sh`, and a lightweight collector started over SSH. No full monitoring stack is required, but last-hour charts require the collector to have been running long enough to gather samples. Network is shown as download/upload in bytes per second, and disk I/O is shown as read/write in bytes per second. The metrics JSONL file is pruned to 7 days with a 70,000-line safety cap. SSH commands are explicitly run through `/bin/sh -c`, so the remote user's login shell may be fish or another shell. If no samples appear after 10-20 seconds, use the dashboard's `Restart` button and inspect `Collector diagnostics`.
|
The SSH client rejects unknown host keys. Connect manually once first:
|
||||||
|
|
||||||
The SSH client uses your local `known_hosts` and rejects unknown host keys. Connect once manually first:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh user@host
|
ssh user@host
|
||||||
```
|
```
|
||||||
|
|
||||||
## Extending jobs
|
## Development
|
||||||
|
|
||||||
Add templates in `src/media_library_viewer/jobs.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
JOB_TEMPLATES["my_job"] = JobTemplate(
|
|
||||||
name="My job",
|
|
||||||
description="What it does.",
|
|
||||||
command_template="my-command --input {path}",
|
|
||||||
destructive=False,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Template variables are shell-quoted before insertion. For destructive jobs, add confirmation UI before executing.
|
|
||||||
|
|
||||||
## Development checks
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PYTHONPATH=src python -m py_compile app.py src/media_library_viewer/*.py src/media_library_viewer/clients/*.py
|
# Backend
|
||||||
ruff check .
|
cd backend && PYTHONPATH=src python -m py_compile src/media_library_viewer_api/main.py
|
||||||
pytest
|
|
||||||
|
# Frontend
|
||||||
|
cd frontend && npx tsc --noEmit && npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Prefer Jellyfin API for library metadata.
|
- Jellyfin server root URL required (not `/web`). The client strips trailing `/web` defensively.
|
||||||
- Enter the Jellyfin server root URL, for example `https://jellyfin.example.com`, not `https://jellyfin.example.com/web`. The client strips a trailing `/web` defensively.
|
- SSH commands run through `/bin/sh -c` regardless of remote login shell.
|
||||||
- Prefer SSH/ffprobe for authoritative disk-level media metadata such as actual bitrate, color transfer, HDR metadata, audio channels, subtitles, and container details.
|
- Job templates are shell-quoted. Add new templates in `backend/src/media_library_viewer_api/jobs.py`.
|
||||||
- Keep cleanup/transcode jobs explicit and template-based; avoid free-form command execution in the UI unless this app is only used locally by trusted users.
|
- Monitoring collector uses JSONL in `/tmp`, pruned to 7 days / 70k lines.
|
||||||
|
|||||||
+41
-8
@@ -2,6 +2,38 @@
|
|||||||
|
|
||||||
FastAPI backend serving the REST API for Jellyfin media browsing, SSH file inspection, and server monitoring.
|
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
|
## Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -11,12 +43,6 @@ source .venv/bin/activate
|
|||||||
pip install -e '.[dev]'
|
pip install -e '.[dev]'
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install dependencies directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install fastapi uvicorn[standard] pydantic-settings paramiko requests python-dotenv pandas
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Create a `.env` file in the project root (or set environment variables):
|
Create a `.env` file in the project root (or set environment variables):
|
||||||
@@ -40,7 +66,13 @@ REMOTE_PATH_PREFIX=
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
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
|
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/libraries` — Per-library breakdown
|
||||||
- `GET /api/dashboard/now-playing` — Active playback sessions
|
- `GET /api/dashboard/now-playing` — Active playback sessions
|
||||||
- `GET /api/monitoring/status` — Collector status
|
- `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
|
- `GET /api/monitoring/disk` — Disk space
|
||||||
- `POST /api/monitoring/start|stop|restart` — Collector controls
|
- `POST /api/monitoring/start|stop|restart` — Collector controls
|
||||||
|
- `GET /api/monitoring/diagnostics` — Collector debug info
|
||||||
- `GET /api/media/status` — Index status
|
- `GET /api/media/status` — Index status
|
||||||
- `POST /api/media/build` — Rebuild index
|
- `POST /api/media/build` — Rebuild index
|
||||||
- `GET /api/media/query` — Query with filters/sort/pagination
|
- `GET /api/media/query` — Query with filters/sort/pagination
|
||||||
|
|||||||
@@ -16,9 +16,21 @@ dependencies = [
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = ["httpx", "pytest", "pytest-asyncio", "ruff"]
|
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]
|
[tool.ruff]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
|
src = ["src"]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "I", "W"]
|
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 dataclasses import dataclass
|
||||||
from typing import Any
|
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().
|
# 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.
|
# 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 functools import lru_cache
|
||||||
|
|
||||||
from clients.jellyfin import JellyfinClient
|
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||||
from clients.ssh import RemoteSSHClient
|
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||||
from config import get_settings
|
from media_library_viewer_api.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
@@ -11,7 +11,7 @@ from typing import Any
|
|||||||
|
|
||||||
import pandas as pd
|
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]:
|
def first_media_source(item: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -11,7 +11,7 @@ import shlex
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
|
||||||
from clients.ssh import CommandResult, RemoteSSHClient
|
from media_library_viewer_api.clients.ssh import CommandResult, RemoteSSHClient
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -7,7 +7,7 @@ from contextlib import asynccontextmanager
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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
|
@asynccontextmanager
|
||||||
+2
-2
@@ -6,8 +6,8 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from dependencies import get_jellyfin_client, get_user_id
|
from media_library_viewer_api.dependencies import get_jellyfin_client, get_user_id
|
||||||
from clients.jellyfin import JellyfinClient
|
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||||
|
|
||||||
@@ -7,10 +7,10 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||||
|
|
||||||
from dependencies import get_ssh_client
|
from media_library_viewer_api.dependencies import get_ssh_client
|
||||||
from clients.ssh import RemoteSSHClient
|
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||||
from config import get_settings
|
from media_library_viewer_api.config import get_settings
|
||||||
from path_utils import resolve_remote_media_path
|
from media_library_viewer_api.path_utils import resolve_remote_media_path
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/files", tags=["files"])
|
router = APIRouter(prefix="/api/files", tags=["files"])
|
||||||
|
|
||||||
@@ -7,9 +7,9 @@ from typing import Any
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from dependencies import get_ssh_client
|
from media_library_viewer_api.dependencies import get_ssh_client
|
||||||
from clients.ssh import RemoteSSHClient
|
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||||
from jobs import JOB_TEMPLATES, run_job
|
from media_library_viewer_api.jobs import JOB_TEMPLATES, run_job
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/jobs", tags=["jobs"])
|
router = APIRouter(prefix="/api/jobs", tags=["jobs"])
|
||||||
|
|
||||||
@@ -6,9 +6,9 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
|
||||||
from dependencies import get_jellyfin_client, get_user_id
|
from media_library_viewer_api.dependencies import get_jellyfin_client, get_user_id
|
||||||
from clients.jellyfin import JellyfinClient
|
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||||
from services.media_index import MediaIndex, build_media_index
|
from media_library_viewer_api.services.media_index import MediaIndex, build_media_index
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/media", tags=["media"])
|
router = APIRouter(prefix="/api/media", tags=["media"])
|
||||||
|
|
||||||
+4
-4
@@ -7,9 +7,9 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from dependencies import get_ssh_client
|
from media_library_viewer_api.dependencies import get_ssh_client
|
||||||
from clients.ssh import RemoteSSHClient
|
from media_library_viewer_api.clients.ssh import RemoteSSHClient
|
||||||
from clients.resources import (
|
from media_library_viewer_api.clients.resources import (
|
||||||
disk_space,
|
disk_space,
|
||||||
read_resource_metrics,
|
read_resource_metrics,
|
||||||
resource_collector_debug_info,
|
resource_collector_debug_info,
|
||||||
@@ -18,7 +18,7 @@ from clients.resources import (
|
|||||||
start_resource_collector,
|
start_resource_collector,
|
||||||
stop_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"])
|
router = APIRouter(prefix="/api/monitoring", tags=["monitoring"])
|
||||||
|
|
||||||
+2
-2
@@ -13,8 +13,8 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
from clients.jellyfin import JellyfinClient
|
from media_library_viewer_api.clients.jellyfin import JellyfinClient
|
||||||
from domain.media import display_media_row, normalize_media_item
|
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
|
# Local generated database. It is ignored by git and can be rebuilt from
|
||||||
# Jellyfin metadata whenever needed.
|
# Jellyfin metadata whenever needed.
|
||||||
Reference in New Issue
Block a user