A self-hosted Windows media library and web portal for movies, series, and audio. Browse your local folders, discover new titles via TMDB/OMDb, search and download subtitles, and stream to any device on your local network. Built-in AI automatically detects and filters NSFW and inappropriate scenes.
git clone https://github.com/levanter-dev/Diwan-Media-Server.git
cd Diwan-Media-Server
copy .env.example .env
Edit .env and add your API keys (see API Keys below).
At minimum you will want a TMDB token for the Explore page.
scripts\01-setup.bat
scripts\02-run-development.bat
Open http://localhost:8080 in your browser.
On other devices, open http://<your-pc-ip>:8080 (e.g. http://192.168.1.50:8080).
To use a friendly name instead of an IP, set DOMAIN=diwan.local in your .env
file. The server automatically advertises the name via mDNS (Bonjour/Zeroconf) so
any device on your network can reach it at http://diwan.local:8080 - no hosts
file editing or router config required.
Use the :8080 form unless the server reports that it successfully opened port
80. Some webOS versions do not resolve mDNS names; in that case use the displayed
LAN IP or add diwan.local to your router's local DNS. A name that does not end in
.local always needs a router/DNS/hosts record.
Press Ctrl+C to stop both servers.
Browser / TV / Phone
|
| http://server-pc:8080
v
Node portal (serves web UI)
|
| http://127.0.0.1:8081/api
v
Python FastAPI server
|-- Windows drives and folders
|-- SQLite database
|-- FFmpeg / FFprobe
|-- Media scanning and jobs
Development uses two processes: Python FastAPI (port 8081) and Node.js (port 8080).
The packaged .exe combines both into a single process.
Diwan uses three external services. All are free.
Used for search, posters, metadata, and discovery rows (trending, popular, now playing).
eyJ....env: TMDB_TOKEN=eyJ...This product uses the TMDB API but is not endorsed or certified by TMDB.
Alternative search engine.
.env: OMDB_API_KEY=your-keySubtitle search and download.
.env:
OPENSUBTITLES_USERNAME=your-username
OPENSUBTITLES_PASSWORD=your-password
OPENSUBTITLES_API_KEY=your-api-key
Credentials are stored in SQLite. The browser only sees configured/not configured status.
Diwan uses on-device AI to detect and handle inappropriate scenes - no files are uploaded to any cloud service. Two AI models run locally:
| Category | Detector | Description |
|---|---|---|
| Sexual activity | OpenCLIP | Explicit sexual content in a scene |
| Female toplessness | NudeNet | Exposed female chest |
| Male toplessness | NudeNet | Exposed male chest |
| General nudity | NudeNet | Any exposed private parts or buttocks |
| Kissing | OpenCLIP | Romantic kissing scenes |
| Revealing attire / swimwear | OpenCLIP | Scantily clad or swimwear scenes |
Each category can be independently set to one of four modes:
| Action | Behavior |
|---|---|
| Off | Category is ignored |
| Marker | Shows colored markers on the playback timeline |
| Warn | Displays a warning overlay before the scene; viewer can choose to skip or continue |
| Skip | Automatically jumps past the detected scene with a brief notice |
Three sensitivity levels control the detection threshold: Low (fewer false positives), Balanced, and High (catches more borderline scenes).
Settings can choose the global analysis model, and each media details page can override that model before re-filtering a title.
| Model | Best use |
|---|---|
| NudeNet + OpenCLIP | Broadest coverage: anatomy plus scene context |
| NudeNet only | More predictable body-part detection with fewer context false positives |
| NudeNet strict private-parts | Highest precision for generic nudity; breasts stay separate from private-parts nudity |
| OpenCLIP prompts only | Experimental markers for scene context; avoid for automatic skips |
| GantMan NSFW model | Optional broad NSFW classifier; requires nsfw_detector, TensorFlow, and NSFW_MODEL_PATH |
| HaramBlur-style NSFWJS | Server-side NSFWJS/GantMan-style classifier profile; HaramBlur itself also uses browser face detection through Human.js |
| FalconsAI NSFW ViT | Strong independent binary NSFW classifier; downloads from Hugging Face on first use |
| FalconsAI NSFW ViT 2026 | Newer gated classifier; accept its model terms and authenticate with HF_TOKEN or hf auth login |
The confirmation sandbox can require multiple model profiles to agree within a time window before a detected segment is used for automatic skipping. A single model can still pass if its confidence is above the configured high-confidence threshold. Media pages can also run all installed model profiles, compare raw segments by model, select any completed combination to drive playback, and manually verify an exact time range as blocked or safe. Selected model timelines are combined before the confirmation rule is applied.
The media filter card also includes a guided unsafe-scene review. It plays only the detected ranges in chronological order, pauses at each scene boundary, and records a Safe or Not safe verdict with an exact scene number. Review analytics group selected-model detections into confidence bands and show the observed false-positive rate for each band and model; unreviewed detections are excluded.
Filter export/import uses the v2 format and includes the global confirmation settings, per-media model overrides, raw per-model timelines, and manual verification ranges.
Detection is intentionally precision-first for automatic skips and requires corroborating frames. No visual classifier can promise 100% accuracy, so detected scenes can be marked safe and automatic skipping can be replaced with warnings or timeline markers for categories where false positives are unacceptable.
Settings can export a JSON filter file containing the policy and all prepared scene timelines. Importing it on another Diwan server matches media by filename and size, allowing a fast computer to do the analysis for a lower-powered one.
Content analysis runs significantly faster with an NVIDIA GPU (CUDA). On CPU-only systems, analysis still works but takes longer. The Settings page shows whether GPU acceleration is available.
During playback, unsupported containers/codecs are converted to an H.264/AAC compatibility stream for webOS and other TVs. This is server-side playback compatibility; the custom controls alone cannot add codec support to a TV.
All settings go in .env. See .env.example for the full list.
| Variable | Default | Description |
|---|---|---|
TMDB_TOKEN | - | TMDB API Read Access Token |
OMDB_API_KEY | - | OMDb API key |
OPENSUBTITLES_USERNAME | - | OpenSubtitles username |
OPENSUBTITLES_PASSWORD | - | OpenSubtitles password |
OPENSUBTITLES_API_KEY | - | OpenSubtitles consumer API key |
PORT | 8080 | Web portal port |
DOMAIN | - | Custom local domain (e.g. diwan.local) |
MEDIA_ROOTS | All drives | Comma-separated folder paths |
DATA_DIR | AppData | Database and config location |
To create a standalone .exe:
ffmpeg.exe and ffprobe.exe in vendor/ffmpeg/bin/scripts\03-build-exe.bat
scripts\04-install-local.bat
scripts\05-run-installed.bat
The server auto-starts at logon. To remove:
scripts\90-uninstall-local.bat
app/ FastAPI backend (database, scanner, scrapers, analysis)
web/ Browser SPA + Node dev server
scripts/ Setup, run, build, install, uninstall
vendor/ffmpeg/ Place ffmpeg.exe + ffprobe.exe here
native_server.py Entry point for the native Windows executable
native_server.spec PyInstaller packaging definition
requirements.txt Python dependencies
requirements-build.txt Build dependencies
.env.example Config template
Python
49.7%
HTML
39.7%
JavaScript
7.6%
CSS
2.0%
A self-hosted Windows media library and web portal for movies, series, and audio. Browse your local folders, discover new titles via TMDB/OMDb, search and download subtitles, and stream to any device on your local network. Built-in AI automatically detects and filters NSFW and inappropriate scenes.
git clone https://github.com/levanter-dev/Diwan-Media-Server.git
cd Diwan-Media-Server
copy .env.example .env
Edit .env and add your API keys (see API Keys below).
At minimum you will want a TMDB token for the Explore page.
scripts\01-setup.bat
scripts\02-run-development.bat
Open http://localhost:8080 in your browser.
On other devices, open http://<your-pc-ip>:8080 (e.g. http://192.168.1.50:8080).
To use a friendly name instead of an IP, set DOMAIN=diwan.local in your .env
file. The server automatically advertises the name via mDNS (Bonjour/Zeroconf) so
any device on your network can reach it at http://diwan.local:8080 - no hosts
file editing or router config required.
Use the :8080 form unless the server reports that it successfully opened port
80. Some webOS versions do not resolve mDNS names; in that case use the displayed
LAN IP or add diwan.local to your router's local DNS. A name that does not end in
.local always needs a router/DNS/hosts record.
Press Ctrl+C to stop both servers.
Browser / TV / Phone
|
| http://server-pc:8080
v
Node portal (serves web UI)
|
| http://127.0.0.1:8081/api
v
Python FastAPI server
|-- Windows drives and folders
|-- SQLite database
|-- FFmpeg / FFprobe
|-- Media scanning and jobs
Development uses two processes: Python FastAPI (port 8081) and Node.js (port 8080).
The packaged .exe combines both into a single process.
Diwan uses three external services. All are free.
Used for search, posters, metadata, and discovery rows (trending, popular, now playing).
eyJ....env: TMDB_TOKEN=eyJ...This product uses the TMDB API but is not endorsed or certified by TMDB.
Alternative search engine.
.env: OMDB_API_KEY=your-keySubtitle search and download.
.env:
OPENSUBTITLES_USERNAME=your-username
OPENSUBTITLES_PASSWORD=your-password
OPENSUBTITLES_API_KEY=your-api-key
Credentials are stored in SQLite. The browser only sees configured/not configured status.
Diwan uses on-device AI to detect and handle inappropriate scenes - no files are uploaded to any cloud service. Two AI models run locally:
| Category | Detector | Description |
|---|---|---|
| Sexual activity | OpenCLIP | Explicit sexual content in a scene |
| Female toplessness | NudeNet | Exposed female chest |
| Male toplessness | NudeNet | Exposed male chest |
| General nudity | NudeNet | Any exposed private parts or buttocks |
| Kissing | OpenCLIP | Romantic kissing scenes |
| Revealing attire / swimwear | OpenCLIP | Scantily clad or swimwear scenes |
Each category can be independently set to one of four modes:
| Action | Behavior |
|---|---|
| Off | Category is ignored |
| Marker | Shows colored markers on the playback timeline |
| Warn | Displays a warning overlay before the scene; viewer can choose to skip or continue |
| Skip | Automatically jumps past the detected scene with a brief notice |
Three sensitivity levels control the detection threshold: Low (fewer false positives), Balanced, and High (catches more borderline scenes).
Settings can choose the global analysis model, and each media details page can override that model before re-filtering a title.
| Model | Best use |
|---|---|
| NudeNet + OpenCLIP | Broadest coverage: anatomy plus scene context |
| NudeNet only | More predictable body-part detection with fewer context false positives |
| NudeNet strict private-parts | Highest precision for generic nudity; breasts stay separate from private-parts nudity |
| OpenCLIP prompts only | Experimental markers for scene context; avoid for automatic skips |
| GantMan NSFW model | Optional broad NSFW classifier; requires nsfw_detector, TensorFlow, and NSFW_MODEL_PATH |
| HaramBlur-style NSFWJS | Server-side NSFWJS/GantMan-style classifier profile; HaramBlur itself also uses browser face detection through Human.js |
| FalconsAI NSFW ViT | Strong independent binary NSFW classifier; downloads from Hugging Face on first use |
| FalconsAI NSFW ViT 2026 | Newer gated classifier; accept its model terms and authenticate with HF_TOKEN or hf auth login |
The confirmation sandbox can require multiple model profiles to agree within a time window before a detected segment is used for automatic skipping. A single model can still pass if its confidence is above the configured high-confidence threshold. Media pages can also run all installed model profiles, compare raw segments by model, select any completed combination to drive playback, and manually verify an exact time range as blocked or safe. Selected model timelines are combined before the confirmation rule is applied.
The media filter card also includes a guided unsafe-scene review. It plays only the detected ranges in chronological order, pauses at each scene boundary, and records a Safe or Not safe verdict with an exact scene number. Review analytics group selected-model detections into confidence bands and show the observed false-positive rate for each band and model; unreviewed detections are excluded.
Filter export/import uses the v2 format and includes the global confirmation settings, per-media model overrides, raw per-model timelines, and manual verification ranges.
Detection is intentionally precision-first for automatic skips and requires corroborating frames. No visual classifier can promise 100% accuracy, so detected scenes can be marked safe and automatic skipping can be replaced with warnings or timeline markers for categories where false positives are unacceptable.
Settings can export a JSON filter file containing the policy and all prepared scene timelines. Importing it on another Diwan server matches media by filename and size, allowing a fast computer to do the analysis for a lower-powered one.
Content analysis runs significantly faster with an NVIDIA GPU (CUDA). On CPU-only systems, analysis still works but takes longer. The Settings page shows whether GPU acceleration is available.
During playback, unsupported containers/codecs are converted to an H.264/AAC compatibility stream for webOS and other TVs. This is server-side playback compatibility; the custom controls alone cannot add codec support to a TV.
All settings go in .env. See .env.example for the full list.
| Variable | Default | Description |
|---|---|---|
TMDB_TOKEN | - | TMDB API Read Access Token |
OMDB_API_KEY | - | OMDb API key |
OPENSUBTITLES_USERNAME | - | OpenSubtitles username |
OPENSUBTITLES_PASSWORD | - | OpenSubtitles password |
OPENSUBTITLES_API_KEY | - | OpenSubtitles consumer API key |
PORT | 8080 | Web portal port |
DOMAIN | - | Custom local domain (e.g. diwan.local) |
MEDIA_ROOTS | All drives | Comma-separated folder paths |
DATA_DIR | AppData | Database and config location |
To create a standalone .exe:
ffmpeg.exe and ffprobe.exe in vendor/ffmpeg/bin/scripts\03-build-exe.bat
scripts\04-install-local.bat
scripts\05-run-installed.bat
The server auto-starts at logon. To remove:
scripts\90-uninstall-local.bat
app/ FastAPI backend (database, scanner, scrapers, analysis)
web/ Browser SPA + Node dev server
scripts/ Setup, run, build, install, uninstall
vendor/ffmpeg/ Place ffmpeg.exe + ffprobe.exe here
native_server.py Entry point for the native Windows executable
native_server.spec PyInstaller packaging definition
requirements.txt Python dependencies
requirements-build.txt Build dependencies
.env.example Config template
Python
49.7%
HTML
39.7%
JavaScript
7.6%
CSS
2.0%