Shadowmask is a self-hosted media library and streaming server for movies and series. It scans your media files, recognizes and organizes them, enriches them with metadata, artwork and subtitles, and streams to multiple players with per-user accounts and access control.
Streaming and playback
- HLS adaptive streaming, direct play, and remux when the codecs already fit
- Hardware-accelerated transcoding with software fallback, and HDR to SDR tone mapping
- Resume across devices, and a per-user bitrate cap
- Multi-track audio, soft (WebVTT) and burned-in subtitles, plus external subtitle files
- Per-user, per-track subtitle sync offset, and admin-derived bilingual stacked subtitle tracks
- Chapter navigation, trickplay seek thumbnails, and autoplay of the next episode
Library and metadata
- Movies, series, seasons, episodes and collections, with automatic recognition and a manual resolution queue for what it cannot match
- Metadata from OMDB and TMDB; subtitles from OpenSubtitles
- Poster, backdrop, banner, logo and clearart variants, resized and served per request
- Content ratings as a normalized field, driving parental controls
- Per-library watcher strategies (local events, polling, scheduled, manual), plus duplicate detection
Discovery
- Full-text search across titles, people and genres
- Continue watching, next up, and composed home hubs
Optional
- Local AI enrichment: transcription and translation, run on your own hardware. See
deployment/ENRICHMENT.md. - Remote content fetch through yt-dlp
| Home | Movies |
|---|---|
![]() |
![]() |
| Movie details | Series details |
|---|---|
![]() |
![]() |
| Player | Version admin |
|---|---|
![]() |
![]() |
On a phone, in each of the three themes:
| Dark | Light | Retro |
|---|---|---|
![]() |
![]() |
![]() |
git clone https://github.com/sndnv/shadowmask.git
cd shadowmask/deployment/production
cp secrets/templates/shadowmask.env.template secrets/shadowmask.env
Fill in SHADOWMASK_JWT_SECRET and SHADOWMASK_STREAM_SECRET (openssl rand -hex 32 for each) and
set an admin password, then point the stack at your libraries and at the address browsers will use
to reach it:
export SHADOWMASK_MOVIES_DIR=/path/to/movies
export SHADOWMASK_TV_DIR=/path/to/tv
export SHADOWMASK_API_BASE=http://192.168.1.10:8080
export SHADOWMASK_WEB_UI_ORIGIN=http://192.168.1.10:8090
docker compose up -d
Both addresses are what the browser resolves, so use the host's address rather than localhost
unless you only ever browse from the host itself.
The server listens on port 8080 and the web UI on 8090; /health reports readiness. See
deployment/production/README.md for TLS, bootstrap, hardware
acceleration and the rest.
- Flutter (
clients/flutter) - the primary interface, one codebase covering the web, the desktop (macOS and Linux) and mobile (Android and iOS). The web build is published asghcr.io/sndnv/shadowmask-web-ui; the desktop ships as a macOS.dmgand a Linux.AppImage; mobile ships as a signed Android.apk, and as an unsigned iOS.ipathat has to be signed at install time with a tool such as Sideloadly or AltStore - Roku (
clients/roku) - a SceneGraph channel for Roku devices. Viewer-only, with no admin surface, and paired by link code so no password is typed on the remote. It ships as a channel zip, installed by sideloading - Basic (
clients/basic) - a dependency-free HTML/JS client served by the server itself at/ui/basic/, useful as a fallback and for debugging
The desktop, mobile and Roku clients are attached to every release.
- Web - open the web UI's address, port 8090 in the quick start above
- Basic - open
/ui/basic/on the server's address, for examplehttp://192.168.1.10:8080/ui/basic/ - macOS - open
shadowmask-client-macos-<version>.dmgand drag the app toApplications. macOS refuses the first open: open it once, then chooseOpen AnywayunderSystem Settings>Privacy & Security. If the default has display issues, useshadowmask-client-macos-<version>-skia.dmginstead - Linux - install libmpv (
sudo apt install libmpv-dev mpvon Debian and Ubuntu), then makeshadowmask-client-linux-<version>.AppImageexecutable and run it.Account>Profile>Applicationsmenu adds it to the desktop's applications menu - Android - open
shadowmask-client-android-<version>.apkon the device and allow installing apps from that source - iOS - sideload
shadowmask-client-ios-<version>.ipawith Sideloadly or AltStore and your Apple ID. With a free Apple ID it has to be signed again every 7 days - Roku - turn on developer mode,
open the device's address in a browser, sign in as
rokudevwith the developer password, then uploadshadowmask-client-roku-<version>.zipand install it
GitHub releases are immutable: no file can be replaced once a release is published. To check a download against that record, with the GitHub CLI signed in:
gh release verify-asset <version> shadowmask-client-linux-<version>.AppImage --repo sndnv/shadowmaskWithout the GitHub CLI, compare the file's digest (sha256sum <file> on Linux,
shasum -a 256 <file> on macOS) with the one shown next to it on the release page.
The desktop, mobile and Roku clients ask for the server's address first, for example
http://192.168.1.10:8080. Sign in with a username and password, or with a link code: create one
under Account > Devices & Access in a client that is signed in, then enter it on the new device.
Roku signs in with a link code only.
A Cargo workspace where every crate depends inward on domain:
domain- core entities, traits, and errors; zero infrastructure dependenciespersistence- SQLite (sqlx) implementations of the repository traitsmedia- FFmpeg/ffprobe, transcoding, HLS, and capability negotiationmetadata- external metadata and subtitle clients (OMDB, TMDB, OpenSubtitles)inference- optional local AI enrichment (transcription, translation)fetch- optional remote content retrieval via yt-dlpservices- business logic for catalog, library, streaming, and usersjobs- background job queue, workers, and schedulerapi- axum HTTP router, handlers, DTOs, and auth/RBACserver- the binary; composition root, config, and bootstrapmocks- in-memory trait implementations used by testscontracts- shared contract tests every repository implementation must pass
Refer to the DEVELOPMENT.md file for more details.
Contributions are always welcome!
Refer to the CONTRIBUTING.md file for more details.
Refer to the SECURITY.md file for how to report a vulnerability.
Shadowmask is self-hosted software, not a service: there is no telemetry, no analytics and no account with us, and every outbound integration is off until an operator turns it on. Refer to the PRIVACY.md file for what is stored and what leaves your server.
We use SemVer for versioning.
- The Rust crates linked into the server binary are credited, with their licence texts, in
THIRD-PARTY-LICENSES.md. It is generated fromCargo.lockbycargo-about;qa.pyfails when it drifts. - The published server images carry that file, this project's
LICENSE, and a Debiancopyrightfile for every packaged dependency.deployment/SERVER-IMAGE-CREDITS.mdlists where each sits inside the image, and why the GPL-2+ FFmpeg they install stays separate from Shadowmask's code. - hls.js is bundled by both web clients for HLS playback,
under the Apache License 2.0. Its bundle inlines
url-toolkit(Apache-2.0) andeventemitter3(MIT); all three notices are inassets/vendor/hls.js.LICENSE.txt, distributed alongside the script. - hash-wasm (its argon2 build) is bundled by the basic web
client to digest passwords, under the MIT License. Its WebAssembly is based on Go's argon2
(BSD-3-Clause); both notices are in
assets/vendor/hash-wasm.LICENSE.txt, distributed alongside the script. - Roboto is bundled by the Flutter clients, under
the SIL Open Font License 1.1, with its licence distributed alongside the fonts at
assets/fonts/Roboto-OFL.txt. - Noto Sans CJK SC (Regular and Medium) is downloaded into the server image and served to the Roku client, under the SIL Open Font License 1.1, with its licence beside the fonts.
- The macOS and iOS applications bundle mpv (LGPL-2.1-or-later), FFmpeg (LGPL-3.0-or-later) and nine
supporting libraries, each as a separately replaceable dynamically linked framework. Android
bundles a different build of most of the same libraries, linked statically into one replaceable
libmpv.soper ABI. Both are credited inclients/flutter/CREDITS.md, with their licence texts underlicenses/. The Linux application links the distribution's own libmpv and bundles none of them. - The enrichment image statically links C++ source vendored by the
ct2rsandsentencepiece-syscrates. Every licence text in those sources is collected underlicenses/enrichment/; the operator's model licensing responsibilities are described indeployment/ENRICHMENT.md. - Test fixtures and dev-deployment media clips are credited in
crates/media/tests/fixtures/CREDITS.mdanddeployment/dev/CREDITS.md. - Metadata and artwork are fetched from TMDB and subtitles from OpenSubtitles, each through an API key the operator supplies. This product uses TMDB and the TMDB APIs but is not endorsed, certified, or otherwise approved by TMDB. The same notice, with TMDB's logo, appears in every client's About screen.
- Ratings and content certifications are supplemented by OMDb, licensed under CC BY-NC 4.0 and not endorsed by or affiliated with IMDb.com. The NonCommercial term follows the operator's key, so a commercial deployment should leave it unset.
- Every client also shows its attributions in the application itself: the desktop and mobile clients
under Account → Profile → About, the Roku channel in the same place, and the basic web client
through the footer link to
about.html.
This project is licensed under the Apache License, Version 2.0 - see the LICENSE file for details.
Copyright 2026 https://github.com/sndnv
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.








