Implement r5500 media stack configuration with NFS and Docker services

- Added configuration for media stack on r5500, including paths for Jellyfin, Sonarr, Radarr, and other media services.
- Integrated NFS client for accessing Jellyfin libraries from nixdesk.
- Established Docker services for Dispatcharr and Organizr, including necessary user and group setups.
- Created systemd services for managing media directories and ensuring proper permissions.
This commit is contained in:
2026-06-04 16:57:15 -03:00
parent 2a911b057b
commit 380b428d9a
19 changed files with 638 additions and 13 deletions
@@ -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 ];
}
@@ -0,0 +1,6 @@
# Docker backend for Dispatcharr and Organizr on r5500.
{ ... }:
{
virtualisation.docker.enable = true;
virtualisation.oci-containers.backend = "docker";
}
@@ -0,0 +1,5 @@
{ ... }:
{
services.flaresolverr.enable = true;
networking.firewall.allowedTCPPorts = [ 8191 ];
}
@@ -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;
};
}
@@ -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;
};
}
@@ -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 ];
}
@@ -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 ];
}
@@ -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
@@ -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}
'';
}
+20
View File
@@ -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 ];
}
+35
View File
@@ -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 ];
};
}
+20
View File
@@ -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 ];
}