Files
ezarr/container_configs.py
dtoro 92aa9b4c22
Some checks failed
Check running / run (push) Has been cancelled
Allow splitting config, downloads and media paths via .env
Adds optional CONFIG_DIR, DOWNLOADS_DIR and MEDIA_DIR env variables so
users can spread configs, downloads and media across different drives
without restructuring ROOT_DIR. Each falls back to its ROOT_DIR-based
default when left empty, so existing setups keep working unchanged.

The -arr services (sonarr, radarr, lidarr, mylar3) now use a single
${DOWNLOADS_DIR}:/data parent bind with a ${MEDIA_DIR}:/data/media
overlay. This preserves TRaSH's single-/data-mount semantics inside
the container (hardlinks still work on one filesystem) while letting
the host-side media tree live wherever the user wants.

README documents the new variables, the generated folder structure,
recommended storage layouts, and the same-filesystem requirement for
hardlinks between downloads and media.
2026-04-17 10:40:44 +02:00

343 lines
12 KiB
Python

import getpass
import os
import pwd
class ContainerConfig:
def __init__(self,
root_dir,
timezone,
plex_claim='',
config_dir=None,
downloads_dir=None,
media_dir=None,
):
self.root_dir = root_dir
self.timezone = timezone
self.plex_claim = plex_claim
# Allow each of config / downloads / media to be overridden independently.
# When not provided, fall back to the ROOT_DIR-based defaults used historically.
self.config_dir = config_dir or f'{root_dir}/config'
self.downloads_dir = downloads_dir or f'{root_dir}/data'
self.media_dir = media_dir or f'{root_dir}/data/media'
self.movie_dir = f'{self.media_dir}/movies'
self.tv_dir = f'{self.media_dir}/tv'
self.music_dir = f'{self.media_dir}/music'
self.book_dir = f'{self.media_dir}/books'
self.comic_dir = f'{self.media_dir}/comics'
self.torrent_dir = f'{self.downloads_dir}/torrents'
self.usenet_dir = f'{self.downloads_dir}/usenet'
self.homarr_dir = f'{root_dir}/data/homarr/appdata'
self.UID = pwd.getpwnam(getpass.getuser()).pw_uid
def _arr_data_volumes(self):
"""Volume lines for services that need the full /data tree (sonarr, radarr, lidarr, mylar3).
Emits a single ``${downloads_dir}:/data`` parent mount plus a ``${media_dir}:/data/media``
overlay. The overlay is a no-op when ``media_dir`` is the default ``${downloads_dir}/media``
(Docker just binds the same subpath) and transparently redirects ``/data/media`` when the
user has pointed ``media_dir`` at a different location. This preserves TRaSH's single-
``/data``-mount semantics while still allowing an independent ``MEDIA_DIR``.
"""
return (
f' - {self.downloads_dir}:/data\n'
f' - {self.media_dir}:/data/media\n'
)
def plex(self):
return (
' plex:\n'
' image: lscr.io/linuxserver/plex:latest\n'
' container_name: plex\n'
' network_mode: host\n'
' environment:\n'
' - PUID=13010\n'
' - PGID=13000\n'
' - VERSION=docker\n'
f' - PLEX_CLAIM={self.plex_claim}\n'
' volumes:\n'
f' - {self.config_dir}/plex-config:/config\n'
f' - {self.media_dir}:/media\n'
' restart: unless-stopped\n\n'
)
def tautulli(self):
return (
' tautulli:\n'
' image: lscr.io/linuxserver/tautulli:latest\n'
' container_name: tautulli\n'
' depends_on:\n'
' - plex\n'
' environment:\n'
f' - PUID={self.UID}\n'
' - PGID=13000\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/tautulli-config:/config\n'
' ports:\n'
' - "8181:8181"\n'
' restart: unless-stopped\n\n'
)
def jellyfin(self):
return (
' jellyfin:\n'
' image: lscr.io/linuxserver/jellyfin:latest\n'
' container_name: jellyfin\n'
' environment:\n'
f' - PUID={self.UID}\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/jellyfin-config:/config\n'
f' - {self.media_dir}:/data\n'
' ports:\n'
' - "8096:8096"\n'
' restart: unless-stopped\n\n'
)
def sonarr(self):
return (
' sonarr:\n'
' image: lscr.io/linuxserver/sonarr:latest\n'
' container_name: sonarr\n'
' environment:\n'
' - PUID=13001\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/sonarr-config:/config\n'
f'{self._arr_data_volumes()}'
' ports:\n'
' - "8989:8989"\n'
' restart: unless-stopped\n\n'
)
def radarr(self):
return (
' radarr:\n'
' image: lscr.io/linuxserver/radarr:latest\n'
' container_name: radarr\n'
' environment:\n'
' - PUID=13002\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/radarr-config:/config\n'
f'{self._arr_data_volumes()}'
' ports:\n'
' - "7878:7878"\n'
' restart: unless-stopped\n\n'
)
def bazarr(self):
return (
' bazarr:\n'
' image: lscr.io/linuxserver/bazarr:latest\n'
' container_name: bazarr\n'
' environment:\n'
' - PUID=13013\n'
' - PGID=13000\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/bazarr-config:/config\n'
f' - {self.media_dir}:/media\n'
' ports:\n'
' - "6767:6767"\n'
' restart: unless-stopped\n\n'
)
def lidarr(self):
return (
' lidarr:\n'
' image: lscr.io/linuxserver/lidarr:latest\n'
' container_name: lidarr\n'
' environment:\n'
' - PUID=13003\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/lidarr-config:/config\n'
f'{self._arr_data_volumes()}'
' ports:\n'
' - "8686:8686"\n'
' restart: unless-stopped\n\n'
)
def mylar3(self):
return (
' mylar3:\n'
' image: lscr.io/linuxserver/mylar3:latest\n'
' container_name: mylar3\n'
' environment:\n'
' - PUID=13005\n'
' - PGID=13000\n'
' - UMASK=002\n'
' volumes:\n'
f' - {self.config_dir}/mylar-config:/config\n'
f'{self._arr_data_volumes()}'
' ports:\n'
' - "8090:8090"\n'
' restart: unless-stopped\n\n'
)
def audiobookshelf(self):
return (
' audiobookshelf:\n'
' image: ghcr.io/advplyr/audiobookshelf:latest\n'
' container_name: audiobookshelf\n'
' environment:\n'
' - user=13014:13000\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/audiobookshelf:/config\n'
f' - {self.media_dir}/audiobooks:/audiobooks\n'
f' - {self.media_dir}/podcasts:/podcasts\n'
f' - {self.media_dir}/audiobookshelf-metadata:/metadata\n'
' ports:\n'
' - "13378:80"\n'
' restart: unless-stopped\n\n'
)
def prowlarr(self):
return (
' prowlarr:\n'
' image: lscr.io/linuxserver/prowlarr:develop\n'
' container_name: prowlarr\n'
' environment:\n'
' - PUID=13006\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/prowlarr-config:/config\n'
' ports:\n'
' - "9696:9696"\n'
' restart: unless-stopped\n\n'
)
def homarr(self):
key = os.urandom(32).hex() # Because os.urandom(32) generates 32 random bytes, and .hex() converts each byte into two hexadecimal characters.
return (
' homarr:\n'
' image: ghcr.io/homarr-labs/homarr:latest\n'
' container_name: homarr\n'
' environment:\n'
f' - SECRET_ENCRYPTION_KEY={key}\n'
' volumes:\n'
f' - {self.homarr_dir}:/appdata\n'
' - /var/run/docker.sock:/var/run/docker.sock\n'
' ports:\n'
' - "7575:7575"\n'
' restart: unless-stopped\n\n'
)
def qbittorrent(self):
return (
' qbittorrent:\n'
' image: lscr.io/linuxserver/qbittorrent:latest\n'
' container_name: qbittorrent\n'
' environment:\n'
' - PUID=13007\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' - WEBUI_PORT=8080\n'
' volumes:\n'
f' - {self.config_dir}/qbittorrent-config:/config\n'
f' - {self.torrent_dir}:/data/torrents\n'
' ports:\n'
' - "8080:8080"\n'
' - "6881:6881"\n'
' - "6881:6881/udp"\n'
' restart: unless-stopped\n\n'
)
def overseerr(self):
return (
' overseerr:\n'
' image: sctx/overseerr:latest\n'
' container_name: overseerr\n'
' environment:\n'
' - PUID=13009\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/overseerr-config:/app/config\n'
' ports:\n'
' - "5055:5055"\n'
' restart: unless-stopped\n\n'
)
def jellyseerr(self):
return (
' jellyseerr:\n'
' image: fallenbagel/jellyseerr:latest\n'
' container_name: jellyseerr\n'
' environment:\n'
' - PUID=13012\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/jellyseerr-config:/app/config\n'
' ports:\n'
' - "5056:5055"\n'
' restart: unless-stopped\n\n'
)
def sabnzbd(self):
return (
' sabnzbd:\n'
' image: lscr.io/linuxserver/sabnzbd:latest\n'
' container_name: sabnzbd\n'
' environment:\n'
' - PUID=13011\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/sabnzbd-config:/config\n'
f' - {self.usenet_dir}:/data/usenet\n'
' ports:\n'
' - "8081:8080"\n'
' restart: unless-stopped\n\n'
)
def jackett(self):
return (
' jackett:\n'
' image: lscr.io/linuxserver/jackett:latest\n'
' container_name: jackett\n'
' environment:\n'
' - PUID=13008\n'
' - PGID=13000\n'
' - UMASK=002\n'
f' - TZ={self.timezone}\n'
' volumes:\n'
f' - {self.config_dir}/jackett-config:/config\n'
' ports:\n'
' - 9117:9117\n'
' restart: unless-stopped\n\n'
)
def flaresolverr(self):
return (
' flaresolverr:\n'
' image: ghcr.io/flaresolverr/flaresolverr:latest\n'
' container_name: flaresolverr\n'
' environment:\n'
' - LOG_LEVEL=${LOG_LEVEL:-info}\n'
' - LOG_HTML=${LOG_HTML:-false}\n'
' - CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}\n'
f' - TZ={self.timezone}\n'
' ports:\n'
' - "8191:8191"\n'
' restart: unless-stopped\n\n'
)