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' )