Reviewed-on: http://gitea.lan/gpatti/MediaLore/pulls/7
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