Alumira — Specification
Status: v1 — implemented. This document describes what Alumira does and why. The code is the reference: where they disagree, the code wins and this document is corrected.
0. Vision & principles
Alumira is a self-hosted media server for a personal collection of films and series. It reads the files where they are, identifies them, and encodes each one once, ahead of time, into a ladder of qualities; playback then serves these ready-made files as adaptive streams to a web interface built around the artwork, on a computer, a phone or a TV with its remote.
Guiding principles
- Media first. A very visual, simple, to-the-point UI. Dead simple UX. Its own identity: we do not imitate the streaming platforms.
- Pre-normalization, never transcoding at playback. During playback the server only serves bytes. Predictable quality and behaviour.
- Split stems, recombined when served. Every track (video per rendition, audio per language/layout, subtitle) is an independent stem; the client receives a multi-track / multi-bitrate manifest generated on the fly.
- Lazy evaluation rather than links. We store identities (fingerprints, rules) and resolve them when needed, rather than maintaining links that drift out of sync.
- Sources are sacred. A store is never a source; a source is never modified, except in explicit
inboxmode. - Simple, imperative code. No OOP; favour build and iteration speed.
1. Scope
V1
- Headless server (Windows, macOS, Linux) with a built-in encoding worker.
- A single web app with roles (admin / user); the administration is a section of the app.
- Movies, series, editions (variants), extras, groups.
- Multiple users, age restrictions, favorites, resume playback.
- Folder/drive sources only.
- Additional encoding workers on the LAN.
Later
- Native clients: Android, iOS, Android TV. Web TV clients: LG webOS, Samsung Tizen (reusing the web client).
- Encoding workers over the Internet.
- Per-media ladder override exposed in the UI (the model supports it from V1, see §6.10).
- Intro / credits detection (chapters + cross-episode audio fingerprint).
- Composing tracks from several sources (e.g. French dub from a DVD over Blu-ray video, PAL speedup correction).
- OCR of image subtitles.
- Dolby Vision.
- Recommendations, playlists and any user-side editorial control.
2. Glossary
| Term | Definition |
|---|---|
| Source | Location (folder/drive) scanned for original files. Read-only (watch), consumed (inbox), or played as its files are (direct). |
| Store | Location where Alumira writes normalized stems. Unlimited number. A store is never a source. |
| Fingerprint | Identity of a source file, independent of its path. |
| Source catalogue | Index fingerprint → current paths (0..n). |
| Stem | A single normalized track, stored as one CMAF fMP4 file (e.g. "1080p H264 SDR video", "fr-FR AAC 2.0 audio", "English WebVTT subtitle"). |
| Rendition | A video quality level (resolution × codec × dynamic range × bit depth). One rendition = one video stem. |
| Rung | One step of the ladder (definition of a rendition to produce). |
| Ladder | Ordered set of rungs + audio/subtitle definitions to produce for a media item. |
| Profile | Named, versioned ladder configuration, editable by the admin. |
| Pass | Group of rungs encoded together in one chain; passes are ordered. |
| Work | A title: movie, series, season, episode. |
| Edition | Variant of a work ("1977 Cut", "Blu-ray 2011", "Deluxe"…). One edition is canonical. |
| Extra | Bonus material (making-of, interview…) attached to a Work or an Edition. |
| Group | Meta entry grouping media (manual or dynamic). |
| Worker | Encoding process (built into the server or remote). |
| Encoder signature | Identity of an encoding configuration that guarantees consistent output (see §7.3). |
3. Overall architecture
┌───────────────────────────────────────────────┐
│ Server │
Sources ──scan──▶ Fingerprint ─▶ Queues ─▶ Scheduler ────┐ │
(watch/inbox) │ catalogue │ │ │
│ ├─ identification ▼ │
│ ├─ normalization Built-in worker
│ ├─ images │ │
│ └─ QA │ │
│ ▼ │
│ SQLite ◀── metadata Stores (fMP4 stems)
│ │ │
│ HTTP: web app + API + HLS manifests ◀──┘ │
└──────────────▲──────────────────────▲─────────┘
│ │
Web / TV client LAN workers
(source streamed
over HTTP)
- One server binary per platform; one worker binary (the server embeds the same worker logic).
- The server serves the web app, the API, HLS manifests, stems (byte ranges) and downloads.
4. Sources & fingerprint catalogue
4.1 Source modes
| Mode | Behaviour |
|---|---|
watch |
Strictly read-only. Alumira never modifies, moves or deletes anything (seedbox, archive drive). |
inbox |
Consumed drop folder: once the media is fully encoded, the source file is deleted (with the subtitles beside it and the folders it leaves empty); no trash. The file itself is lost; what is left is what its profile keeps: its tracks as they are, nothing re-encoded (Original, §6.4, the default for inbox folders), beside its encoded versions, or its encoded versions only, as the add-source dialog says. |
direct |
Played as they are, read-only like watch: nothing encoded nor stored ahead (but small extras). Each file is prepared once it is in: its video keyframes read (packets only), segments of 4 s at least made on them; its subtitles, seek thumbnails (from its keyframes only, decoded by the video chip when it can, an HDR file's turned into SDR; their progress followed) and chapters made as for an encoded file; its audio tracks in AAC played as they are, any other codec (AC-3, DTS, TrueHD…) converted into AAC once (software: there is no audio hardware; small). It is playable as soon as it is identified and prepared. It plays through HLS like the others: its video and AAC tracks are virtual renditions whose segments ffmpeg copies from the file when asked (no encoding): read with the file's own times, cut exactly at the segment's keyframes (the video's fragments before it left out, the sound cut exactly), each fragment given its time back, every track delayed by the same few milliseconds its first frames decode ahead (times never below 0, in step); fragments follow on exactly, a seek plays at once. A device that cannot decode it, or whose connection is too slow for it, gets the qualities transcoded on the fly (below). A file played as it is whose source is gone is unplayable (Maintenance: missing sources, blocking). |
In inbox mode the admin is warned that a profile added later can only start from the best existing stem (or from the Original rung, see §6.4).
Transcoding on the fly: for files played as they are, each profile has a live ladder, set on the Profiles page like its encodes (a resolution, a codec, H.264 or HEVC, and a bitrate per rung; by default 1080p 8 Mb/s, 720p 4.5 Mb/s, 480p 1.8 Mb/s, 360p 900 kb/s, 240p 450 kb/s in H.264); changing it re-encodes nothing. Its qualities are virtual renditions beside the file itself, cut on the same segments (a player switches between them seamlessly): the same quality menu and adaptation to the connection as encoded files. Never above the file: two rungs above its size give one, at its size; when the file itself plays, only rungs under three quarters of its bitrate are offered. Video by the hardware encoder only (VideoToolbox, NVENC, Quick Sync, AMF, VA-API: on servers and NAS too, Synology, QNAP, Unraid with an Intel iGPU or an NVIDIA card; ARM boards through V4L2 later), decoded by the hardware when it can, without B-frames; a server without one offers none. On an Intel chip the picture never leaves it, and is scaled there as the encodes scale (§6.3): by halves at most, with the chip's best filter (scale_vaapi in hq mode, when its driver has it); seek thumbnails of files played as they are too. An HDR file stays HDR for a device showing it (HEVC 10 bits, the file's PQ or HLG, whatever the ladder's codec: every quality in the same range as the file, no change of brightness when the player switches; without a hardware encoder for it, the file alone), and is tone-mapped to SDR for any other. Segments are made when asked, the next one meanwhile, and kept in memory (512 MB) for seeks back and other viewers; one being made goes on as long as someone waits for it, and stops when none does. A player has 3 pieces cut or made at a time (its video, its sound, the next one), the server 16 copies; a request beyond waits its turn. A maximum of viewers transcoded at once (2 by default, 1 to 32, on the Profiles page; a hardware encoder is as busy at 100 kb/s as at 25 Mb/s): beyond it nobody is served worse, a device that can play the file plays it as it is, one that cannot is told to try again in a few minutes or on another device. A place is freed 45 s after its last segment.
4.2 Detection
- Local folders followed through file-system events: nothing is looked at until something changes there, so their disks sleep in peace (a lost event or a folder that cannot be followed falls back to the next point). Network folders (NFS, SMB, FUSE…: changes made by other machines raise no events) are looked at every hour by default (15 min to a day, or only on demand, in Sources), and whenever Alumira is reading there anyway (every 10 min at most). Admin pages never touch a disk: a source's state and a store's free space come from the last look.
- Stability: a file is only taken into account once its size and modification time have been stable for a configurable delay (copy in progress).
4.3 Fingerprints
- Quick fingerprint:
size + hash(start) + hash(end) + hash(k sampled blocks). Used as the identity. - Full hash: computed lazily in the background, only when needed (verification, conflict).
4.4 Catalogue
- The catalogue maintains
fingerprint → {current paths}. A moved file = same fingerprint, new path: no re-import. - A media item only references the fingerprint of its source. The path is resolved in the catalogue when needed (re-encoding, etc.).
- Same fingerprint seen several times → imported once.
- Source disappearance: the media stays available (once normalized it no longer depends on the source).
- Inconsistencies (likely duplicates with different fingerprints, missing sources, etc.) surface in the maintenance section of the administration (§13).
5. Pipeline & queues
detection ─▶ probe ─┬─▶ identification ─────────────┐
├─▶ normalization (passes 1..n) ├─▶ visible
└─▶ images (derivatives) ┘
QA (sampled VMAF) ─▶ maintenance
- Independent queues: identification, normalization, images, QA. Identification and normalization run in parallel; stems are attached to the source file (fingerprint), the edition is resolved by identification.
- Visibility: a media item is only visible to users once it is fully catalogued: identification done AND pass 1 done. A media item that cannot be identified stays invisible and surfaces in maintenance for manual matching.
- Priorities of the normalization queue: pass 1 of every media item before later passes (maximizes the number of visible media during the initial import).
- Every job can be replayed and is idempotent; errors are visible in the administration.
5.1 Identification & metadata providers
- Providers: TMDB and IMDb.
- TMDB: main source (localized metadata, artwork, cast, collections, per-country ratings, alternative episode orders). Requires an API key and attribution; commercial use requires a licence.
- IMDb: no free public API (the official API is paid, through AWS Data Exchange; scraping breaks the terms of use). Used through the
imdb_idprovided by TMDB as the pivot identifier, and through the IMDb non-commercial datasets (ratings). - Each install provides its own TMDB key, entered during onboarding (§13.1).
6. Normalization
6.1 Source preparation
Before encoding, in the filter chain:
- deinterlacing / IVTC (DVD, TV sources);
- constant frame rate (VFR sources);
- black bar detection and cropping;
- normalization of track start offsets (all stems share the same timeline, t=0).
6.2 Rung consistency rules
- H264 ⇒ 8 bit SDR only.
- HDR ⇒ 10 bit ⇒ HEVC or AV1.
- Never upscale: rungs above the source resolution are skipped.
- Supported HDR: HDR10 (and HLG passthrough). Dolby Vision is out of scope.
- HDR stems carry the source's HDR10 metadata (mastering display, light level:
mdcvandclliboxes), written when their chunks are joined: FFmpeg gives it to one output of a filter graph only. - SDR variants of an HDR master: real tone-mapping (libplacebo), never a plain conversion.
6.3 Downscale chain
A fundamental point: low resolutions must look flawless.
- A single decode per pass, then a cascading
filter_complex: each stage divides the resolution by at most 2 (e.g. 2160 → 1080 → 540 → 270), starting from the previous stage, not from the source. - We encode the stages we care about; intermediate stages only exist for downscale quality.
- Reference scaler: libplacebo (Vulkan; MoltenVK on macOS) or zscale in software. Hardware scalers (
scale_qsv,scale_cuda,scale_vt…) are not used by default (e.g. low-power QSV does nearest-neighbour on large downscales). Hardware is used for encoding. - HDR→SDR tone-mapping sits in the chain at the appropriate stage.
- The chain also feeds the trickplay (§6.8).
6.4 Ordered passes
The rungs of a ladder are split into passes, encoded in order. Each pass = one decode + one chain. The manifest includes rungs as soon as they are ready.
Default ladder:
| Pass | Content |
|---|---|
| 1 | 8 bit SDR H264 video: 1080p, 480p, 240p. Audio: every language, AAC 2.0 128 kb/s (downmix, loudnorm) + AAC 5.1 320 kb/s if the source is ≥ 5.1 + light AAC 2.0 64 kb/s. Subtitles. Trickplay. Chapters. |
| 2 | 10 bit HEVC video (HDR kept if the source is HDR): 2160p, 1080p; 8 bit SDR HEVC rescue qualities: 480p, 240p (far lighter than H264 for the same picture on a weak connection). Audio: E-AC-3 5.1 448 kb/s if the source is ≥ 5.1, Opus 2.0 96 kb/s / 5.1 192 kb/s / 7.1 256 kb/s + light Opus 2.0 48 kb/s. |
| last | Original: passthrough remux of the source tracks into the store (no re-encoding). Depends on nothing, slowest to finish. Enabled by default for inbox sources, disabled for watch sources. |
Supported codecs: video H264, HEVC, AV1; audio AAC-LC, Opus, E-AC-3; layouts 2.0, 5.1, 7.1.
6.5 Bitrate
- Capped CRF (
crf+maxrate+bufsize) rather than a fixed bitrate: a cartoon does not use the bitrate of a grainy film. - Fixed, closed GOP, keyframes aligned across all renditions, scene-cut disabled (forced keyframes). 2 s GOP, 6 s segments.
6.6 Audio
- AAC-LC is mandatory (the only universal codec: Apple, TV, web). Opus as a bonus (web, Android). E-AC-3 for multichannel on TVs / AV receivers.
- A stereo downmix is always produced, with EBU R128 loudness normalization (loudnorm).
- Light audio: a light stereo version of each language (
lightrung), in an audio group of its own (aac-light,opus-light) that the manifest gives to the video qualities of 480p and below, so that sound does not outweigh picture on a weak connection. It bears the name of the stereo track it stands for: the player keeps the viewer's choice from one group to the other, and its menu only lists languages and layouts (the codec is the player's business). Used only when every language has one. - Night track (compressed dynamics, stereo): profile option, off by default. Cost: ~1 stereo stem per language.
- Audio is never chunked (priming issues at the joins): encoded in one pass, independently of the video chunks.
- Each source track on its own: its own FFmpeg (sharing one, a mono track could stop early without any error), kept as soon as it is made (never encoded again), its own failure. A track that cannot be encoded is left out: the pass goes on, the media plays without it, Maintenance lists it (incomplete encodes) and a new try makes only the missing tracks.
- Languages identified in BCP-47 (e.g.
fr-FR/fr-CAfor France / Québec French dubs).
6.7 Subtitles
| Source | Processing |
|---|---|
| Text (SRT, WebVTT, mov_text…) | → WebVTT. |
| Styled ASS/SSA | → WebVTT (universal fallback) + original kept for rich client-side rendering (libass wasm, e.g. JASSUB). |
| Image (PGS, VobSub) | Image overlay: subtitles kept as images, rendered by the client; or read into text by a language model, when the admin chooses one for it (below). |
- Flags kept and exposed: forced, SDH, BCP-47 language.
- Tracks in an unknown language (no tag in their file,
und,mul): players show them as such, and they count for no language. Maintenance lists them, each with the language to give it (the title's original one first): a source's track keeps it by its stream, applied to every reading of the source's analysis, so every encode to come uses it; a subtitle file keeps it on its row. The renditions already made take it at once and are renamed after it; for subtitles, the one-per-language rule then applies. - One track per language and use (complete, forced): the best one. SDH are complete subtitles, taken only without plain ones. Among the source's tracks: ASS, then plain text, then pictures (pictures before plain text when they are read into text: they keep their places on screen). A file that is not in the source (below) replaces the track of its language and use, the latest one winning. A track removed by the admin, or replaced by a file, never comes back: a re-encode skips it, a scan leaves its file aside, the automatic search never downloads it again (a file chosen by hand is taken, even removed before). Media encoded before keep their tracks until their next encode, or Maintenance's Keep one per language.
- Picture subtitles read into text (optional, costs): with a model chosen for it, every import reads its picture subtitles; the catalogue's are read on demand from Maintenance. Pictures go to the model by 24, on a neutral ground; each one becomes a WebVTT cue timed as it was, at its place on screen (top, left, right; low and centred: where the player puts subtitles), italics kept. Each batch read is kept. The result is a subtitle file like a downloaded one (
ocr, stems namedext.ocr-<stream>) that replaces the pictures: it can be translated, and is never read again. A WebVTT file that is not in the source is taken as it is (ffmpeg would lose the cues' places). - Subtitle files that are not in the source (§9): found next to it (
<name>.<lang>[.forced|.sdh].srt, or in a Subs folder) or downloaded from OpenSubtitles. Each becomes a stem of its own, outside the passes: added once the title plays, without encoding anything again, and never touched by a re-encode or an upgrade. Text files in a legacy character set are read in the usual code page of their language. - OpenSubtitles (optional; each install brings its own API key, an account raises the daily download quota): subtitles are wanted in the default subtitle languages, unless the title already has them, or sound in that language. Files made for the source's very hash are taken automatically once the title plays; others are chosen by hand, the search covering every subtitle language of the platform (default ones and each account's). A title without any text subtitles is searched in the language spoken too (its original one, else its sound's), though one hears it: what translations start from; taken only when nothing found for the missing languages can be translated from. Downloads are counted per day: what is left waits for the next day. Downloaded files stay in the data directory; the database keeps their record (OpenSubtitles id, release, hash or title match), enough to fetch them again.
- Translation by a language model (on demand): from a media's details, the text subtitles to translate from (any but a translation; the best ones chosen to start with: complete, in the original language, then English) and the languages to translate them into (those without subtitles; the default ones it lacks checked); in batch from Maintenance, the best subtitles into every default language each media lacks. Short notes come first (names, terms, how characters address each other), then the cues by batches of 120, with the scene around them; the timing never goes through the model. Each batch done is kept: a stopped translation resumes where it was. The result is a subtitle file like a downloaded one (
translation), its stems marked as machine-translated for the admin (the player does not tell; their names start withext.tr-). Jobs calling a model (translations, readings) have a queue of their own, never waiting for an encode nor holding one; how many run at a time is a setting (2 by default, 16 at most): each call is paid for. In a media's details, every file added outside the passes (next to the source, downloaded, translated, read) is in an Additions section after the passes.
6.8 Trickplay & chapters
- Trickplay generated from a low-resolution stage of the chain (pass 1): thumbnail sheets (web, Android); HLS I-frame playlist for Apple (later).
- Chapters extracted from the source whenever possible, exposed in the player.
- Subtitles from speech (on demand; it costs): each audio track can have subtitles made by listening to it. The first track of a language (the one the media speaks first: in the title's original language, else its default one) makes that language's subtitles, offered as Create in the media's missing subtitles when it has none in it (in place of a translation; Maintenance's Missing subtitles makes those of the language it speaks); any other track of a language (a commentary, or whatever it is: no use is guessed) makes subtitles of its own, named after its title, a use of their own (they never replace the language's, nor are chosen by themselves in the player). A speech model chosen for the use Listening (from the provider of the AI settings: xAI's Grok Voice Transcribe for now, OpenAI's to come; billed by the hour of sound) hears the track: its centre channel only for a 5.1 mix or more (where the dialogue is), from the source (else its largest rendition), sent as Opus (about 15 MB an hour) in pieces of about five minutes cut in silences, four at a time, each one's words put back at its place (a whole film takes the service longer than the proxies in front of it wait), its language given, hesitations left out, the characters' names given to hear them right. Its words, each timed and its speaker told apart (what a model makes up over music, credits of subtitles or a long phrase looping, left out), are cut into cues as a subtitler would: at pauses, sentence ends, speaker changes, shot changes (measured as for chapters), within 2 lines of 42 characters (13 to 16 signs in Japanese, Chinese, Korean), long enough to be read (17 characters a second; 4, 9, 12 for those), 5/6 s at least, 7 s at most, lingering 0.3 s after the voice, starting with a shot that starts just before it, ending 2 frames before a shot change rather than over it; a quick exchange (two speakers, short lines, the first too short to be read before the second speaks) is one cue, a dashed line each. Then the model of subtitles (the one translating them), when there is one, gets the cues by 80, four batches at a time, with the scene around them, the title, overview and characters' names: what is said, misheard words and names corrected, hesitations and false starts out, condensed only past what the cue can hold in its time (given to it), the speakers' dashes kept, noise taken for speech emptied; the times never go through it. The result is a subtitle file like a translation (
transcription, stems namedext.stt-<stream>), which translations can start from. Two jobs, in the AI lane: listening and writing; what was heard and each batch rewritten are kept until the file is made. A call to a model that takes more than 20 s, or is tried again, is told in the log: when its answer began, when its text did, the tokens of its reasoning, why its tries failed. - Chapters without names (numbered by their authoring tool: "Chapter 01", "Scene 2", "第3章", a time, nothing) are told apart in any language without a model: their titles, numbers, times and punctuation taken out, are the same; three of them at least, or half the chapters, make the media listed in Maintenance (and Missing in its details). On demand, a language model (a use of its own in the settings) names them from the dialogue of the best text subtitles, in one call: 1 to 5 words each, as on a disc's menu, outlining the story without giving it away (no twist, no ending), in each language of the platform (defaults and accounts'); chapters with a name of their own (Recap, Credits) keep it, translated. The names are kept beside the chapters with their start times (a re-encode extracting the same chapters keeps them), in the store's
source.jsontoo; the player shows those of its interface language. They can be removed from the media's details (Additions). - Chapters made where there are none (a media of 15 minutes or more whose source has none, or a single one): at each encode of the pass making its extras, its scene changes are measured on its smallest rendition (ffmpeg: cuts and their strength, black stretches, silences of an audio rendition; a minute or two of decoding) and kept. On demand, the strongest become break points (a fade, black, a sharp cut, the more where nobody speaks and nothing sounds; at least 30 s apart, the start first); the model gets the timed dialogue, the stretches without any and the break points, chooses about one chapter every 7 minutes, each starting at a break point (a silent scene can start one: the picture tells where it is), and names them as above; each start then goes back to where its scene starts: before the exchange that follows it (a question, not its answer), on the best mark at most 2 minutes before (out of a fade first, then after a silence, then a sharp cut). Marks where someone speaks over a cut, or between two close lines, hardly count. The chapters made are kept with their names, beside the source's (none), so a re-encode keeps them.
6.9 Quality control
- Sampled VMAF measurement on every rendition. Anomalies surface in maintenance.
- A second VMAF on a 4K screen (source and rendition both scaled to 3840×2160, VMAF 4K model): unlike the first one it counts what a smaller picture lost — a common yardstick to rank encodes of any height and codec, whatever the device's screen. Playback offers each device only the renditions worth their bitrate: one another playable rendition beats (or matches within 1 point) for less bitrate is dropped, on that yardstick (not measured yet: same height on the first VMAF, else the more efficient codec when also cheaper); HDR renditions (VMAF does not apply) only compete on codec. More bitrate then always brings a better picture: an H264 at twice the bitrate of an HEVC that looks as good is never chosen.
6.10 Profiles: versioning & resolution
- Profiles are versioned. A stem records the profile version that produced it. Editing a profile, or giving a media another one, makes its stems obsolete: an information in maintenance, not an error (they still play). Nothing is re-encoded by itself: the admin upgrades them (one media or a batch), which keeps the stems the current profile makes already (moved to their pass in it), removes the others at once, then runs the passes in fill mode, encoding only what is missing; each pass starts from the source with its own downscale chain. Impossible once the source is gone.
- The effective ladder of a media item is resolved lazily:
global default → type (movie / episode / extra) → media override. The per-media override is in the model from V1; its UI comes later. - The admin sees a size estimate while editing a profile.
7. Workers & distribution
7.1 Workers
- The server embeds a worker. Additional workers can be added on the LAN.
- Token pairing, TLS traffic.
- FFmpeg runs as a subprocess: the one installed on the machine, never bundled (looked up through
-ffmpeg/ALUMIRA_FFMPEG, anffmpeg/directory next to the executable, then thePATH). - Capabilities auto-detected at startup through a one-second test encode with every known encoder of the build (being compiled in does not mean the hardware is there): codecs, hardware, 10 bit, HDR. The result is cached until the FFmpeg build changes (a week at most); the administration can run the tests again. A test that fails slowly on a busy machine, or that contradicts the previous detection, is tried again; what stays inconclusive is tested again 10 minutes later.
7.2 Time-slice distribution
- The video of every pass is encoded in time chunks (one setting, 2 min by default; one chunk for a short media), aligned on keyframes, with or without remote workers: one path for every encode, initial or not, and for a rendition tried again.
- Each chunk runs the whole chain of the pass (every rung) on its slice; this server takes its share, remote workers sharing the job's signature take others (when allowed).
- A finished chunk is kept: an interrupted encode (stop, crash, reboot) resumes from the chunks it lacks. Changing the chunk length starts the cutting again.
- A failed or cancelled encode keeps what it made (finished chunks, assembled renditions, audio tracks): trying it again from Maintenance resumes the same job and makes only the rest.
- The server concatenates the chunks into stems, each rendition once, freeing its chunks as it goes. A chunk is checked before it joins (its files cover its range): a short one is encoded again, alone.
- One encode at a time per media (a pass, a rendition tried again): each replaces stems the other may use; the encodes of other media go on.
- Audio, subtitles and Original are processed outside chunks (§6.6), each stage kept as it finishes.
- A running job reports its stage as
keyorkey:detail(video:3/12: chunk 3 of 12), shown the same way in every page.
7.3 Encoder signature
To avoid uneven quality within one media item:
- Signature =
encoder implementation (libx265, hevc_nvenc…)for each rendition; the preset/parameters travel with the job. The FFmpeg build is not part of it. - Every chunk of a job is encoded with the same encoder. At assembly, chunks must share their codec settings (
avcC/hvcC…): otherwise the job fails instead of producing a broken stream. - Encoder of a job: the one most machines online can run (server and workers), the profile's mode deciding between equals. Alone, the server keeps its choice; with a PC online, a shared encoder (libx264…) wins.
- Failed chunk: reassigned to a worker with the same encoder; failing that, the whole job is restarted.
- Worker update (signature change): the worker drains its running jobs before updating.
- Per-profile choice:
quality(software) orspeed(hardware).
7.4 Source access
- The server streams the source to workers over HTTP (byte ranges). No shared file system, no path mapping between operating systems.
8. Storage & serving
8.1 Stores
- Unlimited number. A store is never a source.
- Default placement policy: ordered fill. Stores form an ordered list; Alumira writes to the first store that is not full, then moves on to the next.
- A store is full when Alumira's files reach its configurable maximum size (empty: the whole disk), or when its disk keeps only a 5 GB safety margin free.
- A stem is always written entirely on a single store. If the store fills up while writing, the stem is rewritten on the next store.
- The stems of one media item may be spread over several stores (lazy resolution).
- Other policies (spreading, SSD/HDD tiering) may come later.
- A store describes itself: no correspondence file, no database needed to understand it.
- The stems of a source are in
media/<its fingerprint>/, each file named after what it is: pass, kind, track, language, settings, flags, profile (p1.video.1080p.hevc.10b.hdr10.crf22.8000k.e1.libx265.@default@3.mp4,p1.audio.t1.fr-FR.aac.2ch.128k.light.@default@3.mp4,ext.os-123456.fr.vtt…). Codec, dimensions and index are read back from the files themselves. - A
source.jsonnext to them keeps what only the source knew: its name, size, duration and chapters. - The
.alumira-storemarker names the store, the installation using it and when it last did (hourly).
- The stems of a source are in
- Adoption: a store added with content (filled by another installation, or before a database was lost) is taken in by a background job: every source folder becomes a media with its stems, after a check that each file is complete; playable once it has picture and sound, identified from the source's name. When the store's last user looks still active (less than two days), the admin is warned, nothing is refused.
- Its sources then join: a scanned file whose fingerprint has a folder takes its place there; it is analysed, only what is missing is encoded, and nothing when its stems follow another profile (upgraded on request, §6.10). Renditions are measured again once the source is there. Sources without stems are encoded as usual; stems without sources keep playing (maintenance: missing sources, nothing lost).
- The same source in two stores (a copied disk): one copy plays, the other is listed in maintenance, to delete or keep as a spare.
8.2 Stem format
- One CMAF fMP4 file per stem, with an index (
sidx), served with byte ranges. No file per segment (no thousands of small files, stems are easy to move between stores).
8.3 Packaging
- HLS only (CMAF fMP4): AVPlayer requires it; ExoPlayer, TVs, Shaka and hls.js play it. No DASH.
- Manifests generated on the fly per session: available rungs, client capabilities, user restrictions, bandwidth cap (§11).
- Signed segment URLs (§10.3).
8.4 Downloads
- The user picks a precise combination (e.g. 720p SDR + Opus fr-FR 2.0 + English subtitles).
- The file is an on-the-fly remux (MP4 or MKV) of the chosen stems, without re-encoding, streamed while it is built.
- Open to every signed-in user, subject to bandwidth control (§11).
8.5 Images
- Dedicated queue: derivatives (several sizes) in JPEG, plus AVIF when the machine encodes AV1, else WebP, served with aggressive caching (the best format the browser takes). Each picture keeps a ThumbHash (about 25 bytes) sent with the cards and pages: its blur shows at once, the picture fading in over it (50 ms) once whole, never drawn as it arrives. Pictures made before get their hash from a background task, all of them first (milliseconds each), then their WebP (one FFmpeg a picture). A picture's URL carries its version (when its sizes were made): kept a year by the browser; a picture made again, or a new one under a reused id, has another URL. Where the version is not known, the browser asks again each time (an answer without the image while it is unchanged). The slideshow keeps its clock: each slot shows the blur, then a small size, then the full one, each once decoded.
- Localized artwork: posters, backdrops, transparent logos, cast portraits. Dominant colours extracted to theme the UI. TMDB images are fetched in their original resolution; their sizes are made from it (backdrops up to 3840 wide); backdrops are chosen the sharpest first (by steps: 4K, 2560, 1920), then the most liked.
- Behind a movie (its page, the home's hero, the player): TMDB's backdrop by default, or a slideshow of pictures taken from the film (6 s each, the next one fading in over half a second, in their order, common to its versions; every slideshow follows one clock: picture, fade and glide are those of the current 6 s slot, so it goes on without a jump from the home to the title page and the player, or after a page left and come back to). In the catalogue, the admin finds a moment on the seek thumbnails, then the very frame (a frame or a second at a time), seen in full size, and takes it: taken from the best there is: the source file while it is there (as the renditions show it: cropped, deinterlaced, at its display size), else the largest rendition (from the fragment holding the time, found with its index: quick even on a large file on a slow disk); an HDR picture is tone-mapped as the SDR renditions are. It is decoded by the video chip when there is one (on an Intel chip, the frames from the keyframe before the time stay there, the one asked only is read back), by the processor when the chip fails; scrubbing, only the picture asked last is taken (the one before stopped). Its black bars are left out, a file played as it is too (never analysed): found on the seek thumbnails, the rows and columns from the edges dark on nine in ten of them (a dark scene is dark on a few only); pictures taken with them before are taken again once. Each picture remembers what it was taken from: when a better one comes (a 4K encode done since, the source back), Maintenance tells it and takes them again; pictures taken before this was recorded are taken again once. As many as wanted, dragged to reorder. Or chosen automatically (no model): 6 pictures from the film's seek thumbnails, between 5 % and half of the film (nothing given away), scored on exposure, sharpness, colour and contrast against the film's own, text (subtitles, credits) and a busy or bright lower left (where the title is written) held against them, a few minutes apart and of other shots; the best ones taken at full size, the sharpest kept, in the film's order. From the film's sheet (it replaces its pictures; only once its seek thumbnails are made), or in batches from Maintenance, which lists the films without pictures of their own.
9. Data model
Conceptual sketch (not an SQL schema).
SourceRoot (id, path, mode: watch|inbox)
SourceFile (quick_fingerprint, full_hash?, size, probe, moviehash?) -- catalogue, kept when the file is gone
SourcePath (fingerprint, source_root, path, seen_at) -- 0..n paths
Store (id, path, capacity, policy)
Work (id, kind: movie|series|season|episode, parent?, numbers/orders)
Edition (id, work, name, canonical: bool, min_age?, sparse override fields)
Extra (id, work|edition, kind, title)
MediaItem (id, edition|extra, source_fingerprint) -- what gets normalized
Stem (id, media_item, kind: video|audio|sub|trickplay, spec, store,
path, profile@version, pass, state, origin: source|external, external_sub?)
ExternalSub (id, source_fingerprint, origin: file|opensubtitles, path, language,
forced, sdh, opensubtitles_id?, release?, matched: hash|title)
Localized (entity, language, title, overview, …) -- multilingual metadata
Artwork (entity, kind, language?, file)
Person (id, name, …)
Credit (person, work|edition, role, character?, audio_language?, image_override?)
Group (id, kind: manual|dynamic, rules?, sort, parent?, pinned, landing_order)
GroupItem (group, work|edition|group, position) -- manual groups
Profile (id, version, rungs[], passes[], audio[], subs[], options)
LadderOverride (scope: type|media, profile) -- planned, UI later
User (id, role, birth_date, audio_langs[], sub_langs[], ui_lang)
Invitation (token, role, expires)
Device (id, user, name, token) -- TV pairing
Progress (user, edition|episode-in-edition, position, updated) -- per edition
Watched (user, work) -- per work
Favorite (user, work)
9.1 Editions
- One canonical edition per work; the other editions inherit from it.
- Each edition is served by one file of the work, chosen by the admin (
Edition.media_id); the editions are ordered and the first one is the main version (played by default; renamable, never removed). A file may serve several editions, or none: it is then a spare copy, kept and encoded but offered to nobody ("Files not served" in the catalogue, "Likely duplicates" in maintenance). A new file joins the edition its name tells (a cut not seen yet names the main version when it is the work's first file, else becomes a new edition), and serves it if it has no file yet; otherwise it is a spare copy. An edition losing its file takes the best spare copy; so does one served by a file that will never play (a damaged source, or one gone, nothing encoded) as soon as a spare copy of it plays (a damaged file replaced by a good one). - Sparse override: an edition only stores what differs; reading =
edition.field ?? canonical.field(title, overview, artwork, cast, age…). - Editions do not share a timeline: audio tracks belong to an edition, never shared.
- Series: see §9.3.
9.2 Cast
- A cast member's thumbnail can be specific to a work / edition (
Credit.image_override). - Dubbing depends on the audio language, not on the edition: a dubbing credit carries an
audio_language. The UI shows the cast matching the user's audio language.
9.3 Series
- Versions of a show (VF, Remastered…) are defined on the show, ordered; the first one is the main version (the episodes' canonical editions). An episode has an edition per version it has a file in. The viewer picks a version on the show page and only sees the seasons and episodes that version has; nothing falls back on another version.
- Episode mapping: the files of a show are mapped to its episodes in one view, every file with its current mapping (version, season, episode or episode range). Mapping tools apply to a selection or to every file: automatic (
S01E02,1x02…), a pattern ({s}season,{e}episode,*anything, a space any separator; without{s}the season comes from the folders), version, season, shift, renumbering. Changes apply at once and can be undone. Only episodes TMDB knows are accepted. - A file covering several episodes (e.g. E01-E02) maps to a range: the viewer sees one block, played from its first episode; the other episodes of the range cannot be played on their own.
- Two files on the same episode of the same version: one too many, reported in the mapping (put one in another version).
- TMDB's alternative orders (DVD, absolute) are supported by the numbering but not offered in the UI.
9.4 Groups
- Manual (ordered list) or dynamic (rules, e.g.
studio = Disney AND year < 1967), nestable. - Can be pre-filled from provider collections (TMDB…).
- Pinnable on the landing page (global, by the admin).
- A group that is empty after filtering (age, visibility) is not shown.
9.5 Progress
- Progress per edition (different timelines); watched status per work.
- "Next episode" stays in the chosen version of the show, with no fallback: the version stops where its files stop.
- Progress writes are buffered in memory and flushed periodically (SQLite write load).
10. Users, access & security
10.1 Roles
adminanduser. A single app; the administration is a restricted section.- Users have no editorial control. They can: play, choose audio / subtitle tracks in the player, add favorites, download.
- Per-user language preferences (audio, subtitles, metadata/UI), used for automatic track selection.
10.2 Authentication
- Invitation links; the invitee creates a password or a passkey (WebAuthn).
- TV pairing by code ("go to
<server>/link, enter ABCD"). - Rate limiting on authentication.
10.3 Internet exposure
- Alumira Connect, in one click: a name of
alumira.net(martin.alumira.net), its certificate from Let's Encrypt by DNS-01 (for the name and*.name), the router asked to open the port (UPnP), the addresses (IPv4 as the service sees it, the machine's own IPv6) sent again every 10 minutes, the whole checked from the Internet. One port everywhere (443 when the machine lets Alumira have it, else 7576): IPv6 translates no port. Nothing of the video goes through Connect: it only names. A local address written as a name (192-168-1-31.martin.alumira.net) leads home in HTTPS without the Internet. The service (cmd/alumira-connect: the zone's DNS server and its API) is ininternal/connect, Alumira's side ininternal/network. - Built-in TLS with automatic ACME (Let's Encrypt) for a domain of one's own, optional; running behind a reverse proxy is supported.
- In HTTPS the session cookie is a
__Host-one: under a shared domain, a neighbour cannot shadow it. - Signed URLs (HMAC, expiry) for manifests, segments and downloads: native players cannot always send authentication headers.
10.4 Age restrictions
- No rating systems exposed: everything is translated into a age limit. Display: (-15) in a white circle.
- Rating → age limit table built into Alumira (per country): not a setting. A rating missing from it is reported in the log when a title carries it, and gets added to the table.
- Ratings are not stored: at identification, the rating of the reference country (else the US one, else the strictest mapped one) gives the title's age limit, and only that age is kept.
- The age limit may differ per edition (e.g. unrated version).
- The user's age is computed from their birth date; if
age < min_age, the media item is invisible. - Unrated media ⇒ invisible to minors.
- Filtering applied server-side, everywhere: lists, search, cast pages, groups, direct URL access, signed URL issuing, downloads.
- Dedicated admin UI to edit the age limits of every media item, with batch actions.
11. Bandwidth & sessions
Constraint: a VOD HLS manifest is loaded once by the player; a rung cannot be removed during playback.
- Global upload budget: measured during onboarding (§13.1), then confirmed or adjusted by the admin; editable later.
- Admission when a playback session opens: the server caps the highest rung in the issued manifest according to the remaining budget.
- Per-session throttling (token bucket): under saturation, sessions are throttled and the player's ABR steps down by itself.
- Streaming > download priority: downloads only use what is left; beyond that they are queued or refused.
12. Web client & UX
12.1 Constraints
- The web client is also the future webOS / Tizen client: light bundle, conservative JS/CSS (TV browsers lag behind Chromium).
- D-pad navigation from day 1: the whole UI is usable with arrows + OK, focus always visible.
- Framework: SolidJS + TypeScript + Vite. No virtual DOM (fine-grained reactivity): better performance on weak TV CPUs, a bundle of a few KB. Conservative build target (ES2017).
- Player: hls.js on a bare
<video>, with an in-house player UI. HLS-focused (we have no DASH), light, widespread on TVs; supports ManagedMediaSource (iOS Safari). Trickplay and image subtitles are rendered by our UI. - Spatial D-pad navigation: small, imperative in-house module.
- Long lists (20k movies): grids are always virtualized.
12.2 Landing page
- Continue watching
- Recently added
- Pinned groups (order set by the admin)
- Movie list
12.3 Media page
- Full-bleed artwork, logo, overview, cast (portraits), age limit.
- Edition picker (at series level for series).
- Extras.
- Favorite.
- Download (choice of the rendition / audio / subtitle combination).
12.4 Player
- Automatic audio / subtitle selection from the preferences, changeable during playback.
- Forced subtitles applied automatically.
- Trickplay, chapters.
- Rich ASS subtitles (libass wasm) and image subtitles as an overlay.
12.5 Localization
- The UI is written in English; other languages are dictionaries keyed by the English string, loaded on demand: French, German, Spanish, Italian, Portuguese, Dutch, Russian, Japanese, Korean, Chinese. Server messages shown in the UI are translated the same way. A new account follows the browser's language when Alumira has it.
- Metadata localized in the user's language, falling back to the original language.
- Search over the original title, translated titles and alternative titles (SQLite FTS5).
12.6 Installed app and desktop app
- Installable web app: a manifest and its icons (one with Android's margin); Chrome and Edge offer "Install", Safari "Add to Dock" or "Add to Home Screen": Alumira opens in a window of its own, without the browser's bars (on an iPhone, its cookies are its own: signing in once more).
- Desktop app (
cmd/alumira-desktop, macOS and Windows, Wails v3 over the system's web engine: WebKit, WebView2): the web app in a window of its own, and an icon in the menu bar or the notification area while it runs (a click: the window; a right click: its menu). At its first start it asks: host the server here (started in the same program, on port 7575 orALUMIRA_LISTEN, its data where the command line keeps them; closing the window hides it, the server goes on for the other devices; its menu gives the address on the network, starting with the computer, the log, quitting, which stops it as an interrupt does) or use a server on the network (those heard listed, or an address typed; checked through/api/status). Missing FFmpeg, a port taken, a server that does not answer: said in the window, with what to do and a button to try again. One instance at a time; started with the computer, the icon only. On macOS, the Dock shows it while its window is open. - Found on the network: a server listening beyond its own machine says so (mDNS / DNS-SD,
_alumira._tcp, under its name, else its machine's); the desktop app lists those it hears. In Docker, only with the host's network. - Packages: a disk image for macOS (one app for Apple chips and Intel, macOS 13 and later), an installer for Windows (NSIS: Program Files, Start menu, a firewall rule for the private network, uninstallation leaving the data), built by
scripts/build-desktop.shand part ofscripts/release.sh. Both carry Alumira's FFmpeg (jellyfin-ffmpeg with VMAF, built from github.com/ejulien/jellyfin-ffmpeg), in a folderffmpegbeside the program, where Alumira looks first: nothing else to install. Not signed (no budget): macOS asks to allow it once, SmartScreen to run it anyway.
13. Administration
- Sources: add,
watch/inboxmode, scan state. - Stores: add, capacity, placement.
- Profiles: edit ladders and passes, versioning, size estimate.
- Workers: pairing, capabilities, signatures, state, drain.
- Jobs: every background task (analysis, identification, encoding passes, quality checks), by state and type, with cancel and retry.
- Normalization dashboard (real-time monitoring; no time estimate, too dependent on the number of workers):
- Catalogue: visible media / awaiting identification / awaiting pass 1 / later passes / fully normalized.
- Workers: state, signature, current job and chunk, speed (× realtime), load.
- Queues: depth per queue and per pass, global throughput (hours of content processed per hour).
- Stores: fill level, active store in the fill order.
- Recent errors, with direct retry.
- Media detail: passes, rungs, chunks, stems produced.
- Identification: fixing identifications by hand, one file or in batch (series and episode numbers, movie versions, extras).
- Catalogue: per title, its versions and extras, cast and dubbing voices, age limit, artwork; for a show, the episode mapping and its versions (§9.3).
- Collections: creation (manual / dynamic), pinning, order on the landing page.
- Age limits: dedicated UI to edit the age limit of any title, rated or not, one at a time or in batch.
- Users: invitations, roles, birth date, languages.
- Bandwidth: the budget in Settings (typed or measured); on the Status page, what is sent and to whom.
- Maintenance, in the first part of the menu: what needs a hand and what the library lacks, most urgent first, with batch actions: failed normalizations, incomplete encodes (audio tracks left out), missing sources that block an encode, sources too damaged to be encoded (to replace), insufficient quality (a rendition under 85 VMAF), likely duplicates, unidentified files, tracks in an unknown language (a language to give each), stems of another profile (§6.10), extra copies of a media in another store (§8.1), aired episodes no file brings (some can be set aside), quality not measured, subtitles missing in a default subtitle language (OpenSubtitles search, within the day's quota; translation by a language model), missing sources with nothing lost.
- Journal: every event of the server (what the terminal shows), kept on disk as JSON lines in files that turn (a size set in Settings, 100 MB by default, the oldest dropped). Each event has its grade (debug, info, warning, error), its system (server, sources, catalogue, encoding, playback, images, subtitles, AI, storage, accounts, jobs) and its lines; the page filters them by grade, system, text and period (kept in the URL), follows the new ones while its top is in view, shows the terminal's colours, links the media named to their sheet and sets sizes, durations and paths apart. What is shown downloads as text; the whole journal as a zip of its files. Debug events (every FFmpeg command among them) only when turned on in Settings, or with
ALUMIRA_DEBUG_FF=1. - Backups: scheduled database snapshots (the most valuable asset: matching, editions, groups, overrides).
- AI features, in Settings: one provider (OpenAI, Anthropic, xAI's Grok, or any API compatible with OpenAI's, a local one included: Ollama, LM Studio, vLLM…) and a model for each feature (subtitle translation, reading picture subtitles, naming chapters), chosen from the provider's list, or none. They are only called by jobs (their own queue, as many at a time as set), on demand; the account owner pays the provider. Each job shows the tokens it takes as they grow: read (those the service had cached among them) and written; the total stays in the jobs' history. A key is checked as soon as it is typed, by listing the provider's models. Transient failures (rate limits, an overloaded service) are tried again.
13.1 Onboarding
On first launch the admin goes through an onboarding that asks the key questions to get a working install. Principles:
- Every step has sensible defaults; everything can be changed later in the administration.
- The onboarding can be resumed: its state is persisted, an interruption loses nothing.
- No scan or encode starts before the final confirmation.
Steps:
- Admin account: username, password or passkey, birth date, UI language (which also sets the default audio, subtitle and metadata languages, and the country of age ratings).
- Network access: Alumira Connect in one click; or, by hand, public URL, TLS mode (built-in ACME / reverse proxy / LAN only), port.
- Stores: at least one store. Write check, free space, measured disk throughput. List order (§8.1).
- Sources: zero or more sources, with their mode (
watch/inbox). For aninboxsource, an explicit question: keep an Original copy in the stores? (yes by default, see §6.4). Quick pre-scan (number and volume of files). - Encoding: capability detection of the built-in worker (test encodes),
quality/speedchoice. - Ladder: default ladder shown, editable. Shows the storage estimate (source volume × ladder) against store capacity.
- Metadata: TMDB API key, with a link to the TMDB key creation page (opened in a new tab); metadata languages to fetch; default country for age rating mapping.
- Bandwidth: upload throughput measurement (opt-in, through an external speed test service; manual entry as a fallback), then confirmation or adjustment of the budget (§11).
- Default languages of new users (audio, subtitles).
- Backups: location and frequency of database snapshots.
- Invitations: first users (optional).
- Summary → scanning and queues start.
14. Technical stack
| Element | Choice |
|---|---|
| Server & worker | Go, imperative style (structs + functions), no cgo → static binary, trivial cross-compilation. |
| Database | SQLite (WAL), pure Go driver (modernc.org/sqlite), FTS5 for search. Thin access layer to allow Postgres later. |
| Media | FFmpeg / ffprobe as subprocesses (crash isolation, no GPL contamination), the build installed on the machine. |
| Scaling / tone-mapping | libplacebo (Vulkan / MoltenVK), zscale. |
| Packaging | HLS CMAF fMP4, dynamic manifests. |
| Client | TypeScript, SolidJS, Vite, hls.js. |
15. Target scale & orders of magnitude
Target: 20,000 movies, 1,000 users.
| Topic | Order of magnitude | Consequence |
|---|---|---|
| Storage | 20k × 10–20 GB ≈ 200–400 TB (excluding series and the Original rung) | Multiple stores and placement are essential. |
| Encoding | ~40,000 h of content | An initial import of several weeks/months → ordered passes and priorities are critical. |
| Streaming | 50–100 concurrent streams × ~15 Mbps (4K) ≈ 1–1.5 Gbps | Disk/network bound, not CPU → bandwidth control (§11). |
| Database | ~100k media entries (episodes included) | SQLite is enough with buffered progress writes. |
16. Open questions
Project licence— decided: AGPL-3.0-or-later (LICENSE), contributions under a contributor agreement (CLA.md) so that a commercial licence can also be offered; "Alumira" is a trademark. FFmpeg stays a separate program, neither linked nor shipped, so its licence (GPL with x264/x265) does not extend to Alumira; each distribution carriesTHIRD_PARTY_LICENSES.md(generated byscripts/notices). Still to settle before any paid offer: TMDB's commercial licence and the IMDb datasets, which are non-commercial (§5.1).