Formats like .mkv, .avi, .wmv, .ts, .m2ts and .tiff are not natively supported by browsers and would stall silently in Doom Scroll mode. Add src/lib/browser-media.ts with BROWSER_VIDEO_EXTENSIONS (.mp4, .webm, .mov, .m4v), BROWSER_IMAGE_EXTENSIONS (.jpg, .jpeg, .png, .gif, .webp, .bmp), and an isBrowserPlayable() helper that extracts the extension without importing Node's path module. Filter doomScrollItems in MixedView, MoviesView, and TvView using this helper so only natively renderable files are passed to DoomScrollView. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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