From 19a5434bb4e113664adf218044a200111bfc1d4a Mon Sep 17 00:00:00 2001 From: Manuel Thalmann Date: Sun, 10 Nov 2024 12:33:36 +0100 Subject: [PATCH] Add scripts for installing jellyfin --- .../docker/services/jellyfin/.dockerignore | 6 + .../jellyfin/docker-compose.secrets.yml | 19 +++ .../services/jellyfin/docker-compose.yml | 127 +++++++++++++++ .../docker/services/jellyfin/main.fish | 146 ++++++++++++++++++ .../docker/services/jellyfin/pvpn-cli.py | 100 ++++++++++++ .../services/jellyfin/rtorrent.Dockerfile | 121 +++++++++++++++ .../Software/docker/services/service.fish | 69 ++++++--- 7 files changed, 570 insertions(+), 18 deletions(-) create mode 100644 scripts/Common/Software/docker/services/jellyfin/.dockerignore create mode 100644 scripts/Common/Software/docker/services/jellyfin/docker-compose.secrets.yml create mode 100644 scripts/Common/Software/docker/services/jellyfin/docker-compose.yml create mode 100644 scripts/Common/Software/docker/services/jellyfin/main.fish create mode 100644 scripts/Common/Software/docker/services/jellyfin/pvpn-cli.py create mode 100644 scripts/Common/Software/docker/services/jellyfin/rtorrent.Dockerfile diff --git a/scripts/Common/Software/docker/services/jellyfin/.dockerignore b/scripts/Common/Software/docker/services/jellyfin/.dockerignore new file mode 100644 index 00000000..2523eb4e --- /dev/null +++ b/scripts/Common/Software/docker/services/jellyfin/.dockerignore @@ -0,0 +1,6 @@ +config/ +downloads/ +media/ +docker-compose.yml +*.env +*.Dockerfile diff --git a/scripts/Common/Software/docker/services/jellyfin/docker-compose.secrets.yml b/scripts/Common/Software/docker/services/jellyfin/docker-compose.secrets.yml new file mode 100644 index 00000000..8aa3570d --- /dev/null +++ b/scripts/Common/Software/docker/services/jellyfin/docker-compose.secrets.yml @@ -0,0 +1,19 @@ +services: + jellyfin: + ports: + - 127.0.0.1:1337:8096 + radarr: + ports: + - 127.0.0.1:1337:7878 + sonarr: + ports: + - 127.0.0.1:1337:8989 + lidarr: + ports: + - 127.0.0.1:1337:8686 + prowlarr: + ports: + - 127.0.0.1:1337:9696 + flood: + ports: + - 127.0.0.1:1337:3000 diff --git a/scripts/Common/Software/docker/services/jellyfin/docker-compose.yml b/scripts/Common/Software/docker/services/jellyfin/docker-compose.yml new file mode 100644 index 00000000..13668b20 --- /dev/null +++ b/scripts/Common/Software/docker/services/jellyfin/docker-compose.yml @@ -0,0 +1,127 @@ +services: + jellyfin: + image: jellyfin/jellyfin + restart: unless-stopped + extends: + file: docker-compose.secrets.yml + service: jellyfin + user: 1337:1337 + hostname: Jellyfin + environment: {} + volumes: + - ./config/jellyfin:/config + - cache:/cache + - ./media:/media + radarr: + image: linuxserver/radarr + restart: unless-stopped + extends: + file: docker-compose.secrets.yml + service: radarr + environment: + PUID: 1337 + PGID: 1337 + volumes: + - /etc/localtime:/etc/localtime:ro + - ./config/radarr:/config + - ./media/movies:/movies + - ./downloads:/downloads + sonarr: + image: linuxserver/sonarr + restart: unless-stopped + extends: + file: docker-compose.secrets.yml + service: sonarr + environment: + PUID: 1337 + PGID: 1337 + volumes: + - /etc/localtime:/etc/localtime:ro + - ./config/sonarr:/config + - ./media/series:/tv + - ./downloads:/downloads + lidarr: + image: linuxserver/lidarr + restart: unless-stopped + extends: + file: docker-compose.secrets.yml + service: lidarr + environment: + PUID: 1337 + PGID: 1337 + volumes: + - /etc/localtime:/etc/localtime:ro + - ./config/lidarr:/config + - ./media/music:/music + - ./downloads:/downloads + prowlarr: + image: linuxserver/prowlarr + restart: unless-stopped + extends: + file: docker-compose.secrets.yml + service: prowlarr + environment: + PUID: 1337 + PGID: 1337 + volumes: + - /etc/localtime:/etc/localtime:ro + - ./config/prowlarr:/config + flaresolverr: + image: flaresolverr/flaresolverr + restart: unless-stopped + environment: + LOG_LEVEL: info + LOG_HTML: "false" + CAPTCHA_SOLVER: none + privoxy: + image: walt3rl/proton-privoxy + restart: unless-stopped + volumes: + - /etc/localtime:/etc/localtime:ro + devices: + - /dev/net/tun + cap_add: + - NET_ADMIN + flood: + image: jesec/flood + restart: unless-stopped + extends: + file: docker-compose.secrets.yml + service: flood + user: 1337:1337 + command: --baseuri /flood + --rundir /flood + --allowedpath /downloads + --rtsocket /rtorrent/.local/share/rtorrent/rtorrent.sock + volumes: + - ./config/flood:/flood + - ./downloads:/downloads + - ./config/rtorrent:/rtorrent + rtorrent: + build: + dockerfile: ./rtorrent.Dockerfile + context: . + restart: unless-stopped + hostname: rtorrent + environment: + PUID: 1337 + PGID: 1337 + PHOME: /config + MAX_UPTIME: 43200 + command: -o ratio.enable= + -o ratio.min.set=200 + -o ratio.max.set=10000 + -o directory.default.set=/downloads + -o 'method.set=group.seeding.ratio.command, "d.cloase = ; d.erase = "' + volumes: + - ./config/rtorrent:/config + - ./downloads:/downloads + - /etc/localtime:/etc/localtime:ro + - ./proton:/proton + devices: + - /dev/net/tun + cap_add: + - NET_ADMIN + +volumes: + cache: {} diff --git a/scripts/Common/Software/docker/services/jellyfin/main.fish b/scripts/Common/Software/docker/services/jellyfin/main.fish new file mode 100644 index 00000000..1348bb96 --- /dev/null +++ b/scripts/Common/Software/docker/services/jellyfin/main.fish @@ -0,0 +1,146 @@ +#!/bin/env fish +begin + set -l dir (status dirname) + set -l user "jellyfin" + set -l domain "media" + set -l server "$domain" "" + set -l servarr radarr sonarr lidarr prowlarr + set -l flood flood + set -l service $user + source "$dir/../service.fish" + + function installSW -V dir -V domain -V server -V service + set -l root (getServiceRoot $argv) + set -l secrets (getServiceSecretsConfig $argv) + set -l source "$dir/$(basename "$secrets")" + set -l port (getRandomPort) + set -l servarrKeys + initializeServiceInstallation $argv + sudo cp "$dir/docker-compose.yml" "$root" + sudo cp "$dir/.dockerignore" "$root" + sudo cp "$dir/pvpn-cli.py" "$root" + sudo cp "$dir/rtorrent.Dockerfile" "$root" + sudo cp "$source" "$secrets" + + installDockerService $argv + end + + function configureSW -V dir -V user -V domain -V service -V servarr -V flood + set -l uid + set -l gid + set -l port + set -l file (mktemp) + set -l root (getServiceRoot $argv) + set -l bin "/usr/local/bin/forgejo" + set -l config "$root/docker-compose.yml" + set -l secrets (getServiceSecretsConfig $argv) + set -l envKey "$(getServiceKey "$service").environment" + configureDockerService $argv + + and sudo useradd \ + --system \ + --shell /bin/false \ + --comment 'Jellyfin server' \ + --create-home \ + $user + + set uid (id -u $user) + set gid (id -g $user) + + for name in $service $flood + set -l userKey "$(getServiceKey "$name").user" + cp "$config" "$file" + USER=$uid:$gid yq -y "$userKey = env.USER" "$file" | sudo tee "$config" >/dev/null + end + + for name in $servarr rtorrent + set -l envKey "$(getServiceKey "$name").environment" + sudo cp "$config" "$file" + + and yq "$envKey.PUID = $uid" "$file" | \ + yq "$envKey.PGID = $gid" | \ + yq -y . | \ + sudo tee "$config" >/dev/null + end + + cp "$config" "$file" + URL="https://$(getServiceDomain "$domain" "")/" yq "$(getServiceKey "$service").environment.JELLYFIN_PublishedServerUrl = env.URL" "$file" | \ + yq -y . | \ + sudo tee "$config" >/dev/null + + for dir in "$root"/{downloads,config/{,jellyfin,flood,rtorrent,radarr,sonarr,lidarr,prowlarr},media/{,movies,series,music}} + sudo mkdir -p "$dir" + and chown -R $uid:$gid "$dir" + end + + rm "$file" + + begin + printf "%s\n" \ + "#!/bin/sh" \ + "ssh -p $port -o StrictHostKeyChecking=no git@127.0.0.1 \"SSH_ORIGINAL_COMMAND=\\\"$SSH_ORIGINAL_COMMAND\\\" \$0 \$@\"" + end | sudo tee "$bin" >/dev/null + + chmod +x "$bin" + end + + function getServiceServers -V server + printf "%s\0" $server + end + + function getServiceLocations -V servarr -V flood + argparse -i "name=" -- $argv + printf "%s\0" \ + "$_flag_name" / ( + for app in $servarr + printf "%s\n" "$app" "/$app" + end) \ + flood "~ ^/flood.*" + end + + function getServiceLocationConfig -a domain s location -V service -V flood + if [ "$s" = "$service" ] + set -l argv $argv[4..] + + printf "%s\n" \ + "location = / {" \ + 'return 302 $scheme://$host/web/;' \ + "}" + + getServiceDefaultProxy $domain $s "$location" --comment "Proxy main Jellyfin traffic" $argv + getServiceDefaultProxy $domain $s "= /web/" --path "/web/index.html" --comment "Proxy main Jellyfin traffic" $argv + getServiceDefaultProxy $domain $s "/socket" --comment "Proxy Jellyfin Websockets traffic" $argv + else if [ "$s" = "$flood" ] + getServiceDefaultProxy $argv + + printf "%s\n" \ + "location = /flood {" \ + 'return 302 $scheme://$host$uri/$is_args$args;' \ + "}" + else + getServiceDefaultProxy $argv --path "$location" + end + end + + function getExtraLocationSettings -a domain s location -V service + if [ "$s" = "$service" ] + if [ "$location" = / ] + printf "%s\n" \ + "# Disable buffering when the nginx proxy gets very resource heavy upon streaming" \ + "proxy_buffering off;" + else if [ "$location" = "/socket" ] + printf "%s\n" \ + '# Websocket' \ + "proxy_http_version 1.1;" \ + 'proxy_set_header Upgrade $http_upgrade;' \ + 'proxy_set_header Connection "upgrade";' + end + end + end + + function getBackupArgs -V root + printf "%s\n" --hidden --no-ignore . --exclude "docker-compose.yml" "$root" + end + + runInstaller --force $argv +end diff --git a/scripts/Common/Software/docker/services/jellyfin/pvpn-cli.py b/scripts/Common/Software/docker/services/jellyfin/pvpn-cli.py new file mode 100644 index 00000000..426ffbad --- /dev/null +++ b/scripts/Common/Software/docker/services/jellyfin/pvpn-cli.py @@ -0,0 +1,100 @@ +from argparse import ArgumentParser +from os import environ +from os.path import dirname +from re import M +import shlex +import subprocess +import sys +from protonvpn_cli.cli import FeatureEnum, protonvpn + +def run_proton(args): + exit( + subprocess.run( + ["pipenv", "run", "proton"], + cwd="/app", + env=dict( + environ, + PIPENV_VENV_IN_PROJECT=f"{1}", + PVPN_CMD_ARGS=" ".join(args))).returncode) + +protonvpn.ensure_connectivity() + +args = sys.argv[1:] + +if not args: + args = shlex.split(environ.get("PVPN_CMD_ARGS") or "") + environ["PVPN_CMD_ARGS"] = "" + +parser = ArgumentParser(exit_on_error=False) +subParsers = parser.add_subparsers(dest="command") +initParser = subParsers.add_parser("init", alias=["i"]) +connectParser = subParsers.add_parser("connect", aliases=["c"]) + +for aliases in [ + ["-f", "--fastest"], + ["-r", "--random"], + ["-s", "--streaming"], + ["--sc"], + ["--p2p"], + ["--tor"] +]: + connectParser.add_argument(*aliases, action="store_true") + +connectParser.add_argument("--cc") +parsedArgs = None + +try: + parsedArgs = parser.parse_args(args) +except: + pass + +if parsedArgs is not None and parsedArgs.command == "init": + userName = input("Enter your Proton VPN username or email: ") + subprocess.run(["protonvpn-cli", "login", userName]) +else: + session = protonvpn.get_session() + try: + session.ensure_valid() + except: + raise Exception("Your current session is invalid. Please initialize the session using the `init` subcommand.") + + environ["PVPN_USERNAME"] = session.vpn_username + (environ.get("PVPN_TAGS") or "") + environ["PVPN_PASSWORD"] = session.vpn_password + environ["PVPN_TIER"] = f"{session.vpn_tier}" + + if parsedArgs is not None and ( + len( + list( + filter( + lambda item: item[1] not in [False, None], + vars(parsedArgs).items()))) > 1): + country = protonvpn.get_country() + + def match(server): + features = list() + + if parsedArgs.streaming: + features.append(FeatureEnum.STREAMING) + if parsedArgs.sc: + features.append(FeatureEnum.SECURE_CORE) + if parsedArgs.p2p: + features.append(FeatureEnum.P2P) + if parsedArgs.tor: + features.append(FeatureEnum.TOR) + + return (parsedArgs.cc is None or server.exit_country.lower() == parsedArgs.cc.lower()) and ( + all(feature in server.features for feature in features)) + + servers = session.servers.filter(match) + + if len(servers) > 0: + if parsedArgs.fastest or not parsedArgs.random: + server = servers.get_fastest_server() + else: + server = servers.get_random_server() + + run_proton(["connect", server.name]) + else: + raise Exception(f"Unable to find a server matching the specified criteria {args[1:]}!") + else: + run_proton(args) diff --git a/scripts/Common/Software/docker/services/jellyfin/rtorrent.Dockerfile b/scripts/Common/Software/docker/services/jellyfin/rtorrent.Dockerfile new file mode 100644 index 00000000..3fbeeffa --- /dev/null +++ b/scripts/Common/Software/docker/services/jellyfin/rtorrent.Dockerfile @@ -0,0 +1,121 @@ +FROM walt3rl/proton-privoxy AS proton +FROM jesec/rtorrent AS rtorrent +FROM debian + +ARG PVPN_CLI_VER=2.2.12 +ARG USERNAME=proton + +ENV PVPN_TAGS="+pmp" \ + PVPN_PROTOCOL=udp \ + PVPN_CMD_ARGS="connect --p2p --random" \ + PVPN_DEBUG= \ + HOST_NETWORK= \ + DNS_SERVERS_OVERRIDE= \ + PUID=1000 \ + PGID=1000 \ + PHOME=/home/${USERNAME} \ + NATPMP_TIMEOUT=60 \ + NATPMP_INTERVAL= \ + MAX_UPTIME= + +WORKDIR /root +COPY --from=rtorrent / / +RUN mkdir /app +COPY --from=proton /root/.pvpn-cli /root/.pvpn-cli +COPY --from=proton /app/proton-privoxy/run /app/proton + +RUN \ + sed -i \ + -e "/^exec privoxy/d" \ + -e "/^ln -s/d" \ + /app/proton \ + && install -t /usr/local/bin /app/proton \ + && rm /app/proton + +RUN apt-get update -y \ + && apt-get upgrade -y \ + && apt-get install -y \ + curl \ + gnupg \ + && curl https://repo.protonvpn.com/debian/dists/stable/main/binary-all/protonvpn-stable-release_1.0.3-3_all.deb -o proton.deb \ + && dpkg --install proton.deb \ + && apt-get remove -y \ + curl \ + && apt-get update -y \ + && apt-get install -y protonvpn-cli \ + && rm -rf /var/lib/apt/lists + +RUN apt-get update -y \ + && apt-get upgrade -y \ + && apt-get install -y \ + git \ + iproute2 \ + iptables \ + natpmpc \ + pipenv \ + python3-setuptools \ + sudo \ + && rm -rf /var/lib/apt/lists + +RUN \ + cd /app \ + && PIPENV_VENV_IN_PROJECT=1 pipenv install git+https://github.com/Rafficer/linux-cli-community.git@v$PVPN_CLI_VER#egg=protonvpn-cli + +RUN printf "%s\n" \ + "#!/usr/bin/env -S dbus-run-session -- bash" \ + "mkdir -p /proton/{keyrings,protonvpn}" \ + "mkdir -p ~/.local/share" \ + "mkdir -p ~/.config" \ + "ln -Ts /proton/keyrings ~/.local/share/keyrings >/dev/null 2>&1" \ + "ln -Ts /proton/protonvpn ~/.config/protonvpn >/dev/null 2>&1" \ + "eval \"\$(echo -n 'root' | gnome-keyring-daemon --unlock)\"" \ + "python3 /app/pvpn-cli.py \"\$@\"" > ./pvpn-cli \ + && install -Dm 755 ./pvpn-cli /usr/local/bin \ + && rm ./pvpn-cli + +RUN printf "%s\n" \ + "#!/bin/bash" \ + "groupadd --gid \$PGID ${USERNAME} > /dev/null" \ + "useradd --create-home --home-dir \$PHOME ${USERNAME} --uid \$PUID -g ${USERNAME} 2>/dev/null" \ + '[ ! -z "$1" ] && [ "$1" = "init" ] && export PVPN_CMD_ARGS="$@"' \ + "pvpn-cli || exit" \ + 'ip link show proton0 > /dev/null 2>&1 || exit' \ + 'fallback="$(expr ${NATPMP_TIMEOUT} \* 3 / 4)"' \ + 'export NATPMP_INTERVAL="${NATPMP_INTERVAL:-$fallback}"' \ + 'echo "Opening a port using NAT-PMP for $NATPMP_TIMEOUT seconds…"' \ + 'output="$(natpmpc -a 0 0 tcp "$NATPMP_TIMEOUT")"' \ + 'natpmpc -a 0 0 udp "$NATPMP_TIMEOUT"' \ + 'port="$(echo "$output" | grep -m 1 " public port [[:digit:]]\+ " | sed "s/.* public port \([[:digit:]]\+\).*/\\1/")"' \ + 'echo "Port $port has been opened for P2P data transfer!"' \ + 'echo "The NAT-PMP port forwarding will be updated every $NATPMP_INTERVAL seconds"' \ + 'export PEERPORT="$port"' \ + "{" \ + " while true" \ + " do" \ + ' echo "Refreshing NAT-PMP port forwarding…"' \ + ' natpmp -a 0 0 udp "$NATPMP_TIMEOUT"' \ + ' natpmpc -a 0 0 tcp "$NATPMP_TIMEOUT"' \ + ' echo "NAT-PMP port forwarding has been refreshed!"' \ + ' sleep "$NATPMP_INTERVAL"' \ + " done" \ + "} &" \ + "set -m" \ + '[ ${MAX_UPTIME:-0} -gt 0 ] && {' \ + ' soudo -iu '"${USERNAME}"' rtorrent -o network.port_range.set=$port-$port,system.daemon.set=true $@ &' \ + ' pid=$!' \ + ' sleep "$MAX_UPTIME"' \ + ' pkill -9 $pid' \ + '} || {' \ + ' sudo -u '"${USERNAME}"' rtorrent -o network.port_range.set=$port-$port,system.daemon.set=true $@' \ + '}' > ./rtorrent-entrypoint \ + && install -Dm 755 ./rtorrent-entrypoint /usr/local/bin \ + && rm ./rtorrent-entrypoint + +COPY pvpn-cli.py /app/pvpn-cli.py + +#RUN apt-get update -y \ +# && apt-get install -y sudo +# RUN echo "${USERNAME} ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers + +VOLUME [ "/proton" ] +ENTRYPOINT [ "rtorrent-entrypoint" ] diff --git a/scripts/Common/Software/docker/services/service.fish b/scripts/Common/Software/docker/services/service.fish index 0637fdbd..fe5ca5a7 100644 --- a/scripts/Common/Software/docker/services/service.fish +++ b/scripts/Common/Software/docker/services/service.fish @@ -68,10 +68,53 @@ begin echo "$domain" end + function getExtraServerConfig -a subdomain domain + end + + function getServiceLocationConfig -a domain service location + getServiceDefaultProxy $domain $service $location "" $argv + end + + function getServiceDefaultProxy -a domain service location + argparse -i "comment=" "path=" "url=" -- $argv + set -l url + set -l config (getServiceSecretsConfig $argv) + set -l portKey (__getServicePortKey "$service") + set -l port (yq --raw-output "$portKey" "$config" | extractPort) + + if [ -n "$_flag_url" ] + set url "$_flag_url" + else + set url "http://127.0.0.1:$port" + + if [ -n "$_flag_path" ] + set url "$url$_flag_path" + end + end + + printf "%s\n" \ + "location $location {" \ + (if [ -n "$_flag_comment" ] + echo "# $_flag_comment" + end) \ + "proxy_pass $url;" \ + 'proxy_set_header Host $host;' \ + 'proxy_set_header X-Real-IP $remote_addr;' \ + 'proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ + 'proxy_set_header X-Forwarded-Proto $scheme;' \ + 'proxy_set_header X-Forwarded-Protocol $scheme;' \ + 'proxy_set_header X-Forwarded-Host $http_host;' \ + (getExtraLocationSettings $argv) \ + "}" + end + + function getExtraLocationSettings -a domain service location + end + function initializeServiceInstallation -V nginxRoot - mkdir -p (getServiceRoot $argv) - mkdir -p "$nginxRoot" - mkdir -p (dirname (getServiceSecretsConfig $argv)) + sudo mkdir -p (getServiceRoot $argv) + sudo mkdir -p "$nginxRoot" + sudo mkdir -p (dirname (getServiceSecretsConfig $argv)) end function installDockerService -V dir -V nginxRoot @@ -79,9 +122,9 @@ begin set -l servers (getServiceServers $argv | string split0) for i in (seq 1 2 (count $servers)) - set -l locations (getServiceLocations $i $argv) + set -l locations (getServiceLocations $i $argv | string split0) - for j in (seq 1 4 (count $locations)) + for j in (seq 1 2 (count $locations)) set -l file (mktemp) set -l port (getRandomPort) set -l service $locations[$j] @@ -98,7 +141,6 @@ begin end function configureDockerService - set -l config (getServiceSecretsConfig $argv) set -l servers (getServiceServers $argv | string split0) set -l nginxConfig (__getServiceNginxConfig $argv) @@ -112,22 +154,13 @@ begin printf "%s\n" \ "server {" \ "listen 80;" \ - "server_name $domain;" + "server_name $domain;" \ + (getExtraServerConfig $subdomain $domain $argv) for j in (seq 1 2 (count $locations)) set -l service $locations[$j] set -l location $locations[(math $j + 1)] - set -l portKey (__getServicePortKey "$service") - set -l port (yq --raw-output "$portKey" "$config" | extractPort) - - printf "%s\n" \ - "location $location {" \ - "proxy_pass http://127.0.0.1:$port;" \ - 'proxy_set_header Host $host;' \ - 'proxy_set_header X-Real-IP $remote_addr;' \ - 'proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ - 'proxy_set_header X-Forwarded-Proto $scheme;' \ - "}" + getServiceLocationConfig $domain $service $location $argv end echo "}"