Libraries and media
Media roots
A media root is a top-level folder the server may read. Roots come from:
STREAM_MEDIA_ROOT(falls back toMEDIA_PATH, then../flixer-data/media). Accepts a JSON array (["D:/Movies","D:/Shows"]), an OS-delimiter-separated list, or a single path.- Connected network shares (see below), merged in by
NetworkSharesService.roots().
src/api/mediaLibraries/mediaRootsService.ts:
- Root id =
root-+ first 12 hex chars ofsha256(resolved path); name = folder basename. Shares useshare-<12 hex of share uuid>. - Paths inside a root are relative and validated by
parseRelativePath:.,..,\,:and NUL are rejected. resolveDirectoryusesrealpathand checks the result stays inside the root (no symlink escapes).browselists non-hidden sub-directories, sorted.
The streamer fetches the same list from GET /api/streamer/media-roots (HMAC-signed, ±5 min) so ids are identical on both sides.
Media libraries
A library is DB-backed (table media_libraries) and has a name (unique, 1–100), a type (movies, tvshows, music, homevideos, mixed), an imageStorage mode (cache – artwork in the indexer cache, default; alongside – next to the media) and 1–50 folders { rootId, path }.
- Folders are validated (must exist inside their root) and de-duplicated on create/update.
- Names are unique →
409on conflict. - The admin list adds per folder
rootName,location(absolute path) andavailable(false if the root/folder is offline). MediaLibrariesService.forIndexer()gives the indexer each folder'sabsolutePath.- Deleting a library cascades to its
media_items.
Endpoints: /api/admin/media-libraries (see Admin API).
Indexing flow
POST /api/indexer/media/itemsupserts an item in a transaction (MediaItemsService.upsert): matches byexternal_key, then by path/type, then by relative key if a library folder moved; links parents (series → season → episode, album → track), creating placeholder parents if the parent has not arrived yet.- Fields an admin edited (
edited_fields: title, sortTitle, year, season/episode numbers, overview) are kept on rescans; manually uploaded images (providermanual) are preserved. prune(called after a clean scan) deletes items of that library not seen since the scan started (with a clock-skew margin) and orphaned placeholders.- Writes pass through a
WorkLimiter(INDEXER_MAX_WRITES=3 concurrent,INDEXER_MAX_QUEUED_WRITES=200 queued, else429, which the indexer retries).
Media items and images
media_items.images is a JSON list of {type, path, provider, ...}; types used by the UI include poster, backdrop, thumb, logo. Images are never exposed by path: clients request GET /api/media/items/:id/images/:index (authenticated; Cache-Control: private, max-age=300). Files indexed by the indexer are fetched from it (IndexerClient.fetchImage); manual uploads live in MEDIA_IMAGE_DIR and are referenced as upload:<filename> (src/api/media/mediaImageStorage.ts).
Metadata editor (admin)
/api/admin/media/items… (src/api/adminMedia/adminMediaController.ts):
PATCH /items/:ideditstitle,sortTitle,year,seasonNumber,episodeNumber,episodeEndNumber,overview; edited field names are recorded inedited_fields.POST /items/:id/images?type=uploads an image (multipart fieldimage, ≤ 15 MB);DELETE /items/:id/images/:indexremoves one.GET /items,GET /items/:id,DELETE /items/:idfor browsing/removal.- Subtitle management endpoints (see Playback and history).
Network shares (SMB)
src/api/networkShares/*, table network_shares, admin-only (/api/admin/network-shares).
- Input:
name,host(hostname/IP),share(name only, no slashes), optionalusername,domain(needs a username),password,readOnly(default true). - Passwords are encrypted with AES-256-GCM; the key is derived by HKDF-SHA256 from
STREAM_TOKEN_SECRET(contextflixer:network-shares:v1). If the secret changes the password cannot be decrypted and the share reports an error until re-entered. - Linux:
mount -t cifsunderSMB_MOUNT_DIR/<share id>(needs root orSYS_ADMIN; the password is passed via thePASSWDenv var, never on the command line). macOS:mount_smbfs. Windows:net useand a UNC path. - Create/update connects first and fails with
400(secret-redacted message) if it cannot; unique-name conflict →409; deleting a share used by a library →409. - Status per share:
connecting,connected,error. On startup shares reconnect in the background.POST /:id/reconnectretries. - Responses never contain the password (
hasPasswordonly) and exposerootId,location,status,error. - Docker needs
docker-compose.smb.yml(addsSYS_ADMIN, unconfined AppArmor, shared bind propagation for streamer/indexer). The image installscifs-utils. Alternative: mount the share yourself and list that folder inSTREAM_MEDIA_ROOT. - Mutations call
live.invalidate("shares", …)so the admin UI updates instantly.
Browsing for users
src/api/media/mediaHomeController.ts (/api/media): GET /home, GET /search, GET /browse/:kind (movies → movie+video, tv → series, music → album), GET /items/:id (detail with seasons/episodes or tracks, cast, genres, ratings, technical info). Placeholder items are never returned. Details in API reference.