10 KiB
Sortarr Project Info
Purpose: self-hosted Jellyfin ecosystem organizer and dashboard, fully editable and Docker Compose runnable. It watches downloads, plans/moves media into Jellyfin-friendly folders across four media drives, displays storage/library/download/release status, and exposes configurable tools such as subtitle audit and ffmpeg transcoding.
Runtime
- Root:
/home/drop/jellyfin/scripts/sortarr - Web UI:
http://localhost:8088or host LAN IP on port8088 - Backend API: port
8099 - Compose files:
compose.yaml,compose.override.yaml,compose.prod.yaml - Env file:
.env - Default dry-run: enabled via
SORTARR_DRY_RUN=true - Active containers:
sortarr-web,sortarr-backend - Known unrelated/orphan container:
sortarrmay still appear restarting from an older compose shape.
Host Paths
Configured in .env:
- Downloads:
/home/drop/jellyfin/downloadsmounted as/downloads - Media drive 1:
/home/drop/jellyfin/mediashare1mounted as/media/drive1 - Media drive 2:
/home/drop/jellyfin/mediashare2mounted as/media/drive2 - Media drive 3:
/home/drop/jellyfin/mediashare3mounted as/media/drive3 - Media drive 4:
/home/drop/jellyfin/mediashare4mounted as/media/drive4 - Config:
/home/drop/jellyfin/scripts/sortarr/config - Logs:
/home/drop/jellyfin/scripts/sortarr/logs - Data/state:
/home/drop/jellyfin/scripts/sortarr/data
Architecture
web: nginx serves static HTML/CSS/JS fromweb/srcand proxies/api/*to backend.backend: Python 3.12 stdlib HTTP API plus background scanner thread. Backend image installsffmpeg.- Optional profiles:
redisprofilecachepostgresprofiledatabasemedia-toolsprofiletools
No frontend framework and no backend web framework are used. This is intentional for editability.
Important Files
.env.example: sample deployment variables..env: real local deployment paths and runtime values. Ignored by git.compose.yaml: main stack.compose.override.yaml: dev bind mounts and debug defaults.compose.prod.yaml: prod restart/dry-run defaults.backend/default-config/app.toml: full default config.config/app.toml: host-editable override config.config/custom-theme.css: host-editable CSS token overrides.backend/sortarr/app.py: API server and route handlers.backend/sortarr/config.py: TOML/env config loading and merging.backend/sortarr/scanner.py: 24/7 downloads scanner thread.backend/sortarr/parser.py: filename media parser.backend/sortarr/organizer.py: destination planning, collision handling, move execution, NFO writing.backend/sortarr/storage.py: drive stats and drive selection.backend/sortarr/library.py: explicit library scan/indexing and Movies/TV collection grouping.backend/sortarr/metadata.py: optional TMDb metadata lookup for covers, summaries, and TV episode lists.backend/sortarr/media_probe.py: safe ffprobe wrapper for audio/subtitle/video stream details.backend/sortarr/tools.py: subtitle audit and transcoder tools.backend/sortarr/downloads.py: current/downloadslisting and recent moved/planned download history.backend/sortarr/releases.py: free RSS/JSON upcoming release providers.backend/sortarr/store.py: JSON state store indata/state.json.web/src/index.html: app shell and page markup.web/src/app.js: hash router, API calls, rendering, settings/tools behavior.web/src/styles.css: layout/design system.web/src/themes.css: 10 editable theme presets.docs/*.md: API/config/operations docs.
Configuration Model
Config precedence:
backend/default-config/app.tomlconfig/app.toml.envvariables passed into Compose- Runtime settings saved in
data/state.jsonundersettings
Key config areas:
[app]: dry-run, scan interval, settle time, log level, extensions, incomplete suffixes, library scan limits, cache size cap.[paths]: downloads/data/logs/cache container paths.[[drives]]: four media drives with id/name/path/min-free-space.[library]: folder and filename templates, collision policy, permissions mode.[metadata]: NFO behavior and optional TMDb credentials/settings.[[release_providers]]: free RSS/JSON providers.[theme]: default theme and custom CSS.
Runtime Settings page can update:
dry_runscan_interval_secondssettle_secondslibrary_scan_max_fileslibrary_scan_timeout_secondslog_level
Media Organizer Behavior
Background scanner watches /downloads continuously.
Safety:
- Ignores incomplete suffixes such as
.part,.!qB,.tmp,.crdownload. - Requires files to be stable for
settle_seconds. - Dry-run plans moves without moving.
- Actual moves go through a temporary
.sortingpath before final rename. - Collision policies:
keep-both,skip,replace. - Events and plans are stored in
data/state.json.
Parsing:
- Detects movies, episodes, seasons, and multi-episode releases.
- Recognizes
S01E02,S01E02E03, and1x02style episode patterns. - Extracts year and quality tokens where present.
Drive choice:
- Checks whether the title already has a home under
MoviesorShows. - If no home exists, picks eligible drive with most free space.
- Enforces
min_free_gb.
Naming:
- Movies:
Movies/{title} ({year})/{title} ({year}){quality}{ext} - Episodes:
Shows/{title}/Season {season:02d}/{title} - SxxExx - Episode{quality}{ext} - Templates are editable in TOML.
Library Indexing
Regular dashboard refresh does not walk the media filesystem.
Library indexing is explicit:
- UI button: Library page ->
Scan library - API:
POST /api/library/scan - Scans only direct child folders of each media drive named:
MoviesShowsTVTV Shows
The library scanner skips system/recycle folders and has timeout/file-count limits. Results are cached in data/state.json and used by dashboard/tools.
Current cache fields include:
- drive stats
- indexed media items split by
MoviesandTV/TV Showsroots - collection groups for movies and TV series
- optional TMDb posters, overviews, and TV season episode metadata
- extension breakdown
- scanned file count
- truncation flag
- per-media
has_subtitleswhen available from scan
Frontend Pages
The UI uses hash routing in web/src/app.js.
Routes:
#/overview: storage, file type breakdown, recent events.#/library: poster grid with All/Movies/TV Shows tabs, series/episode drilldown, missing/upcoming episode state, and media stream inspection.#/downloads: current/downloadsmedia bundles with matching subtitles/sidecars plus recent Sortarr plans/moves from/downloads.#/releases: missing/upcoming library episodes plus configured public providers.#/tools: transcoder, subtitle audit, duplicate finder placeholder.#/settings: appearance controls, descriptive runtime controls, raw config details.
Theme system:
- Theme choices live on the Settings page and persist in
localStorage. - Compact density toggle persists in
localStorage. - Presets:
slate,midnight,graphite,nord,dracula,solar,forest,marine,ember,paper. - Tokens live in
web/src/themes.css; host overrides inconfig/custom-theme.css.
Backend API
GET /api/health: healthcheck.GET /api/config: public config with secrets removed.GET /api/dashboard: state + cached library + drive stats; no filesystem library scan.POST /api/scan: run one downloads scan now.POST /api/library/scan: refresh cached library index.GET /api/downloads: current/downloadsfiles plus recent planned/moved download history.GET /api/releases: upcoming releases.GET /api/media/probe: ffprobe stream details for a selected file.POST /api/media/tracks: dry-run or execute ffmpeg remux track default/removal changes.GET /api/theme/custom.css: custom CSS.POST /api/settings: update runtime settings.GET /api/tools/subtitles: subtitle audit from cached library data.GET /api/tools/transcoder: build ffmpeg transcode queue from cached library.POST /api/tools/transcoder/run-next: run next ffmpeg transcode if dry-run is disabled.
Tools
Subtitle audit:
- Uses cached library index, not live filesystem probes.
- Requires a fresh library scan for accurate
has_subtitles. - Reports checked count, with-subtitles count, missing count, unknown count, and missing examples.
Transcoder:
- Backend image installs
ffmpeg. - Queue includes cached indexed media not already
.mp4. - Output path is source path with
.mp4suffix. - Command uses
libx264,aac, andmov_text. - In dry-run mode,
run-nextreports without executing. - With dry-run disabled, runs one job synchronously with a 1 hour timeout.
Duplicate finder:
- Reports duplicate title groups from the cached library index.
Release Providers
No paid API dependency.
Bundled providers, disabled by default so the Releases page stays centered on the local library:
- TMDb RSS upcoming movies.
- TVMaze public schedule JSON.
Provider logic is in backend/sortarr/releases.py; add new RSS/JSON adapters there and configure in TOML.
Verification Commands
Common checks:
python -m compileall backend/sortarr
node --check web/src/app.js
docker compose config
docker compose up -d --build
docker exec sortarr-backend python -m sortarr.healthcheck
docker exec sortarr-backend ffmpeg -version
Endpoint checks from inside backend:
docker exec sortarr-backend python -c "from urllib.request import urlopen; print(urlopen('http://127.0.0.1:8099/api/health').status)"
docker exec sortarr-backend python -c "from urllib.request import urlopen; import json; print(json.load(urlopen('http://127.0.0.1:8099/api/tools/transcoder'))['transcoder']['ffmpeg_available'])"
Current Caveats / Next Good Tasks
- Settings are runtime/persisted in JSON state but not written back into
config/app.toml. - Transcoding runs synchronously; future improvement should add a job queue with progress/cancel/history.
- Duplicate finder reports duplicate title groups from the cached library index.
- Subtitle audit only becomes exact after a fresh manual library scan because it relies on cached
has_subtitles. - Library scan only checks direct child folders named
Movies,TV, orTV Showsunder each media drive. - Backend is stdlib HTTP server; fine for self-hosting behind LAN/reverse proxy, but add auth before exposing publicly.