# MediaLore A self-hosted web UI for browsing media libraries on a NAS or local filesystem. Configure folders as typed libraries — supporting **Games**, **Movies**, **TV**, and **Mixed Media** library types. ## Features - **Games library** — grid of game cover art scanned from folders. Each game folder contains a `.zip` archive and optional artwork (`cover.*`, `widecover.*`). Clicking a game opens a detail modal with a download link. - **Movies library** — grid of movie posters scanned from per-movie folders. Reads `.nfo` sidecar files (Kodi-compatible) for metadata (title, year, plot, rating, genres). Clicking a movie opens a full-screen video player. - **TV library** — browse TV series → seasons → episodes. Reads `tvshow.nfo` and per-episode `.nfo` files for metadata. Supports standard season folder layouts and flat (seasonless) series. - **Mixed media library** — folder-navigable browser that mirrors the directory structure on disk. Images open in a lightbox; videos open in an inline player with full seek support. - **Thumbnail generation** — lazy on-demand thumbnails for images (via `sharp`) and video frames (via `ffmpeg`). Thumbnails are cached to disk in `.thumbnails/` and regenerated when the source file changes. - **Tag system** — create tag categories and items, then assign tags to individual media items per-library. Filter any library view by one or more tags. - **Library management UI** — add and remove libraries at `/manage` without touching config files. Configuration persists across restarts in `libraries.json`. - **User authentication** — iron-session cookie auth with `admin` and `user` roles. Admins manage libraries, scan settings, tags, and users. Users have read-only access, optionally restricted to specific libraries. - **Background scanner** — scans all libraries on demand (or on a schedule) to pre-generate thumbnails and populate metadata caches. - **Path-jailed file serving** — all file access is verified to stay within the configured library root before being served. ## Project Structure ``` MediaLoreWeb/ ├── libraries.json # Runtime library config (managed via UI, do not edit by hand) ├── .thumbnails/ # Disk cache for generated thumbnails (auto-created, gitignored) ├── src/ │ ├── app/ │ │ ├── layout.tsx │ │ ├── page.tsx # Home — library cards (redirects to /manage if empty) │ │ ├── manage/page.tsx # Library management │ │ ├── manage/tags/page.tsx # Tag management │ │ ├── manage/users/page.tsx # User management (admin only) │ │ ├── manage/scan/page.tsx # Scan settings and manual trigger │ │ ├── library/[id]/page.tsx # Library view (games / movies / tv / mixed) │ │ └── api/ │ │ ├── auth/login/route.ts # POST /api/auth/login │ │ ├── auth/logout/route.ts # POST /api/auth/logout │ │ ├── auth/register/route.ts # POST /api/auth/register │ │ ├── libraries/route.ts # GET, POST /api/libraries │ │ ├── libraries/[id]/route.ts # PATCH, DELETE /api/libraries/:id │ │ ├── games/route.ts # GET /api/games │ │ ├── movies/route.ts # GET /api/movies │ │ ├── tv/route.ts # GET /api/tv (series, seasons, episodes) │ │ ├── browse/route.ts # GET /api/browse (mixed media) │ │ ├── file/route.ts # GET /api/file │ │ ├── thumbnail/route.ts # GET /api/thumbnail │ │ ├── game-cover/route.ts # POST /api/game-cover (upload cover art) │ │ ├── library-cover/[id]/route.ts # GET /api/library-cover/:id │ │ ├── scan/route.ts # POST /api/scan │ │ ├── scan-settings/route.ts # GET, POST /api/scan-settings │ │ ├── settings/route.ts # GET, POST /api/settings │ │ ├── tags/categories/route.ts # GET, POST /api/tags/categories │ │ ├── tags/categories/[id]/route.ts# PATCH, DELETE /api/tags/categories/:id │ │ ├── tags/items/route.ts # GET, POST /api/tags/items │ │ ├── tags/items/[id]/route.ts # PATCH, DELETE /api/tags/items/:id │ │ ├── tags/assignments/route.ts # GET, POST, DELETE /api/tags/assignments │ │ ├── tags/library-assignments/route.ts # GET /api/tags/library-assignments │ │ ├── users/route.ts # GET, POST /api/users │ │ ├── users/[id]/route.ts # PATCH, DELETE /api/users/:id │ │ └── users/[id]/permissions/route.ts # GET, POST /api/users/:id/permissions │ ├── components/ │ │ ├── FilterPanel.tsx │ │ ├── LibraryCard.tsx │ │ ├── NavLink.tsx │ │ ├── DoomScrollView.tsx │ │ ├── games/ │ │ │ ├── GamesView.tsx │ │ │ └── GameDetailModal.tsx │ │ ├── movies/ │ │ │ ├── MoviesView.tsx │ │ │ └── MovieDetailModal.tsx │ │ ├── tv/ │ │ │ └── TvView.tsx │ │ ├── mixed/ │ │ │ ├── MixedView.tsx │ │ │ ├── VideoPlayerModal.tsx │ │ │ └── ImageLightbox.tsx │ │ └── tags/ │ │ └── TagSelector.tsx │ ├── lib/ │ │ ├── auth.ts # iron-session auth, requireAdmin / requireLibraryAccess helpers │ │ ├── db.ts # SQLite setup and migrations (better-sqlite3) │ │ ├── libraries.ts # Config read/write, path resolution, path-jailing │ │ ├── media-utils.ts # Shared constants (HIDDEN_FILES, VIDEO_EXTENSIONS) and helpers │ │ ├── games.ts # Games library scanner │ │ ├── movies.ts # Movies library scanner │ │ ├── tv.ts # TV library scanner (series / seasons / episodes) │ │ ├── files.ts # Mixed media directory scanner │ │ ├── nfo.ts # Kodi-compatible .nfo XML parser │ │ ├── scanner.ts # Full-library background scan orchestrator │ │ ├── thumbnails.ts # Thumbnail cache + generation (sharp / ffmpeg) │ │ ├── tags.ts # Tag CRUD and assignment helpers │ │ ├── users.ts # User CRUD and permission helpers │ │ └── app-settings.ts # App-level settings (scan schedule, etc.) │ ├── hooks/ │ │ └── useUserSettings.ts │ └── types/ │ └── index.ts ``` ## Developer Setup **Requirements:** Node.js 18+, `ffmpeg` and `ffprobe` (for video thumbnails) > Video thumbnails require `ffmpeg` and `ffprobe` to be installed and available on `$PATH`. If they are missing, video tiles gracefully fall back to a generic icon — no errors are thrown. > > **macOS:** `brew install ffmpeg` > **Ubuntu/Debian:** `sudo apt install ffmpeg` > **Windows:** Download from [ffmpeg.org](https://ffmpeg.org/download.html) and add to `PATH` ```bash # 1. Install dependencies npm install # 2. Copy the example env file and fill in SESSION_SECRET cp .env.example .env.local # 3. Start the development server npm run dev ``` Open [http://localhost:3000](http://localhost:3000). On first run you will be prompted to create an admin account. Other available commands: ```bash npm run build # Production build npm run start # Start production server (run build first) npm run lint # Run ESLint ``` ## Environment Variables | Variable | Required | Description | |----------|----------|-------------| | `SESSION_SECRET` | Yes | Secret used to sign session cookies. Must be at least 32 characters. | | `COOKIE_SECURE` | No | Set to `true` in production (HTTPS). Defaults to `false`. | ## Authentication MediaLore uses cookie-based sessions (iron-session). On first launch, navigate to `/register` to create the initial admin account. Subsequent registrations require an existing admin to create accounts via `/manage/users`. **Roles:** | Role | Capabilities | |------|-------------| | `admin` | Full access: manage libraries, users, tags, scan settings | | `user` | Read-only access to assigned libraries | Library-level permissions can be configured per user at `/manage/users`. ## Library Configuration Libraries are managed through the **Manage Libraries** screen at `/manage` in the app. | Field | Description | |-------|-------------| | Name | Display name shown in the UI | | Path | Absolute or project-relative path to the library root folder on disk | | Type | `games`, `movies`, `tv`, or `mixed` | The app validates that the path exists as a directory before saving. Configuration is stored in `libraries.json` at the project root. ## Library Folder Conventions ### Games (`"type": "games"`) Each game is a subdirectory containing: ``` Games/ └── My Game Title/ ├── My Game Title.zip # Required — the downloadable archive ├── cover.png # Optional — portrait cover art (case-insensitive) └── widecover.jpg # Optional — landscape/hero cover art (case-insensitive) ``` Subdirectories without a `.zip` are treated as series containers — their child directories are scanned as individual games. ### Movies (`"type": "movies"`) Each movie is a subdirectory containing a single video file and optional sidecar files: ``` Movies/ └── The Matrix (1999)/ ├── The Matrix (1999).mkv # Required — any supported video extension ├── movie.nfo # Optional — Kodi-compatible metadata ├── poster.jpg # Optional — portrait poster art └── backdrop.jpg # Optional — backdrop/fanart image ``` Supported video extensions: `.mkv`, `.mp4`, `.avi`, `.mov`, `.m4v`, `.wmv`, `.ts`, `.m2ts` Poster filenames are matched case-insensitively against `poster`, `cover`, or `folder`. Backdrop filenames are matched against `backdrop`, `fanart`, or `background`. ### TV (`"type": "tv"`) ``` TV/ └── Breaking Bad/ ├── tvshow.nfo # Optional — series metadata ├── poster.jpg # Optional — series poster ├── Season 01/ │ ├── s01e01.mkv │ ├── s01e01.nfo # Optional — episode metadata │ └── ... └── Season 02/ └── ... ``` Season directory names are matched against patterns: `Season 01`, `S01`, `1`, `01`. If no season subdirectories contain video files, the series root itself is treated as a flat (seasonless) season. ### Mixed Media (`"type": "mixed"`) No specific structure is required. The UI mirrors the directory tree exactly as it exists on disk. Supported media types: - **Video:** `.mp4`, `.mov`, `.mkv`, `.avi`, `.webm`, `.m4v` - **Image:** `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`, `.tiff` ## API Reference All API routes are server-side. File paths are never exposed in client-side state — only opaque `/api/file?...` URLs are sent to the browser. ### Auth | Route | Method | Description | |-------|--------|-------------| | `/api/auth/login` | POST | Authenticate. Body: `{ username, password }` | | `/api/auth/logout` | POST | Clear session cookie | | `/api/auth/register` | POST | Create account. Body: `{ username, password }`. First user becomes admin. | ### Libraries | Route | Method | Description | |-------|--------|-------------| | `/api/libraries` | GET | Returns the full configured library list | | `/api/libraries` | POST | Adds a library. Body: `{ name, path, type }` | | `/api/libraries/:id` | PATCH | Updates a library | | `/api/libraries/:id` | DELETE | Removes a library by id | ### Media | Route | Method | Description | |-------|--------|-------------| | `/api/games?libraryId=` | GET | Scans the games library and returns structured game entries | | `/api/movies?libraryId=` | GET | Scans the movies library and returns movie entries | | `/api/tv?libraryId=` | GET | Returns TV series list | | `/api/tv?libraryId=&seriesId=` | GET | Returns seasons for a series | | `/api/tv?libraryId=&seriesId=&seasonId=` | GET | Returns episodes for a season | | `/api/browse?libraryId=&path=` | GET | Lists the contents of a directory within a mixed library | | `/api/file?libraryId=&path=` | GET | Streams a file; supports HTTP `Range` requests for seekable video | | `/api/thumbnail?libraryId=&path=` | GET | Returns a cached square thumbnail (JPEG); `404` if generation fails | ### Tags | Route | Method | Description | |-------|--------|-------------| | `/api/tags/categories` | GET, POST | List or create tag categories | | `/api/tags/categories/:id` | PATCH, DELETE | Update or delete a category | | `/api/tags/items` | GET, POST | List or create tag items | | `/api/tags/items/:id` | PATCH, DELETE | Update or delete a tag item | | `/api/tags/assignments` | GET, POST, DELETE | Get, add, or remove tag assignments on a media item | | `/api/tags/library-assignments?libraryId=` | GET | All tag assignments for a library (used by filter panel) | ### Users & Settings | Route | Method | Description | |-------|--------|-------------| | `/api/users` | GET, POST | List or create users (admin only) | | `/api/users/:id` | PATCH, DELETE | Update or delete a user | | `/api/users/:id/permissions` | GET, POST | Get or set library-level permissions for a user | | `/api/settings` | GET, POST | App-level settings | | `/api/scan` | POST | Trigger a full library scan | | `/api/scan-settings` | GET, POST | Get or update scan schedule settings | ## Tech Stack - [Next.js 15](https://nextjs.org/) (App Router) - [React 19](https://react.dev/) - [TypeScript 5](https://www.typescriptlang.org/) - [Tailwind CSS 4](https://tailwindcss.com/) - [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) — SQLite database - [iron-session](https://github.com/vvo/iron-session) — cookie-based sessions - [sharp](https://sharp.pixelplumbing.com/) — image thumbnail generation - [ffmpeg](https://ffmpeg.org/) — video thumbnail extraction