Last active
March 17, 2025 18:26
-
-
Save Vulwsztyn/b600cb0a4ced7090cb1408e718b990b0 to your computer and use it in GitHub Desktop.
Arr stack script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pystache | |
import os | |
import secrets | |
dot_env_template = """ARR_DIR={{arr_dir}} | |
JELLYFIN_CACHE={{jellyfin_cache}} | |
TZ={{tz}} | |
MEDIA_DIR_NAME={{media_dir_name}} | |
TORRENTS_DIR_NAME={{torrents_dir_name}} | |
SECRET_ENCRYPTION_KEY={{secret_encryption_key}} | |
CONFIGS_DIR_NAME={{configs_dir_name}} | |
""" | |
docker_compose_template = """services: | |
jellyfin: | |
image: jellyfin/jellyfin | |
container_name: jellyfin | |
# do not set the use to 1000:1000 here, it will cause permission issues | |
network_mode: host | |
# ports: # you do not need it if you are using host network, but I left it to be able to see what port this is running on | |
# - '8096:8096' | |
volumes: | |
- $ARR_DIR:/mnt/arr-stack | |
- $ARR_DIR/$CONFIGS_DIR_NAME/jellyfin:/config | |
- $JELLYFIN_CACHE:/cache | |
restart: unless-stopped | |
extra_hosts: | |
- host.docker.internal:host-gateway | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
{{#use_nvidia_runtime}} | |
runtime: nvidia | |
deploy: | |
resources: | |
reservations: | |
devices: | |
- capabilities: | |
- gpu | |
{{/use_nvidia_runtime}} | |
jellyseerr: | |
image: fallenbagel/jellyseerr:latest | |
container_name: jellyseerr | |
environment: | |
- LOG_LEVEL=debug | |
- TZ=$TZ | |
ports: | |
- '5055:5055' | |
volumes: | |
- $ARR_DIR/$CONFIGS_DIR_NAME/jellyseerr:/app/config | |
restart: unless-stopped | |
jackett: | |
container_name: jackett | |
image: linuxserver/jackett | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/jackett:/config' | |
- '$ARR_DIR/$MEDIA_DIR_NAME/torrents:/downloads' | |
ports: | |
- '9117:9117' | |
restart: unless-stopped | |
{{#use_sonarr}} | |
sonarr: | |
container_name: sonarr | |
image: linuxserver/sonarr | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
ports: | |
- '8989:8989' | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/sonarr:/config' | |
- '$ARR_DIR:/data' | |
restart: unless-stopped | |
{{/use_sonarr}} | |
{{#use_radarr}} | |
radarr: | |
container_name: radarr | |
image: linuxserver/radarr | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
ports: | |
- '7878:7878' | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/radarr:/config' | |
- '$ARR_DIR:/data' | |
restart: unless-stopped | |
{{/use_radarr}} | |
{{#use_lidarr}} | |
lidarr: | |
container_name: lidarr | |
image: ghcr.io/linuxserver/lidarr | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/liadarr:/config' | |
- '$ARR_DIR:/data' | |
ports: | |
- '8686:8686' | |
restart: unless-stopped | |
{{/use_lidarr}} | |
{{#use_readarr}} | |
readarr: | |
container_name: readarr | |
image: 'hotio/readarr:nightly' | |
ports: | |
- '8787:8787' | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/readarr:/config' | |
- '$ARR_DIR:/data' | |
restart: unless-stopped | |
{{/use_readarr}} | |
{{#use_readarr_audiobooks}} | |
# Notice the different port for the audiobook container | |
readarr-audio-books: | |
container_name: readarr-audio-books | |
image: 'hotio/readarr:nightly' | |
ports: | |
- '8786:8787' | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/readarr-audio-books:/config' | |
- '$ARR_DIR:/data' | |
restart: unless-stopped | |
{{/use_readarr_audiobooks}} | |
{{#use_bazarr}} | |
bazarr: | |
container_name: bazarr | |
image: ghcr.io/linuxserver/bazarr | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/bazarr:/config' | |
- '$ARR_DIR:/data' | |
ports: | |
- '6767:6767' | |
restart: unless-stopped | |
{{/use_bazarr}} | |
qflood: | |
container_name: qflood | |
image: hotio/qflood | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- UMASK=002 | |
- TZ=$TZ | |
- FLOOD_AUTH=false | |
volumes: | |
- '$ARR_DIR/torrents:/data/torrents' | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/qflood:/config' | |
restart: unless-stopped | |
{{#use_vpn}} | |
network_mode: 'service:vpn' | |
depends_on: | |
- vpn | |
{{/use_vpn}} | |
{{^use_vpn}} | |
ports: | |
- '8080:8080' | |
- '3005:3000' | |
{{/use_vpn}} | |
{{#use_homarr}} | |
homarr: | |
container_name: homarr | |
image: ghcr.io/homarr-labs/homarr:latest | |
restart: unless-stopped | |
volumes: | |
- /var/run/docker.sock:/var/run/docker.sock # <--- add this line here! | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/homarr:/appdata' | |
environment: | |
- SECRET_ENCRYPTION_KEY=$SECRET_ENCRYPTION_KEY # <--- can be generated with `openssl rand -hex 32` | |
ports: | |
- '7575:7575' | |
{{/use_homarr}} | |
{{#use_vpn}} | |
vpn: | |
container_name: vpn | |
# image: 'dperson/openvpn-client:latest' | |
build: | |
dockerfile: openvpn.Dockerfile | |
environment: | |
- 'OTHER_ARGS= --mute-replay-warnings' | |
cap_add: | |
- net_admin | |
restart: unless-stopped | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/openvpn:/vpn' | |
security_opt: | |
- 'label:disable' | |
devices: | |
- '/dev/net/tun:/dev/net/tun' | |
ports: | |
# - '8112:8112' #deluge web UI Port | |
- '8080:8080' | |
- '3005:3000' | |
{{/use_vpn}} | |
{{#use_heimdall}} | |
heimdall: | |
container_name: heimdall | |
image: ghcr.io/linuxserver/heimdall | |
environment: | |
- PUID=1000 | |
- PGID=1000 | |
- TZ=$TZ | |
volumes: | |
- '$ARR_DIR/$CONFIGS_DIR_NAME/heimdall:/config' | |
ports: | |
- '8090:80' | |
restart: unless-stopped | |
{{/use_heimdall}} | |
""" | |
dockerfile_template = """FROM dperson/openvpn-client:latest | |
WORKDIR /etc/openvpn | |
RUN wget https://raw.githubusercontent.com/ProtonVPN/scripts/refs/heads/master/update-resolv-conf.sh | |
RUN mv update-resolv-conf.sh update-resolv-conf | |
RUN chmod +x update-resolv-conf | |
""" | |
def input_with_default(prompt, default): | |
value = input(f"{prompt} [{default = }]: ") | |
return value if value else default | |
settings = { | |
"arr_dir": "/home/$USER/arr", | |
"jellyfin_cache": "/home/$USER/jellyfin/cache", | |
"tz": "Europe/Berlin", | |
"media_dir_name": "media", | |
"configs_dir_name": "configs", | |
"shows_dir_name": "shows", | |
"movies_dir_name": "movies", | |
"audiobooks_dir_name": "audiobooks", | |
"music_dir_name": "music", | |
"books_dir_name": "books", | |
"torrents_dir_name": "torrents", | |
"use_vpn": True, | |
"use_sonarr": True, | |
"use_radarr": True, | |
"use_lidarr": True, | |
"use_readarr": True, | |
"use_readarr_audiobooks": True, | |
"use_bazarr": True, | |
"use_homarr": True, | |
"use_heimdall": True, | |
"use_nvidia_runtime": True, | |
"secret_encryption_key": secrets.token_hex(32), | |
} | |
dry_run = input_with_default("Dry run (y/n)?", "no").lower().startswith("y") | |
create_files_here_if_dry_run = False | |
if dry_run: | |
create_files_here_if_dry_run = ( | |
input_with_default("Create files here (y/n)?", "no").lower().startswith("y") | |
) | |
media_dirs = [ | |
key | |
for key in settings | |
if "dir_name" in key and key != "media_dir_name" and key != "configs_dir_name" | |
] | |
change_anything = input_with_default("Do you want to change any directory (y/n)?", "no") | |
if not change_anything.startswith("n"): | |
settings["arr_dir"] = input_with_default( | |
"Enter root arr dir (to put media dir, config dir & dockerfiles)", | |
settings["arr_dir"], | |
) | |
settings["media_dir_name"] = input_with_default( | |
"Enter media dir name (in arr_dir; shows, movies etc, will be put here)", | |
settings["media_dir_name"], | |
) | |
settings["configs_dir_name"] = input_with_default( | |
"Enter configs dir name (will be put in arr_dir)", | |
settings["configs_dir_name"], | |
) | |
for key in media_dirs: | |
settings[key] = input_with_default(f"Enter {key}", settings[key]) | |
settings["jellyfin_cache"] = input_with_default( | |
"Enter jellyfin cache dir", settings["jellyfin_cache"] | |
) | |
settings["tz"] = input_with_default("Enter timezone", settings["tz"]) | |
for key in settings: | |
if isinstance(settings[key], str): | |
settings[key] = settings[key].replace("$USER", os.environ["USER"]) | |
settings[key] = settings[key].replace("$HOME", os.environ["HOME"]) | |
settings["use_vpn"] = ( | |
input_with_default("Use VPN (y/n)?", "y" if settings["use_vpn"] else "n") | |
.lower() | |
.startswith("y") | |
) | |
use_everything = input_with_default("Use every service (y/n)?", "yes") | |
if use_everything.startswith("n"): | |
for key in (k for k in settings if "use" in k and "vpn" not in k): | |
settings[key] = ( | |
input_with_default( | |
f"Use {key.split('_')[1]} (y/n)?", "y" if settings["key"] else "n" | |
) | |
.lower() | |
.startswith("y") | |
) | |
if not dry_run: | |
os.makedirs( | |
os.path.join(os.path.expanduser(settings["arr_dir"]), "configs"), exist_ok=True | |
) | |
if settings["use_vpn"]: | |
os.makedirs( | |
os.path.join(os.path.expanduser(settings["arr_dir"]), "configs", "openvpn"), | |
exist_ok=True, | |
) | |
for dir_name in media_dirs: | |
os.makedirs( | |
os.path.join( | |
os.path.expanduser(settings["arr_dir"]), | |
settings["media_dir_name"], | |
settings[dir_name], | |
), | |
exist_ok=True, | |
) | |
if not dry_run: | |
dot_env_path = os.path.join(os.path.expanduser(settings["arr_dir"]), ".env") | |
docker_compose_path = os.path.join( | |
os.path.expanduser(settings["arr_dir"]), "docker-compose.yml" | |
) | |
print(dot_env_path) | |
print(docker_compose_path) | |
with open(dot_env_path, "w") as f: | |
f.write(pystache.render(dot_env_template, settings)) | |
with open(docker_compose_path, "w") as f: | |
f.write(pystache.render(docker_compose_template, settings)) | |
elif create_files_here_if_dry_run: | |
with open(".env", "w") as f: | |
f.write(pystache.render(dot_env_template, settings)) | |
with open("docker-compose.yml", "w") as f: | |
f.write(pystache.render(docker_compose_template, settings)) | |
with open("openvpn.Dockerfile", "w") as f: | |
f.write(dockerfile_template) | |
if settings["use_vpn"]: | |
openvpn_dockerfile_path = os.path.join( | |
os.path.expanduser(settings["arr_dir"]), "openvpn.Dockerfile" | |
) | |
print(openvpn_dockerfile_path) | |
with open(openvpn_dockerfile_path, "w") as f: | |
f.write(dockerfile_template) | |
show_proton_vpn_instructions = False | |
if settings["use_vpn"]: | |
show_proton_vpn_instructions = ( | |
input_with_default("Do you want to see ProtonVPN instructions (y/n)?", "yes") | |
.lower() | |
.startswith("y") | |
) | |
if show_proton_vpn_instructions: | |
print("Go to https://account.protonvpn.com/account-password:") | |
create_vpn_config = ( | |
input_with_default( | |
"Do you want to create a VPN config with the script or manually (s/m)?", | |
"script", | |
) | |
.lower() | |
.startswith("s") | |
) | |
if create_vpn_config: | |
print("From section OpenVPN / IKEv2 username") | |
username = input("Paste OpenVPN / IKEv2 username") | |
password = input("Paste OpenVPN / IKEv2 password") | |
if not dry_run: | |
with open( | |
os.path.join( | |
os.path.expanduser(settings["arr_dir"]), | |
"configs", | |
"openvpn", | |
"vpn.auth", | |
), | |
"w", | |
) as f: | |
f.write(f"{username}\n{password}\n") | |
elif create_files_here_if_dry_run: | |
with open("vpn.auth", "w") as f: | |
f.write(f"{username}\n{password}\n") | |
else: | |
print( | |
f"In directory {os.path.join(os.path.expanduser(settings['arr_dir']), 'configs', 'openvpn')} create a file called vpn.auth with your username and password on separate lines" | |
) | |
print("Go to https://account.protonvpn.com/downloads") | |
print("Download the OpenVPN config file you want") | |
print( | |
f"Move the downloaded file to {os.path.join(os.path.expanduser(settings['arr_dir']), 'configs', 'openvpn')}" | |
) | |
print( | |
f"You can go to {os.path.expanduser(settings['arr_dir'])} and run `docker compose up -d` to start the services" | |
) | |
show_further_setup_instructions = ( | |
input_with_default("Do you want to see further setup instructions (y/n)?", "yes") | |
.lower() | |
.startswith("y") | |
) | |
if not show_further_setup_instructions: | |
exit() | |
server_ip = input("Enter the server IP: ") | |
show_heimdall_instructions = ( | |
input_with_default("Do you want to see Heimdall instructions (y/n)?", "yes") | |
.lower() | |
.startswith("y") | |
) | |
if show_heimdall_instructions: | |
print(f"Go to http://{server_ip}:8090") | |
print("For each of apps click 'Application list' -> 'Add' and:") | |
print(f" -> Add jellyfin with url http://{server_ip}:8096") | |
print(f" -> Add jellyseerr with url http://{server_ip}:5055") | |
if settings["use_sonarr"]: | |
print(f" -> Add sonarr with url http://{server_ip}:8989") | |
if settings["use_radarr"]: | |
print(f" -> Add radarr with url http://{server_ip}:7878") | |
if settings["use_lidarr"]: | |
print(f" -> Add lidarr with url http://{server_ip}:8686") | |
if settings["use_readarr"]: | |
print(f" -> Add readarr with url http://{server_ip}:8787") | |
if settings["use_readarr_audiobooks"]: | |
print(f" -> Add readarr-audio-books with url http://{server_ip}:8786") | |
if settings["use_bazarr"]: | |
print(f" -> Add bazarr with url http://{server_ip}:6767") | |
print(f" -> Add qbittorrent with url http://{server_ip}:8080") | |
input("Press enter to continue") | |
print(f"Go to http://{server_ip}:9117") | |
print( | |
"Click on add indexer and add a few indexers e.g.: 1337x, audiobookbay, TheRARBG, The Pirate Bay" | |
) | |
print("These will suffice for now unless you know what you are doing") | |
input("Press enter to continue") | |
print( | |
f"Go to qbittorrent http://{server_ip}:8080. Default username is admin and password is adminadmin." | |
) | |
print("Change those under Tools -> Options -> WebUI if you wish.") | |
print("Under Tools -> Options:") | |
print(" set 'Default Save Path' to /data/torrents") | |
print(" set 'Keep incomplete torrents in:' to /data/torrents/incomplete") | |
input("Press enter to continue") | |
print("For every arr service (You can go to them from heimdall)") | |
print("1. set username and password") | |
print("2. Settings -> Download Client -> Add -> qBittorrent") | |
print( | |
f" Host: {server_ip}, Port: 8080, Username: admin, Password: adminadmin (the last 2 will be different if you changed them)" | |
) | |
print("3. Settings -> Media management -> Root folder -> Add:") | |
if settings["use_sonarr"]: | |
print( | |
f" /data/{settings['media_dir_name']}/{settings['shows_dir_name']} for sonarr" | |
) | |
if settings["use_radarr"]: | |
print( | |
f" /data/{settings['media_dir_name']}/{settings['movies_dir_name']} for radarr" | |
) | |
if settings["use_lidarr"]: | |
print( | |
f" /data/{settings['media_dir_name']}/{settings['music_dir_name']} for lidarr" | |
) | |
if settings["use_readarr"]: | |
print( | |
f" /data/{settings['media_dir_name']}/{settings['books_dir_name']} for readarr" | |
) | |
if settings["use_readarr_audiobooks"]: | |
print( | |
f" /data/{settings['media_dir_name']}/{settings['audiobooks_dir_name']} for readarr-audio-books" | |
) | |
print(f"4. Add every relevant indexer") | |
print(f"4.1 Settings -> Indexers -> Add indexer -> Torznab") | |
print( | |
f"4.2. In Jackett copy the Torznab link for the indexer you want to add and paste it in URL" | |
) | |
print(f"4.3. Set the API key to the one from Jackett (top right)") | |
input("Press enter to continue") | |
if settings["use_bazarr"]: | |
print(f"Go to bazarr at http://{server_ip}:6767") | |
for service in ["sonarr", "radarr"]: | |
print("Settings -> {service}") | |
print(f"Address: {server_ip} or {service}") | |
print(f"Port: {8989 if service == 'sonarr' else 7878}") | |
print("API key: get API key from {service} Settings -> General and paste here") | |
print() | |
print(f"Go to jellyfin at http://{server_ip}:8096/web/#/dashboard/libraries") | |
print(f"Add all libraries in /data/{settings['media_dir_name']}") | |
input("Press enter to continue") | |
print(f" Go to http://{server_ip}:8096/web/#/dashboard/playback/transcoding") | |
print("Set proper hardware acceleration") | |
print("If using nvidia then Nvidia NVENC") | |
print( | |
"Check which formats work at https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new" | |
) | |
print("Go to http://{server_ip}:8096/web/#/dashboard/keys") | |
input("Press enter to continue") | |
print("Add new key with name Jellyseer and copy it") | |
print(f"Go to http://{server_ip}:5055/settings/jellyfin") | |
print(f"Paste the key set hostname to {server_ip} and port to 8096") | |
print(f"Go to http://{server_ip}:5055/settings/services and add radarr and sonarr") | |
print("with API keys from their settings -> General") | |
print("Set the hostname {server_ip} and port to 7878 (radarr) or 8989 (sonarr)") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment