- Add `movies` type: per-movie folders with video files, poster/backdrop images, and optional Jellyfin NFO metadata (title, year, plot, rating, genres, runtime). Grid view with 2:3 poster art, detail modal with play and two-click delete of the movie folder. - Add `tv` type: Series -> Season -> Episode hierarchy with lazy loading at each level. Reads tvshow.nfo and episodedetails NFO files for metadata. Episode grid with video thumbnails, streams via existing video player. Delete is limited to the entire series folder to avoid breaking Jellyfin. - Add fast-xml-parser dependency for Kodi/Jellyfin NFO parsing (lib/nfo.ts) - Migrate existing DB to expand the libraries CHECK constraint to include the two new types; migration is idempotent and preserves existing data. 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 — currently supporting Games and Mixed Media library types.
Features
- Games library — displays a grid of game cover art scanned from folders. Each game folder is expected to contain a
.ziparchive and optional artwork (cover.*,widecover.*). Clicking a game opens a detail modal with a download link for the zip. - Mixed media library — a folder-navigable browser that mirrors the directory structure on disk. Image and video files display auto-generated square thumbnails. Videos open in an inline player (with full seek support via HTTP range requests). Images open in a lightbox. Other files are opened in a new tab.
- Thumbnail generation — lazy on-demand thumbnails for images (via
sharp) and video frames (viaffmpeg). Thumbnails are cached to disk in.thumbnails/and regenerated automatically when the source file changes. - Library management UI — add and remove libraries at
/managewithout touching any config files. Configuration persists across restarts inlibraries.json. - 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)
├── data/ # Example media (not committed to production)
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx # Home — library cards (redirects to /manage if empty)
│ │ ├── manage/page.tsx # Library management — add/remove libraries
│ │ ├── library/[id]/page.tsx # Library view (games or mixed)
│ │ └── api/
│ │ ├── libraries/route.ts # GET /api/libraries, POST /api/libraries
│ │ ├── libraries/[id]/route.ts # DELETE /api/libraries/:id
│ │ ├── games/route.ts # GET /api/games?libraryId=
│ │ ├── browse/route.ts # GET /api/browse?libraryId=&path=
│ │ ├── file/route.ts # GET /api/file?libraryId=&path=
│ │ └── thumbnail/route.ts # GET /api/thumbnail?libraryId=&path=
│ ├── components/
│ │ ├── LibraryCard.tsx
│ │ ├── NavLink.tsx
│ │ ├── games/
│ │ │ ├── GamesView.tsx
│ │ │ └── GameDetailModal.tsx
│ │ └── mixed/
│ │ ├── MixedView.tsx
│ │ ├── VideoPlayerModal.tsx
│ │ └── ImageLightbox.tsx
│ ├── lib/
│ │ ├── libraries.ts # Config read/write, path resolution, add/remove helpers
│ │ ├── games.ts # Games library scanner
│ │ ├── files.ts # Mixed library directory scanner
│ │ └── thumbnails.ts # Thumbnail cache + generation (sharp / ffmpeg)
│ └── 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 ffmpeg
Ubuntu/Debian:sudo apt install ffmpeg
Windows: Download from ffmpeg.org and add toPATH
# 1. Install dependencies
npm install
# 2. Start the development server
npm run dev
Open http://localhost:3000.
Other available commands:
npm run build # Production build
npm run start # Start production server (run build first)
npm run lint # Run ESLint
Library Configuration
Libraries are managed through the Manage Libraries screen at /manage in the app. No manual file editing is required.
When you add a library via the UI, you provide:
| Field | Description |
|---|---|
| Name | Display name shown in the UI |
| Path | Absolute or project-relative path to the library root folder on disk |
| Type | Games or Mixed Media |
The app validates that the path exists as a directory before saving. Configuration is stored in libraries.json at the project root and persists across restarts.
If no libraries are configured, navigating to / automatically redirects to /manage.
Paths can point anywhere on the filesystem — they do not need to be inside the project directory.
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)
- The
.zipfilename can be anything; the first.zipfound in the folder is used. - Cover art filenames are matched case-insensitively against
cover.*andwidecover.*. Any image extension is accepted.
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.
| Route | Method | Description |
|---|---|---|
/api/libraries |
GET | Returns the full configured library list |
/api/libraries |
POST | Adds a library. Body: { name, path, type }. Validates the path exists. |
/api/libraries/:id |
DELETE | Removes a library by id |
/api/games?libraryId=<id> |
GET | Scans the games library and returns structured game entries |
/api/browse?libraryId=<id>&path=<subpath> |
GET | Lists the contents of a directory within a mixed library |
/api/file?libraryId=<id>&path=<relpath> |
GET | Streams a file; supports HTTP Range requests for seekable video playback |
/api/thumbnail?libraryId=<id>&path=<relpath> |
GET | Returns a cached square thumbnail (JPEG) for an image or video file; 404 if generation fails or ffmpeg is unavailable |
Tech Stack
- Next.js 15 (App Router)
- React 19
- TypeScript 5
- Tailwind CSS 4