From 486e24cd8e199fd9c58ec472dee3ce2ebab79796 Mon Sep 17 00:00:00 2001 From: Nicola Canepa Date: Fri, 26 Dec 2025 12:15:54 +0000 Subject: [PATCH 1/2] Make code more pythonic --- container_configs.py | 120 ++++++++++----------- main.py | 243 ++++++++++++++++++++----------------------- 2 files changed, 176 insertions(+), 187 deletions(-) diff --git a/container_configs.py b/container_configs.py index 3620cd2..57a829f 100644 --- a/container_configs.py +++ b/container_configs.py @@ -1,4 +1,6 @@ +import getpass import os +import pwd class ContainerConfig: def __init__(self, @@ -8,17 +10,17 @@ def __init__(self, ): self.root_dir = root_dir self.timezone = timezone - self.config_dir = root_dir + '/config' + self.config_dir = f'{root_dir}/config' self.plex_claim = plex_claim - self.movie_dir = root_dir + '/media/movies' - self.tv_dir = root_dir + '/media/tv' - self.music_dir = root_dir + '/media/music' - self.book_dir = root_dir + '/media/books' - self.comic_dir = root_dir + '/media/comics' - self.torrent_dir = root_dir + '/data/torrents' - self.usenet_dir = root_dir + '/data/usenet' - self.homarr_dir = root_dir + '/data/homarr/appdata' - self.UID = os.popen('id -u').read().rstrip('\n') + self.movie_dir = f'{root_dir}/media/movies' + self.tv_dir = f'{root_dir}/media/tv' + self.music_dir = f'{root_dir}/media/music' + self.book_dir = f'{root_dir}/media/books' + self.comic_dir = f'{root_dir}/media/comics' + self.torrent_dir = f'{root_dir}/data/torrents' + self.usenet_dir = f'{root_dir}/data/usenet' + self.homarr_dir = f'{root_dir}/data/homarr/appdata' + self.UID = pwd.getpwnam(getpass.getuser()).pw_uid def plex(self): return ( @@ -30,10 +32,10 @@ def plex(self): ' - PUID=13010\n' ' - PGID=13000\n' ' - VERSION=docker\n' - ' - PLEX_CLAIM=' + self.plex_claim + '\n' + f' - PLEX_CLAIM={self.plex_claim}\n' ' volumes:\n' - ' - ' + self.config_dir + '/plex-config:/config\n' - ' - ' + self.root_dir + '/data/media:/media\n' + f' - {self.config_dir}/plex-config:/config\n' + f' - {self.root_dir}/data/media:/media\n' ' restart: unless-stopped\n\n' ) @@ -45,11 +47,11 @@ def tautulli(self): ' depends_on:\n' ' - plex\n' ' environment:\n' - ' - PUID='+ self.UID + '\n' + f' - PUID={self.UID}\n' ' - PGID=13000\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/tautulli-config:/config\n' + f' - {self.config_dir}/tautulli-config:/config\n' ' ports:\n' ' - "8181:8181"\n' ' restart: unless-stopped\n\n' @@ -61,13 +63,13 @@ def jellyfin(self): ' image: lscr.io/linuxserver/jellyfin:latest\n' ' container_name: jellyfin\n' ' environment:\n' - ' - PUID='+ self.UID + '\n' + f' - PUID={self.UID}\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/jellyfin-config:/config\n' - ' - ' + self.root_dir + '/data/media:/data\n' + f' - {self.config_dir}/jellyfin-config:/config\n' + f' - {self.root_dir}/data/media:/data\n' ' ports:\n' ' - "8096:8096"\n' ' restart: unless-stopped\n\n' @@ -82,10 +84,10 @@ def sonarr(self): ' - PUID=13001\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/sonarr-config:/config\n' - ' - ' + self.root_dir + '/data:/data\n' + f' - {self.config_dir}/sonarr-config:/config\n' + f' - {self.root_dir}/data:/data\n' ' ports:\n' ' - "8989:8989"\n' ' restart: unless-stopped\n\n' @@ -100,10 +102,10 @@ def radarr(self): ' - PUID=13002\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/radarr-config:/config\n' - ' - ' + self.root_dir + '/data:/data\n' + f' - {self.config_dir}/radarr-config:/config\n' + f' - {self.root_dir}/data:/data\n' ' ports:\n' ' - "7878:7878"\n' ' restart: unless-stopped\n\n' @@ -117,10 +119,10 @@ def bazarr(self): ' environment:\n' ' - PUID=13013\n' ' - PGID=13000\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/bazarr-config:/config\n' - ' - ' + self.root_dir + '/data/media:/media\n' + f' - {self.config_dir}/bazarr-config:/config\n' + f' - {self.root_dir}/data/media:/media\n' ' ports:\n' ' - "6767:6767"\n' ' restart: unless-stopped\n\n' @@ -135,10 +137,10 @@ def lidarr(self): ' - PUID=13003\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/lidarr-config:/config\n' - ' - ' + self.root_dir + '/data:/data\n' + f' - {self.config_dir}/lidarr-config:/config\n' + f' - {self.root_dir}/data:/data\n' ' ports:\n' ' - "8686:8686"\n' ' restart: unless-stopped\n\n' @@ -153,10 +155,10 @@ def readarr(self): ' - PUID=13004\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/readarr-config:/config\n' - ' - ' + self.root_dir + '/data:/data\n' + f' - {self.config_dir}/readarr-config:/config\n' + f' - {self.root_dir}/data:/data\n' ' ports:\n' ' - "8787:8787"\n' ' restart: unless-stopped\n\n' @@ -172,8 +174,8 @@ def mylar3(self): ' - PGID=13000\n' ' - UMASK=002\n' ' volumes:\n' - ' - ' + self.config_dir + '/mylar-config:/config\n' - ' - ' + self.root_dir + '/data:/data\n' + f' - {self.config_dir}/mylar-config:/config\n' + f' - {self.root_dir}/data:/data\n' ' ports:\n' ' - "8090:8090"\n' ' restart: unless-stopped\n\n' @@ -186,12 +188,12 @@ def audiobookshelf(self): ' container_name: audiobookshelf\n' ' environment:\n' ' - user=13014:13000\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/audiobookshelf:/config\n' - ' - ' + self.root_dir + '/data/media/audiobooks:/audiobooks\n' - ' - ' + self.root_dir + '/data/media/podcasts:/podcasts\n' - ' - ' + self.root_dir + '/data/media/audiobookshelf-metadata:/metadata\n' + f' - {self.config_dir}/audiobookshelf:/config\n' + f' - {self.root_dir}/data/media/audiobooks:/audiobooks\n' + f' - {self.root_dir}/data/media/podcasts:/podcasts\n' + f' - {self.root_dir}/data/media/audiobookshelf-metadata:/metadata\n' ' ports:\n' ' - "13378:80"\n' ' restart: unless-stopped\n\n' @@ -206,9 +208,9 @@ def prowlarr(self): ' - PUID=13006\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/prowlarr-config:/config\n' + f' - {self.config_dir}/prowlarr-config:/config\n' ' ports:\n' ' - "9696:9696"\n' ' restart: unless-stopped\n\n' @@ -221,9 +223,9 @@ def homarr(self): ' image: ghcr.io/homarr-labs/homarr:latest\n' ' container_name: homarr\n' ' environment:\n' - ' - SECRET_ENCRYPTION_KEY=' + key + '\n' + f' - SECRET_ENCRYPTION_KEY={key}\n' ' volumes:\n' - ' - ' + self.homarr_dir + ':/appdata\n' + f' - {self.homarr_dir}:/appdata\n' ' - /var/run/docker.sock:/var/run/docker.sock\n' ' ports:\n' ' - "7575:7575"\n' @@ -239,11 +241,11 @@ def qbittorrent(self): ' - PUID=13007\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' - WEBUI_PORT=8080\n' ' volumes:\n' - ' - ' + self.config_dir + '/qbittorrent-config:/config\n' - ' - ' + self.torrent_dir + ':/data/torrents\n' + f' - {self.config_dir}/qbittorrent-config:/config\n' + f' - {self.torrent_dir}:/data/torrents\n' ' ports:\n' ' - "8080:8080"\n' ' - "6881:6881"\n' @@ -260,9 +262,9 @@ def overseerr(self): ' - PUID=13009\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/overseerr-config:/app/config\n' + f' - {self.config_dir}/overseerr-config:/app/config\n' ' ports:\n' ' - "5055:5055"\n' ' restart: unless-stopped\n\n' @@ -277,9 +279,9 @@ def jellyseerr(self): ' - PUID=13012\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/jellyseerr-config:/app/config\n' + f' - {self.config_dir}/jellyseerr-config:/app/config\n' ' ports:\n' ' - "5056:5055"\n' ' restart: unless-stopped\n\n' @@ -294,10 +296,10 @@ def sabnzbd(self): ' - PUID=13011\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/sabnzbd-config:/config\n' - ' - ' + self.usenet_dir + ':/data/usenet\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' @@ -312,9 +314,9 @@ def jackett(self): ' - PUID=13008\n' ' - PGID=13000\n' ' - UMASK=002\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' volumes:\n' - ' - ' + self.config_dir + '/jackett-config:/config\n' + f' - {self.config_dir}/jackett-config:/config\n' ' ports:\n' ' - 9117:9117\n' ' restart: unless-stopped\n\n' @@ -329,7 +331,7 @@ def flaresolverr(self): ' - LOG_LEVEL=${LOG_LEVEL:-info}\n' ' - LOG_HTML=${LOG_HTML:-false}\n' ' - CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}\n' - ' - TZ=' + self.timezone + '\n' + f' - TZ={self.timezone}\n' ' ports:\n' ' - "8191:8191"\n' ' restart: unless-stopped\n\n' diff --git a/main.py b/main.py index 22b240d..b1918e0 100644 --- a/main.py +++ b/main.py @@ -16,7 +16,8 @@ def take_boolean_input(default=True): return False print('Please answer with y or n.', end=' ') -def take_input(service_name, service_type): +def take_input(service_name, service_type, name_to_show=None): + print(f'Use {name_to_show or service_name.capitalize()}? [Y/n]', end=" ") choice = take_boolean_input() if choice: services_classed[service_type].append(service_name) @@ -33,7 +34,7 @@ def take_directory_input(): print('Please make sure the path is absolute, meaning it starts at the root of your filesystem and starts with "/":', end=' ') def get_system_timezone(): - tz_path = "/etc/localtime" + tz_path = '/etc/localtime' if os.path.exists(tz_path): if os.path.islink(tz_path): @@ -41,130 +42,116 @@ def get_system_timezone(): return tz.split('zoneinfo/')[-1] return None -print('Welcome to the EZarr CLI.') -print('This CLI will ask you which services you\'d like to use and more. If you\'d like more information about a ' - 'certain service, look in the README.') - -print('\n===SERVARR===') -services_classed['servarr'] = [] -print('Use Sonarr? [Y/n]', end=" ") -take_input('sonarr', 'servarr') -print('Use Radarr? [Y/n]', end=" ") -take_input('radarr', 'servarr') -print('Use Lidarr? [Y/n]', end=" ") -take_input('lidarr', 'servarr') -print('Use Readarr? [Y/n]', end=" ") -take_input('readarr', 'servarr') -print('Use Mylar3? [Y/n]', end=" ") -take_input('mylar3', 'servarr') -print('Use Audiobookshelf? [Y/n]', end=" ") -take_input('audiobookshelf', 'servarr') -print('Use Homarr? [Y/n]', end=" ") -take_input('homarr', 'servarr') -if len(services_classed['servarr']) == 0: - print('Warning: no media management services selected.') -if services_classed['servarr'].__contains__('sonarr') or services_classed['servarr'].__contains__('radarr'): - print('Use Bazarr? [Y/n]', end=" ") - take_input('bazarr', 'servarr') - -print('\n===INDEXERS===') -services_classed['indexer'] = [] -print('Use Prowlarr? [Y/n]', end=" ") -take_input('prowlarr', 'indexer') -print('Use Jackett? [Y/n]', end=" ") -take_input('jackett', 'indexer') -if len(services_classed['indexer']) == 0: - print('Warning: no indexing service selected.') - -print('\n===CLOUDFLARE BYPASS===') -services_classed['bypass'] = [] -print('Use Flaresolverr? [Y/n]', end=" ") -take_input('flaresolverr', 'bypass') - -print('\n===MEDIA SERVERS===') -services_classed['ms'] = [] -print('Use PleX? [Y/n]', end=" ") -take_input('plex', 'ms') -if services_classed['ms'].__contains__('plex'): - print('Use Tautulli? [Y/n]', end=" ") - take_input('tautulli', 'ms') - if services_classed['servarr'].__contains__('sonarr') or services_classed['servarr'].__contains__('radarr'): - print('Use Overseerr? [Y/n]', end=" ") - take_input('overseerr', 'servarr') -print('Use Jellyfin? [Y/n]', end=" ") -take_input('jellyfin', 'ms') -if (services_classed['ms'].__contains__('jellyfin') - and (services_classed['servarr'].__contains__('sonarr') or services_classed['servarr'].__contains__('radarr'))): - print('Use Jellyseerr? [Y/n]', end=" ") - take_input('jellyseerr', 'servarr') -if len(services_classed['ms']) == 0: - print('Warning: no media servers selected.') - -print('\n===BITTORRENT===') -services_classed['torrent'] = [] -print('Use qBittorrent? [Y/n]', end=" ") -take_input('qbittorrent', 'torrent') - -print('\n===USENET===') -services_classed['usenet'] = [] -print('Use SABnzbd? [Y/n]', end=" ") -take_input('sabnzbd', 'usenet') - -if len(services_classed['torrent']) == 0 and len(services_classed['usenet']) == 0: - print('Warning: no usenet or BitTorrent clients selected.') - -services = [] -for service_class in services_classed.keys(): - services.extend(services_classed[service_class]) -if len(services) == 0: - print('No services selected. Terminating.') - exit(1) - -print('\n===CONFIGURATION===') - -print('Please enter your timezone (like "Europe/Amsterdam") or press enter to use your system\'s configured timezone:', end=' ') -timezone = input() -if timezone == '': - timezone = get_system_timezone() - -if len(timezone) == 0: # if user pressed enter and reading timezone from /etc/localtime failed then default to Amsterdam - timezone = 'Europe/Amsterdam' - -plex_claim = '' -if services.__contains__('plex'): - print('If you have a PleX claim token, enter it now. Otherwise, just press enter.', end=' ') - plex_claim = input() - -print('Where would you like to keep your files?', end=' ') -root_dir = take_directory_input() - -compose = open('docker-compose.yml', 'w') -compose.write( - '---\n' - 'services:\n' -) - -container_config = ContainerConfig(root_dir, timezone, plex_claim=plex_claim) - -for service in services: - compose.write(getattr(container_config, service)()) -compose.close() -print("Docker compose file generated successfully.") - -print("Do you want to also generate the required folder structure and permissions? (this is required for first time setup) [Y/n]: ") -generate_permissions = take_boolean_input() -if generate_permissions: - permission_setup = UserGroupSetup(root_dir=root_dir) +def main(): + print('Welcome to the EZarr CLI.') + print("This CLI will ask you which services you'd like to use and more. If you'd like more information about a " + 'certain service, look in the README.') + + print('\n===SERVARR===') + services_classed['servarr'] = [] + take_input('sonarr', 'servarr') + take_input('radarr', 'servarr') + take_input('lidarr', 'servarr') + take_input('readarr', 'servarr') + take_input('mylar3', 'servarr') + take_input('audiobookshelf', 'servarr') + take_input('homarr', 'servarr') + if len(services_classed['servarr']) == 0: + print('Warning: no media management services selected.') + if 'sonarr' in services_classed['servarr'] or 'radrr' in services_classed['servarr']: + take_input('bazarr', 'servarr') + + print('\n===INDEXERS===') + services_classed['indexer'] = [] + take_input('prowlarr', 'indexer') + take_input('jackett', 'indexer') + if len(services_classed['indexer']) == 0: + print('Warning: no indexing service selected.') + + print('\n===CLOUDFLARE BYPASS===') + services_classed['bypass'] = [] + take_input('flaresolverr', 'bypass') + + print('\n===MEDIA SERVERS===') + services_classed['ms'] = [] + take_input('plex', 'ms') + if 'plex' in services_classed['ms']: + take_input('tautulli', 'ms') + if 'sonarr' in services_classed['servarr'] or 'radarr' in services_classed['servarr']: + take_input('overseerr', 'servarr') + take_input('jellyfin', 'ms') + if ('jellyfin' in services_classed['ms'] + and ('sonarr' in services_classed['servarr'] or 'radarr' in services_classed['servarr'])): + take_input('jellyseerr', 'servarr') + if len(services_classed['ms']) == 0: + print('Warning: no media servers selected.') + + print('\n===BITTORRENT===') + services_classed['torrent'] = [] + take_input('qbittorrent', 'torrent', 'qBittorrent') + + print('\n===USENET===') + services_classed['usenet'] = [] + take_input('sabnzbd', 'usenet', 'SABnzbd') + + if len(services_classed['torrent']) == 0 and len(services_classed['usenet']) == 0: + print('Warning: no usenet or BitTorrent clients selected.') + + services = [] + for service_class in services_classed.keys(): + services.extend(services_classed[service_class]) + if len(services) == 0: + print('No services selected. Terminating.') + exit(1) + + print('\n===CONFIGURATION===') + + print('Please enter your timezone (like "Europe/Amsterdam") or press enter to use your system\'s configured timezone:', end=' ') + timezone = input() + if timezone == '': + timezone = get_system_timezone() + + if len(timezone) == 0: # if user pressed enter and reading timezone from /etc/localtime failed then default to Amsterdam + timezone = 'Europe/Amsterdam' + + plex_claim = '' + if 'plex' in services: + print('If you have a PleX claim token, enter it now. Otherwise, just press enter.', end=' ') + plex_claim = input() + + print('Where would you like to keep your files?', end=' ') + root_dir = take_directory_input() + + compose = open('docker-compose.yml', 'w') + compose.write( + '---\n' + 'services:\n' + ) + + container_config = ContainerConfig(root_dir, timezone, plex_claim=plex_claim) + for service in services: - try: - getattr(permission_setup, service)() - except AttributeError: - pass -else: - print("Permission and folder structure generation skipped by user.") - - -print('Process complete. You can now run "docker compose up -d" to start your containers.') -print('Thank you for using EZarr. If you experience any issues or have feature requests, add them to our issues.') -print('For questions, you can also use the discussions tab.') -exit(0) + compose.write(getattr(container_config, service)()) + compose.close() + print("Docker compose file generated successfully.") + + print("Do you want to also generate the required folder structure and permissions? (this is required for first time setup) [Y/n]: ") + generate_permissions = take_boolean_input() + if generate_permissions: + permission_setup = UserGroupSetup(root_dir=root_dir) + for service in services: + try: + getattr(permission_setup, service)() + except AttributeError: + pass + else: + print("Permission and folder structure generation skipped by user.") + + + print('Process complete. You can now run "docker compose up -d" to start your containers.') + print('Thank you for using EZarr. If you experience any issues or have feature requests, add them to our issues.') + print('For questions, you can also use the discussions tab.') + exit(0) + +if __name__ == '__main__': + main() From 7eee325670df44e607429e5538bf0604b1fb8425 Mon Sep 17 00:00:00 2001 From: Nicola Canepa Date: Fri, 26 Dec 2025 12:15:54 +0000 Subject: [PATCH 2/2] Make code more pythonic --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index b1918e0..6398c5a 100644 --- a/main.py +++ b/main.py @@ -58,7 +58,7 @@ def main(): take_input('homarr', 'servarr') if len(services_classed['servarr']) == 0: print('Warning: no media management services selected.') - if 'sonarr' in services_classed['servarr'] or 'radrr' in services_classed['servarr']: + if 'sonarr' in services_classed['servarr'] or 'radarr' in services_classed['servarr']: take_input('bazarr', 'servarr') print('\n===INDEXERS===')