diff --git a/modules/hosts/14900k/_private/jellyfin-nfs-export.nix b/modules/hosts/14900k/_private/jellyfin-nfs-export.nix index 48336f5..577ecfb 100644 --- a/modules/hosts/14900k/_private/jellyfin-nfs-export.nix +++ b/modules/hosts/14900k/_private/jellyfin-nfs-export.nix @@ -1,4 +1,4 @@ -# NFS exports from nixdesk (14900k) to nix-server (192.168.2.238): +# NFS exports from nixdesk (14900k) to r5500 (192.168.2.100), formerly nix-server (192.168.2.238): # - /mnt/deep/jellyfin → nix-server /mnt/nixdesk-jellyfin (Jellyfin bulk libraries) # # Jellyfin root on nixdesk uses owner olivier + group nfsmedia (990); dirs here are 2775 so @@ -48,7 +48,8 @@ in # Squash nix-server clients to olivier:nfsmedia so Jellyfin can write .nfo/posters into # existing olivier-owned library folders (990-only squash was "other" r-x on typical 755 trees). exports = '' - /mnt/deep/jellyfin 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=${toString olivierUid},anongid=990,fsid=1) + /mnt/deep/jellyfin 192.168.2.100(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=${toString olivierUid},anongid=990,fsid=1) + /mnt/deep/jellyfin 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=${toString olivierUid},anongid=990,fsid=2) ''; }; diff --git a/modules/hosts/nix-server/_services/dispatcharr.nix b/modules/hosts/nix-server/_services/dispatcharr.nix index 3b2fae3..dcdd1cf 100644 --- a/modules/hosts/nix-server/_services/dispatcharr.nix +++ b/modules/hosts/nix-server/_services/dispatcharr.nix @@ -9,7 +9,9 @@ { systemd.tmpfiles.settings."nix-server-dispatcharr-data" = { "/var/lib/dispatcharr"."d" = { - mode = "0755"; + # Dispatcharr runs as a non-root user inside the container; keep /data writable. + # (Plugins like EPG Janitor write to `/data/*.json`.) + mode = "0777"; user = "root"; group = "root"; }; diff --git a/modules/hosts/nix-server/configuration.nix b/modules/hosts/nix-server/configuration.nix index 6c4cc8b..1dac5e8 100644 --- a/modules/hosts/nix-server/configuration.nix +++ b/modules/hosts/nix-server/configuration.nix @@ -17,20 +17,10 @@ self.nixosModules.users ./_services/attic-cache-server.nix ./_services/portainer.nix - ./_services/organizr.nix ./_services/swiftshare.nix ./_services/personal-website.nix ./_services/immich.nix - ./_services/jellyfin.nix - ./_services/nixdesk-nfs-client.nix ./_services/ddrm-media-server.nix - ./_services/sonarr.nix - ./_services/prowlarr.nix - ./_services/flaresolverr.nix - ./_services/radarr.nix - ./_services/qbittorrent.nix - ./_services/seerr.nix - ./_services/dispatcharr.nix ./_services/gitea.nix ./_services/cloudflare-dyndns.nix ]; diff --git a/modules/hosts/r5500/_private/media-disk.nix b/modules/hosts/r5500/_private/media-disk.nix new file mode 100644 index 0000000..3d54d62 --- /dev/null +++ b/modules/hosts/r5500/_private/media-disk.nix @@ -0,0 +1,52 @@ +# Media stack storage on r5500: btrfs subvolume @media-stack on the OS disk (sda4). +{ config, pkgs, lib, ... }: +let + btrfsUuid = "934a5ec3-4bab-49c3-96c9-c857c50076ba"; + btrfsDevice = "/dev/disk/by-uuid/${btrfsUuid}"; + # Created under subvol=@ → full path is @/@media-stack (not a top-level @media-stack). + mediaSubvol = "@/@media-stack"; +in +{ + # Create @media-stack before /mnt/media-stack is mounted. + systemd.services.r5500-media-stack-subvolume = { + description = "Create btrfs subvolume @media-stack on sda4 if missing"; + before = [ "mnt-media\\x2dmedia\\x2dstack.mount" ]; + wantedBy = [ "local-fs-pre.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + rootMount="/mnt/.r5500-btrfs-root" + mkdir -p "$rootMount" + if ! mountpoint -q "$rootMount"; then + mount -o subvol=@ "${btrfsDevice}" "$rootMount" + umountAfter=1 + else + umountAfter=0 + fi + if ! ${pkgs.btrfs-progs}/bin/btrfs subvolume show "$rootMount/@media-stack" >/dev/null 2>&1; then + ${pkgs.btrfs-progs}/bin/btrfs subvolume create "$rootMount/@media-stack" + fi + if [ "$umountAfter" -eq 1 ]; then + umount "$rootMount" + fi + ''; + }; + + # Optional 1 TiB cap (run once after first boot if desired): + # sudo btrfs quota enable /mnt/media-stack && sudo btrfs qgroup limit 1T /mnt/media-stack + + fileSystems."/mnt/media-stack" = { + device = btrfsDevice; + fsType = "btrfs"; + neededForBoot = false; + options = [ + "subvol=${mediaSubvol}" + "compress=zstd" + "noatime" + "nofail" + "x-systemd.device-timeout=30" + ]; + }; +} diff --git a/modules/hosts/r5500/_private/media-paths.nix b/modules/hosts/r5500/_private/media-paths.nix new file mode 100644 index 0000000..7c2b26a --- /dev/null +++ b/modules/hosts/r5500/_private/media-paths.nix @@ -0,0 +1,70 @@ +# Shared media group, directory layout, and Jellyfin config bind-mount on /mnt/media-stack. +{ config, lib, pkgs, ... }: +let + paths = import ./media-stack-paths.nix; + prowlarrCustomIndexer = "${./../_services/prowlarr/torrent9-custom.yml}"; + inherit (paths) + mediaRoot + downloadsDir + downloadsIncompleteDir + sonarrDataDir + radarrDataDir + prowlarrDataDir + qbittorrentDataDir + seerrConfigDir + dispatcharrDataDir + organizrDataDir + jellyfinConfigDir + jellyfinMoviesDir + jellyfinTvDir + ; +in +{ + _module.args.mediaStackPaths = paths; + + users.groups.media = { }; + + users.users.server.extraGroups = [ "media" ]; + + # Layout dirs only after /mnt/media-stack is mounted (tmpfiles at early boot would + # otherwise create paths on the root fs and break bind mounts / boot). + systemd.services.r5500-media-stack-dirs = { + description = "Create media-stack directory layout"; + after = [ "mnt-media\\x2dmedia\\x2dstack.mount" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + set -e + install -d -m 0775 -o root -g media ${mediaRoot} + install -d -m 2775 -o root -g media ${jellyfinMoviesDir} ${jellyfinTvDir} ${downloadsDir} + install -d -m 2775 -o root -g media ${downloadsIncompleteDir} + install -d -m 0755 -o jellyfin -g jellyfin ${jellyfinConfigDir} + install -d -m 0700 -o sonarr -g sonarr ${sonarrDataDir} + install -d -m 0700 -o radarr -g radarr ${radarrDataDir} + install -d -m 0700 -o prowlarr -g prowlarr ${prowlarrDataDir} + install -d -m 0750 -o qbittorrent -g qbittorrent ${qbittorrentDataDir} + install -d -m 0755 -o jellyseerr -g jellyseerr ${seerrConfigDir} 2>/dev/null \ + || install -d -m 0755 -o root -g root ${seerrConfigDir} + install -d -m 0777 -o root -g root ${dispatcharrDataDir} + install -d -m 0755 -o organizr -g organizr ${organizrDataDir} 2>/dev/null \ + || install -d -m 0755 -o root -g root ${organizrDataDir} + install -d -m 0700 -o prowlarr -g prowlarr ${prowlarrDataDir}/Definitions/Custom + ln -sfn ${prowlarrCustomIndexer} ${prowlarrDataDir}/Definitions/Custom/torrent9-custom.yml + ''; + }; + + # Jellyfin metadata on the media subvolume; nofail so a missing subvol never bricks boot. + fileSystems."/var/lib/jellyfin" = { + device = jellyfinConfigDir; + fsType = "none"; + neededForBoot = false; + options = [ + "bind" + "nofail" + "x-systemd.after=mnt-media\\x2dmedia\\x2dstack.mount" + ]; + }; +} diff --git a/modules/hosts/r5500/_private/media-stack-paths.nix b/modules/hosts/r5500/_private/media-stack-paths.nix new file mode 100644 index 0000000..195d030 --- /dev/null +++ b/modules/hosts/r5500/_private/media-stack-paths.nix @@ -0,0 +1,16 @@ +# Path constants for the r5500 media stack (imported by _services/* and media-paths.nix). +{ + mediaRoot = "/mnt/media-stack"; + downloadsDir = "/mnt/media-stack/downloads"; + downloadsIncompleteDir = "/mnt/media-stack/downloads/incomplete"; + sonarrDataDir = "/mnt/media-stack/sonarr"; + radarrDataDir = "/mnt/media-stack/radarr"; + prowlarrDataDir = "/mnt/media-stack/prowlarr"; + qbittorrentDataDir = "/mnt/media-stack/qbittorrent"; + seerrConfigDir = "/mnt/media-stack/seerr"; + dispatcharrDataDir = "/mnt/media-stack/dispatcharr"; + organizrDataDir = "/mnt/media-stack/organizr"; + jellyfinConfigDir = "/mnt/media-stack/jellyfin/config"; + jellyfinMoviesDir = "/mnt/media-stack/jellyfin/movies"; + jellyfinTvDir = "/mnt/media-stack/jellyfin/tv"; +} diff --git a/modules/hosts/r5500/_services/dispatcharr.nix b/modules/hosts/r5500/_services/dispatcharr.nix new file mode 100644 index 0000000..1a9d64e --- /dev/null +++ b/modules/hosts/r5500/_services/dispatcharr.nix @@ -0,0 +1,34 @@ +{ lib, pkgs, mediaStackPaths, ... }: +let + dataDir = mediaStackPaths.dispatcharrDataDir; +in +{ + systemd.tmpfiles.settings."r5500-dispatcharr-data" = { + "${dataDir}"."d" = { + mode = "0777"; + user = "root"; + group = "root"; + }; + }; + + systemd.services.docker-dispatcharr.preStart = lib.mkBefore '' + ${pkgs.coreutils}/bin/mkdir -p ${dataDir} + ''; + + virtualisation.oci-containers.containers.dispatcharr = { + image = "ghcr.io/dispatcharr/dispatcharr:latest"; + ports = [ "9191:9191" ]; + volumes = [ + "${dataDir}:/data" + ]; + environment = { + DISPATCHARR_ENV = "aio"; + REDIS_HOST = "localhost"; + CELERY_BROKER_URL = "redis://localhost:6379/0"; + DISPATCHARR_LOG_LEVEL = "info"; + TZ = "America/Moncton"; + }; + }; + + networking.firewall.allowedTCPPorts = [ 9191 ]; +} diff --git a/modules/hosts/r5500/_services/docker-media.nix b/modules/hosts/r5500/_services/docker-media.nix new file mode 100644 index 0000000..ea67df3 --- /dev/null +++ b/modules/hosts/r5500/_services/docker-media.nix @@ -0,0 +1,6 @@ +# Docker backend for Dispatcharr and Organizr on r5500. +{ ... }: +{ + virtualisation.docker.enable = true; + virtualisation.oci-containers.backend = "docker"; +} diff --git a/modules/hosts/r5500/_services/flaresolverr.nix b/modules/hosts/r5500/_services/flaresolverr.nix new file mode 100644 index 0000000..55d0f13 --- /dev/null +++ b/modules/hosts/r5500/_services/flaresolverr.nix @@ -0,0 +1,5 @@ +{ ... }: +{ + services.flaresolverr.enable = true; + networking.firewall.allowedTCPPorts = [ 8191 ]; +} diff --git a/modules/hosts/r5500/_services/jellyfin.nix b/modules/hosts/r5500/_services/jellyfin.nix new file mode 100644 index 0000000..93e182b --- /dev/null +++ b/modules/hosts/r5500/_services/jellyfin.nix @@ -0,0 +1,29 @@ +# Jellyfin on r5500. Local libraries: /mnt/media-stack/jellyfin/{movies,tv}. +# Bulk libraries: /mnt/nixdesk-jellyfin/{movies,tv} (NFS from 14900k). +{ lib, ... }: +{ + nixpkgs.overlays = [ + (final: prev: { + jellyfin-web = prev.jellyfin-web.overrideAttrs (oldAttrs: { + postInstall = + (oldAttrs.postInstall or "") + + '' + find "$out" -type f \( -name 'banner-light.*.png' -o -name 'banner-dark.*.png' \) \ + -exec truncate -s 0 {} \; + ''; + }); + }) + ]; + + users.users.jellyfin.extraGroups = [ "media" ]; + + services.jellyfin = { + enable = true; + openFirewall = true; + }; + + systemd.services.jellyfin.serviceConfig = { + SupplementaryGroups = [ "media" ]; + PrivateUsers = lib.mkForce false; + }; +} diff --git a/modules/hosts/r5500/_services/nixdesk-nfs-client.nix b/modules/hosts/r5500/_services/nixdesk-nfs-client.nix new file mode 100644 index 0000000..9b32db3 --- /dev/null +++ b/modules/hosts/r5500/_services/nixdesk-nfs-client.nix @@ -0,0 +1,28 @@ +# NFS mounts of nixdesk (14900k) bulk storage for r5500. Exports live in +# modules/hosts/14900k/_private/jellyfin-nfs-export.nix +# +# Jellyfin library paths: +# Movies → /mnt/nixdesk-jellyfin/movies +# Shows → /mnt/nixdesk-jellyfin/tv +{ ... }: +let + nfsExportHost = "192.168.2.25"; + nfsClientOpts = [ + "rw" + "noatime" + "nofail" + "_netdev" + "nfsvers=3" + "tcp" + "lookupcache=none" + "x-systemd.automount" + "x-systemd.idle-timeout=3600" + ]; +in +{ + fileSystems."/mnt/nixdesk-jellyfin" = { + device = "${nfsExportHost}:/mnt/deep/jellyfin"; + fsType = "nfs"; + options = nfsClientOpts; + }; +} diff --git a/modules/hosts/r5500/_services/organizr.nix b/modules/hosts/r5500/_services/organizr.nix new file mode 100644 index 0000000..3675bb9 --- /dev/null +++ b/modules/hosts/r5500/_services/organizr.nix @@ -0,0 +1,48 @@ +{ lib, pkgs, mediaStackPaths, ... }: +let + configDir = mediaStackPaths.organizrDataDir; +in +{ + users.groups.organizr = { gid = 950; }; + users.users.organizr = { + isSystemUser = true; + uid = 950; + group = "organizr"; + }; + + systemd.tmpfiles.settings."r5500-organizr-config" = { + "${configDir}"."d" = { + mode = "0755"; + user = "organizr"; + group = "organizr"; + }; + }; + + systemd.tmpfiles.settings."r5500-organizr-config-perms" = { + "${configDir}"."Z" = { + mode = "0755"; + user = "organizr"; + group = "organizr"; + }; + }; + + systemd.services.docker-organizr.preStart = lib.mkBefore '' + ${pkgs.coreutils}/bin/mkdir -p ${configDir} + ${pkgs.coreutils}/bin/chown -R organizr:organizr ${configDir} + ''; + + virtualisation.oci-containers.containers.organizr = { + image = "ghcr.io/organizr/organizr:latest"; + ports = [ "8888:80" ]; + volumes = [ + "${configDir}:/config" + ]; + environment = { + PUID = "950"; + PGID = "950"; + TZ = "America/Moncton"; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8888 ]; +} diff --git a/modules/hosts/r5500/_services/prowlarr.nix b/modules/hosts/r5500/_services/prowlarr.nix new file mode 100644 index 0000000..40424a3 --- /dev/null +++ b/modules/hosts/r5500/_services/prowlarr.nix @@ -0,0 +1,20 @@ +{ lib, mediaStackPaths, ... }: +{ + services.prowlarr = { + enable = true; + dataDir = mediaStackPaths.prowlarrDataDir; + }; + + users.groups.prowlarr = { }; + users.users.prowlarr = { + isSystemUser = true; + group = "prowlarr"; + extraGroups = [ "media" ]; + }; + + systemd.services.prowlarr.serviceConfig.ReadWritePaths = lib.mkAfter [ + mediaStackPaths.prowlarrDataDir + ]; + + networking.firewall.allowedTCPPorts = [ 9696 ]; +} diff --git a/modules/hosts/r5500/_services/prowlarr/torrent9-custom.yml b/modules/hosts/r5500/_services/prowlarr/torrent9-custom.yml new file mode 100644 index 0000000..d985cbb --- /dev/null +++ b/modules/hosts/r5500/_services/prowlarr/torrent9-custom.yml @@ -0,0 +1,193 @@ +--- +id: torrent9-custom +name: Torrent9 (Custom URL) +description: "Torrent9 is a FRENCH Public site for MOVIES / TV / GENERAL" +language: fr-FR +type: public +encoding: UTF-8 +followredirect: true +testlinktorrent: false +links: + - https://www.torrent9.club/ + - https://www6.torrent9.to/ +legacylinks: + - https://www.torrent9.pl/ # this is a proxy for torrent9clone + - https://torrent9.black-mirror.xyz/ # this is a proxy for torrent9clone + - https://torrent9.unblocked.casa/ # this is a proxy for torrent9clone + - https://torrent9.proxyportal.fun/ # this is a proxy for torrent9clone + - https://torrent9.uk-unblock.xyz/ # this is a proxy for torrent9clone + - https://torrent9.ind-unblock.xyz/ # this is a proxy for torrent9clone + - https://ww1.torrent9.to/ + - https://www.torrent9.is/ + - https://torrent9.li/ # not a proxy for torrent9 or torrent9clone + - https://www.oxtorrent.me/ + - https://www.torrent9.gg/ + - https://www.torrent9.fi/ # this is the torrent9clone domain + - https://www.torrent9.fm/ + - https://torrent9.se/ # redirect to www. + - https://torrent9.ninjaproxy1.com/ # no response data + - https://torrent9.proxyninja.org/ # Error 1007 + - https://www.torrent9.se/ + - https://torrent9.unblockninja.com/ # 403 forbidden + - https://ww1.torrent9.fm/ + - https://www.torrent9.zone/ # clone? details links are broken + - https://torrent9.to/ + - https://ww2.torrent9.to/ + - https://www5.torrent9.to/ + +caps: + # dont forget to update the search fields category case block + categorymappings: + - {id: films, cat: Movies, desc: "Movies"} + - {id: series, cat: TV, desc: "TV"} + - {id: musique, cat: Audio, desc: "Music"} + - {id: ebook, cat: Books, desc: "Books"} + - {id: logiciels, cat: PC, desc: "Software"} + - {id: jeux-pc, cat: PC/Games, desc: "PC Games"} + - {id: other, cat: Other, desc: "Other"} # dummy cat for results missing icon + + modes: + search: [q] + tv-search: [q, season, ep] + movie-search: [q] + music-search: [q] + book-search: [q] + allowrawsearch: true + +settings: + - name: info_flaresolverr + type: info_flaresolverr + - name: multilang + type: checkbox + label: Replace MULTi by another language in release name + default: false + - name: multilanguage + type: select + label: Replace MULTi by this language + default: FRENCH + options: + FRENCH: FRENCH + MULTi FRENCH: MULTi FRENCH + ENGLISH: ENGLISH + MULTi ENGLISH: MULTi ENGLISH + VOSTFR: VOSTFR + MULTi VOSTFR: MULTi VOSTFR + - name: vostfr + type: checkbox + label: Replace VOSTFR and SUBFRENCH with ENGLISH + default: false + - name: sort + type: select + label: Sort requested from site (Only works for searches with Keywords) + default: ".html" + options: + ".html": best + ".html,trie-date-d": created desc + ".html,trie-date-a": created asc + ".html,trie-seeds-d": seeders desc + ".html,trie-seeds-a": seeders asc + ".html,trie-poid-d": size desc + ".html,trie-poid-a": size asc + ".html,trie-nom-d": title desc + ".html,trie-nom-a": title asc + +download: + selectors: + - selector: a[href^="magnet:?"] + attribute: href + - selector: script:contains("magnet:?") + filters: + - name: regexp + args: "\\s'(magnet:\\?.+?)';" + +search: + paths: + - path: "{{ if .Keywords }}search_torrent/{{ .Keywords }}{{ .Config.sort }}{{ else }}home/{{ end }}" + keywordsfilters: + # if searching for season packs with S01 to saison 1 #9712 + - name: re_replace + args: ["(?i)(S0)(\\d{1,2})$", "saison $2"] + - name: re_replace + args: ["(?i)(S)(\\d{1,3})$", "saison $2"] + - name: replace + args: [" ", "-"] + + headers: + # site blocks all Linux User-Agents, so use a slightly altered Windows Jackett UA here (e.g. Safari/537.36 > Safari/537.35) + User-Agent: ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.35"] + + rows: + selector: table.table-striped > tbody > tr + filters: + - name: andmatch + + fields: + category_optional: + selector: td:nth-child(1) i + optional: true + case: + i[class="fa fa-video-camera"]: films + i[class="fa fa-tv"]: series # search by name + i[class="fa fa-desktop"]: series # keywordless search + i[class="fa fa-music"]: musique + i[class="fa fa-gamepad"]: jeux-pc + i[class="fa fa-laptop"]: logiciels + i[class="fa fa-book"]: ebook + category: + text: "{{ if .Result.category_optional }}{{ .Result.category_optional }}{{ else }}other{{ end }}" + title_default: + selector: td:nth-child(1) a + title_optional: + selector: td:nth-child(1) a[title] + attribute: title + optional: true + title_phase1: + text: "{{ if .Result.title_optional }}{{ .Result.title_optional }}{{ else }}{{ .Result.title_default }}{{ end }}" + filters: + - name: re_replace + args: ["(?i)\\b(FRENCH|MULTI|TRUEFRENCH|VOSTFR|SUBFRENCH)\\b(.+?)(\\b((19|20)\\d{2})\\b)$", "$3 $1$2"] + title_vostfr: + text: "{{ .Result.title_phase1 }}" + filters: + - name: re_replace + args: ["(?i)\\b(vostfr|subfrench)\\b", "ENGLISH"] + title_phase2: + text: "{{ if .Config.vostfr }}{{ .Result.title_vostfr }}{{ else }}{{ .Result.title_phase1 }}{{ end }}" + title_multilang: + text: "{{ .Result.title_phase2 }}" + filters: + - name: re_replace + args: ["(?i)\\b(MULTI(?!.*(?:FRENCH|ENGLISH|VOSTFR)))\\b", "{{ .Config.multilanguage }}"] + title: + text: "{{ if .Config.multilang }}{{ .Result.title_multilang }}{{ else }}{{ .Result.title_phase2 }}{{ end }}" + details: + selector: td:nth-child(1) a + attribute: href + download: + selector: td:nth-child(1) a + attribute: href + date: + selector: td:nth-child(2):contains("/") + optional: true + default: now + filters: + - name: dateparse + args: "dd/MM/yyyy" + size: + selector: "{{ if .Keywords }}td:nth-child(3){{ else }}td:nth-child(2){{ end }}" + filters: + - name: re_replace + args: ["(\\w)o", "$1B"] + seeders: + selector: "{{ if .Keywords }}td:nth-child(4){{ else }}td:nth-child(3){{ end }}" + optional: true + default: 0 + leechers: + selector: "{{ if .Keywords }}td:nth-child(5){{ else }}td:nth-child(4){{ end }}" + optional: true + default: 0 + downloadvolumefactor: + text: 0 + uploadvolumefactor: + text: 1 +# engine n/a diff --git a/modules/hosts/r5500/_services/qbittorrent.nix b/modules/hosts/r5500/_services/qbittorrent.nix new file mode 100644 index 0000000..95f61f7 --- /dev/null +++ b/modules/hosts/r5500/_services/qbittorrent.nix @@ -0,0 +1,43 @@ +{ lib, mediaStackPaths, ... }: +let + webPort = 8081; + btPort = 51413; + inherit (mediaStackPaths) downloadsDir downloadsIncompleteDir qbittorrentDataDir; +in +{ + services.qbittorrent = { + enable = true; + openFirewall = true; + webuiPort = webPort; + torrentingPort = btPort; + }; + + users.groups.qbittorrent = { }; + users.users.qbittorrent = { + isSystemUser = true; + group = "qbittorrent"; + extraGroups = [ "media" ]; + home = qbittorrentDataDir; + }; + + fileSystems."/var/lib/qbittorrent" = { + device = qbittorrentDataDir; + fsType = "none"; + neededForBoot = false; + options = [ + "bind" + "nofail" + "x-systemd.after=mnt-media\\x2dmedia\\x2dstack.mount" + ]; + }; + + networking.firewall.allowedTCPPorts = [ webPort btPort ]; + networking.firewall.allowedUDPPorts = [ btPort ]; + + # Default save path for new torrents (existing qBittorrent.conf may override after migration). + systemd.services.qbittorrent.preStart = lib.mkAfter '' + mkdir -p ${downloadsDir} ${downloadsIncompleteDir} + chmod 2775 ${downloadsDir} ${downloadsIncompleteDir} + chgrp media ${downloadsDir} ${downloadsIncompleteDir} + ''; +} diff --git a/modules/hosts/r5500/_services/radarr.nix b/modules/hosts/r5500/_services/radarr.nix new file mode 100644 index 0000000..ed54711 --- /dev/null +++ b/modules/hosts/r5500/_services/radarr.nix @@ -0,0 +1,20 @@ +{ lib, mediaStackPaths, ... }: +{ + services.radarr = { + enable = true; + dataDir = mediaStackPaths.radarrDataDir; + }; + + users.groups.radarr = { }; + users.users.radarr = { + isSystemUser = true; + group = "radarr"; + extraGroups = [ "media" ]; + }; + + systemd.services.radarr.serviceConfig.ReadWritePaths = lib.mkAfter [ + mediaStackPaths.radarrDataDir + ]; + + networking.firewall.allowedTCPPorts = [ 7878 ]; +} diff --git a/modules/hosts/r5500/_services/seerr.nix b/modules/hosts/r5500/_services/seerr.nix new file mode 100644 index 0000000..e0f49cc --- /dev/null +++ b/modules/hosts/r5500/_services/seerr.nix @@ -0,0 +1,35 @@ +{ lib, mediaStackPaths, ... }: +{ + nixpkgs.overlays = [ + (final: prev: { + seerr = prev.seerr.overrideAttrs (oldAttrs: { + postInstall = + (oldAttrs.postInstall or "") + + '' + find "$out/share/public" -maxdepth 1 -type f \( -name 'logo_full.svg' -o -name 'logo_stacked.svg' \) \ + -exec truncate -s 0 {} \; + ''; + }); + }) + ]; + + services.seerr = { + enable = true; + openFirewall = true; + configDir = mediaStackPaths.seerrConfigDir; + }; + + users.groups.jellyseerr = { }; + users.users.jellyseerr = { + isSystemUser = true; + group = "jellyseerr"; + }; + + systemd.services.seerr.serviceConfig = { + DynamicUser = lib.mkForce false; + User = "jellyseerr"; + Group = "jellyseerr"; + ReadWritePaths = [ mediaStackPaths.seerrConfigDir ]; + }; + +} diff --git a/modules/hosts/r5500/_services/sonarr.nix b/modules/hosts/r5500/_services/sonarr.nix new file mode 100644 index 0000000..5f106b1 --- /dev/null +++ b/modules/hosts/r5500/_services/sonarr.nix @@ -0,0 +1,20 @@ +{ lib, mediaStackPaths, ... }: +{ + services.sonarr = { + enable = true; + dataDir = mediaStackPaths.sonarrDataDir; + }; + + users.groups.sonarr = { }; + users.users.sonarr = { + isSystemUser = true; + group = "sonarr"; + extraGroups = [ "media" ]; + }; + + systemd.services.sonarr.serviceConfig.ReadWritePaths = lib.mkAfter [ + mediaStackPaths.sonarrDataDir + ]; + + networking.firewall.allowedTCPPorts = [ 8989 ]; +} diff --git a/modules/hosts/r5500/configuration.nix b/modules/hosts/r5500/configuration.nix index 7f4c93f..1f6455a 100644 --- a/modules/hosts/r5500/configuration.nix +++ b/modules/hosts/r5500/configuration.nix @@ -14,6 +14,19 @@ inputs.sops-nix.nixosModules.sops self.nixosModules.system self.nixosModules.users + ./_private/media-disk.nix + ./_private/media-paths.nix + ./_services/docker-media.nix + ./_services/nixdesk-nfs-client.nix + ./_services/jellyfin.nix + ./_services/sonarr.nix + ./_services/radarr.nix + ./_services/prowlarr.nix + ./_services/flaresolverr.nix + ./_services/seerr.nix + ./_services/qbittorrent.nix + ./_services/dispatcharr.nix + ./_services/organizr.nix ]; boot.loader.grub = {