Reviewed-on: http://gitea.lan/gpatti/MediaLore/pulls/18
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
.ziparchive 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
.nfosidecar 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.nfoand per-episode.nfofiles 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 (viaffmpeg). 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
/managewithout touching config files. Configuration persists across restarts inlibraries.json. - User authentication — iron-session cookie auth with
adminanduserroles. 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
ffmpegandffprobeto 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 ffmpegUbuntu/Debian:sudo apt install ffmpegWindows: Download from ffmpeg.org and add toPATH
# 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. On first run you will be prompted to create an admin account.
Other available commands:
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 (App Router)
- React 19
- TypeScript 5
- Tailwind CSS 4
- better-sqlite3 — SQLite database
- iron-session — cookie-based sessions
- sharp — image thumbnail generation
- ffmpeg — video thumbnail extraction