Media Stack

Home

Services running on PCT 102 (media-core, 192.168.2.191). Stack: media-core (compose 26).


Service URLs

Service URL Port
Jellyfin jellyfin.carr-family.org 8096 (standalone, static route)
Sonarr sonarr.carr-family.org 8989
Radarr radarr.carr-family.org 7878
Prowlarr prowlarr.carr-family.org 9696
Jellyseerr seerr.carr-family.org 5055
Calibre-Web Automated book.carr-family.org 8083
Audiobookshelf audiobook.carr-family.org 13378
Audiobookrequest abr.carr-family.org 8000
Shelfmark shelfmark.carr-family.org 8084
OpenBooks openbooks.carr-family.org 8875
Komga komga.carr-family.org 25600
RomM romm.carr-family.org 8984
FlareSolverr (internal only) 8191

Storage Layout

Path Contents
/mnt/tank/media/content/movies Movies
/mnt/tank/media/content/tv TV shows
/mnt/tank/media/content/books Calibre library (managed by CWA — never move files manually)
/mnt/tank/media/content/books-seeds MAM torrent seeding copies (never touch)
/mnt/tank/media/content/audiobooks Audiobookshelf + Shelfmark audiobook downloads
/mnt/tank/media/content/comics Komga library
/mnt/tank/media/content/roms RomM ROM library (organize by platform subfolder)
/mnt/tank/media/bookingest CWA ingest folder + Shelfmark ebook downloads

Calibre-Web Automated (CWA)

Volumes: /mnt/tank/media/bookingest:/cwa-book-ingest, /mnt/tank/media/content/books:/calibre-library

Config: NETWORK_SHARE_MODE=true, CWA_WATCH_MODE=poll

Ingest mount gotcha — verify with docker inspect <container> | grep Mounts. If CWA sees an empty ingest folder after a compose update, the container wasn't redeployed. Fix:

pct exec 108 -- docker stack deploy -c /mnt/tank/appdata/portainer/compose/26/docker-compose.yml media-core

root:root ownership breaks deletes — CWA web server runs as abc (uid 1000) but the ingest processor runs as root, and NETWORK_SHARE_MODE=true skips the post-import chown. Files land as root:root and abc gets [Errno 13] Permission denied on delete. Fix existing library:

pct exec 102 -- chown -R 1000:1000 /mnt/tank/media/content/books

The MAM hardlink script runs a background chown 5 minutes after each torrent completes to keep future imports clean.

Bulk-delete always shows success (CWA bug)/ajax/deleteselectedbooks always returns {"success": true}. Check container logs for Deleting book X failed if books reappear. config_calibre_split must be 0 in app.db — if 1 with wrong dir, all cover lookups silently fail.

Stale thumbnails: DELETE FROM thumbnail; in app.db to force regeneration. Cover generation runs nightly at 04:00.

Don't bulk-delete duplicates while ingest is running — drop files into bookingest, wait for CWA to finish all of them, then delete. Deleting mid-ingest causes silent EPERM failures.

Calibre Email Setup

  • SMTP hostname: 192.168.2.83 (Proton Bridge on PCT 109)
  • Port: 1025, Encryption: None
  • Login: [email protected], Password: 8A2SC9qao04GsSqBrfjtFg

calibredb Commands

# Get CWA container name
pct exec 102 -- docker ps --format '{{.Names}}' | grep calibre

# List all books as JSON
pct exec 102 -- docker exec <cwa-container> calibredb list \
  --fields=id,title,authors,series,series_index,tags,publisher,formats \
  --sort-by=title -s '' --library-path=/calibre-library --for-machine

# Remove by ID (deletes metadata + files; does NOT touch books-seeds)
pct exec 102 -- docker exec <cwa-container> calibredb remove \
  --library-path=/calibre-library <id1>,<id2>

Calibre Deduplication

Always keep the entry with the most metadata (series, series_index, tags, publisher). Never touch /mnt/tank/media/content/books-seeds.

Known duplicate patterns (exact-title matching misses these):

Pattern Example
Series prefix "Mistborn 02 - The Well of Ascension" vs "The Well of Ascension"
Subtitle append "After the Funeral: A Hercule Poirot Mystery" vs "After the Funeral"
Author punctuation "J. K. Rowling" vs "J.K. Rowling"
Series tag in title "Famine (The Four Horsemen Book 3)" vs "Famine"

Calibre has a large Agatha Christie / Hercule Poirot set where every book exists twice: once as "Title" (bare) and once as "Title: A Hercule Poirot Mystery" (with series + series_index). Always keep the latter.


Calibre Library Cleanup

Junk patterns to watch for:

Pattern Search term Notes
Comics — Spider-Man spider-man Title starts with Amazing Spider-Man
Comics — Bone bone + author Jeff Smith Individual chapter volumes
Lonely Planet (long) lonely planet Author is Lonely Planet or Planet, Lonely & ...
Lonely Planet (short) LP Title starts with LP , author Unknown
D&D — Forgotten Realms forgotten realms Title starts with Forgotten Realms -
Rough Guides rough guide Travel guides
Torrent metadata anonamouse Titled Torrent_downloaded_from_anonamouse.net
RPG character sheets character sheet e.g. Lone Wolf character sheets
Bare index files index Title is exactly index

Bulk-remove by keyword (filters precisely before deleting):

IDS=$(pct exec 102 -- docker exec <cwa-container> calibredb list \
  --fields=id,title --sort-by=title -s '<keyword>' \
  --library-path=/calibre-library --for-machine | python3 -c "
import json,sys
data=json.load(sys.stdin)
ids=[str(b['id']) for b in data if '<keyword>'.lower() in b['title'].lower()]
print(','.join(ids))
")
pct exec 102 -- docker exec <cwa-container> calibredb remove --library-path=/calibre-library ${IDS}

Shelfmark

Book search & request at shelfmark.carr-family.org. Image: ghcr.io/calibrain/shelfmark:latest (full, with browser automation). Config: /mnt/tank/appdata/shelfmark/plugins/.

After any config edit:

pct exec 108 -- docker service update --force media-core_shelfmark

Auth: mounts /mnt/tank/appdata/calibre-web-automated/app.db read-only to /auth/app.db — reuses Calibre-Web logins.

Volumes:

  • /mnt/tank/media/bookingest:/books — ebook download destination
  • /mnt/tank/media/content/audiobooks:/audiobooks — audiobook download destination

qBittorrent connection (prowlarr_clients.json):

  • URL: 192.168.2.190:8080, creds: admin / 32Ab0321!!
  • Ebook category: books-shelfmark, Audiobook category: audiobooks
  • DOWNLOAD_PROGRESS_UPDATE_INTERVAL=10 (was 1 — caused login spam in qbt logs)
  • HARDLINK_TORRENTS=false, HARDLINK_TORRENTS_AUDIOBOOK=false

Remote path mappings (advanced.json) — must use camelCase keys and host: "qbittorrent" (snake_case silently ignored):

[
  {"host": "qbittorrent", "remotePath": "/data/bookingest", "localPath": "/books"},
  {"host": "qbittorrent", "remotePath": "/data/content/audiobooks", "localPath": "/audiobooks"}
]

Komga

Comics & manga at komga.carr-family.org. Library: /mnt/tank/media/content/comics.

CONVERT_TO_CBZ=false, REPAIR_EXTENSIONS=false — do not re-enable (was renaming .cbr → .cbz, breaking seeding).


RomM (compose 32)

ROM manager with in-browser EmulatorJS at romm.carr-family.org. Port 8984, host-mode on 192.168.2.191. Auth: RomM built-in (no Authentik — would break EmulatorJS API calls).

  • ROMs: /mnt/tank/media/content/roms:/romm/library — organize by platform subfolder (e.g. roms/gba/, roms/n64/)
  • Config: /mnt/tank/appdata/romm/config/config.ymlmust exist before first start or settings won't persist
  • DB: /mnt/tank/appdata/romm/db
  • Assets/covers: /mnt/tank/appdata/romm/assets, /mnt/tank/appdata/romm/resources
  • Metadata: IGDB client ID u5audru2zn9na5a0x3wq26yslmk1s5, RetroAchievements key UV9Xf6VdH9D8N1bpzuePqAAUkwGErZW5
pct exec 108 -- docker stack deploy -c /mnt/tank/appdata/portainer/compose/32/docker-compose.yml romm

Jellyfin (PCT 101 — standalone, not Swarm)

Media server at jellyfin.carr-family.org. Static route in Traefik → 192.168.2.191:8096.


Audio Recorder (PCT 107 — stack: audiorecorder)

Browser-based system audio capture at audiorecorder.carr-family.org. Chrome/Edge only — enable "Share system audio" in the share dialog. Transcription via Whisper on PCT 109.

  • Image: audiorecorder:latest — built locally on PCT 107 from /tank/appdata/audiorecorder/app/
  • Recordings: /tank/appdata/audiorecorder/recordings/.webm files with .txt transcripts alongside
  • Compose: /tank/appdata/audiorecorder/docker-compose.yml (must be piped to PCT 108 for deploy)

Rebuild & redeploy:

tar -cf - -C /tank/appdata/audiorecorder/app . | pct exec 107 -- bash -c 'mkdir -p /tmp/audiorecorder-build && tar -xf - -C /tmp/audiorecorder-build'
pct exec 107 -- docker build -t audiorecorder:latest /tmp/audiorecorder-build/
pct exec 108 -- docker service update --force audiorecorder_audiorecorder

FlareSolverr

Cloudflare bypass for Prowlarr indexers. Internal only at 192.168.2.191:8191.