Add jellyseerr

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-07-16 12:26:27 +01:00
parent e087cdb630
commit 03e53accae
41 changed files with 1968 additions and 824 deletions

View File

@@ -1,118 +0,0 @@
{ user, home }:
{
config,
inputs,
pkgs,
system,
...
}:
let
selfPkgs = inputs.self.packages.${system};
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers volumes networks;
mkApp = type: name: shortName: urlBase: port: mediaFolderBase: {
inherit
type
name
shortName
urlBase
port
mediaFolderBase
;
};
arrs = [
(mkApp "radarr" "Radarr" "radarr" "/manage/films" 7878 "/films")
(mkApp "radarr" "Radarr (UHD)" "radarr-uhd" "/manage/films/uhd" 7878 "/films")
(mkApp "radarr" "Radarr (Anime)" "radarr-anime" "/manage/anime/films" 7878 "/anime/films")
(mkApp "sonarr" "Sonarr" "sonarr" "/manage/shows" 8989 "/shows")
(mkApp "sonarr" "Sonarr (UHD)" "sonarr-uhd" "/manage/shows/uhd" 8989 "/shows")
(mkApp "sonarr" "Sonarr (Anime)" "sonarr-anime" "/manage/anime/shows" 8989 "/anime/shows")
];
in
{
imports = [
(import ./prowlarr { inherit user home arrs; })
(import ./recyclarr { inherit user home arrs; })
];
home-manager.users.${user} = {
sops = {
secrets = builtins.listToAttrs (
builtins.map (arr: {
name = "${arr.shortName}/apiKey";
value.sopsFile = ../../../../../../../secrets/secrets.yaml;
}) arrs
);
templates = builtins.listToAttrs (
builtins.map (arr: {
name = "${arr.shortName}-env";
value.content = ''
API_KEY=${hmConfig.sops.placeholder."${arr.shortName}/apiKey"}
'';
}) arrs
);
};
virtualisation.quadlet = {
volumes = builtins.listToAttrs (
builtins.map (arr: {
name = arr.shortName;
value = { };
}) arrs
);
containers = builtins.listToAttrs (
builtins.map (arr: {
name = arr.shortName;
value = {
containerConfig = {
image = "docker-archive:${selfPkgs."docker-${arr.type}"}";
networks = [
networks.media.ref
networks.transmission.ref
networks.traefik.ref
];
volumes =
let
postStart = pkgs.writeTextFile {
name = "post-start.sh";
executable = true;
text = ''
${builtins.readFile ./common.sh}
${builtins.readFile ./${arr.type}/post-start.sh}
'';
};
in
[
"${postStart}:/etc/${arr.type}/post-start.sh:ro"
"${volumes.${arr.shortName}.ref}:/var/lib/${arr.type}"
"/mnt/storage/private/storm/containers/storage/volumes/transmission-data/_data:/var/lib/transmission"
"/mnt/storage/private/storm/containers/storage/volumes/media/_data:/var/lib/media"
];
environments = {
INSTANCE_NAME = arr.name;
URL_BASE = arr.urlBase;
ROOT_FOLDER = "/var/lib/media${arr.mediaFolderBase}";
DOWNLOAD_CATEGORY = arr.shortName;
};
environmentFiles = [ hmConfig.sops.templates."${arr.shortName}-env".path ];
labels = [
"traefik.enable=true"
"traefik.http.routers.${arr.shortName}.rule=Host(`media.karaolidis.com`) && PathPrefix(`${arr.urlBase}`)"
"traefik.http.routers.${arr.shortName}.middlewares=authelia@docker"
];
};
unitConfig.After = [
"${containers.transmission._serviceName}.service"
"sops-nix.service"
];
};
}) arrs
);
};
};
}

View File

@@ -1,134 +0,0 @@
{
user,
home,
arrs,
}:
{
config,
inputs,
pkgs,
system,
...
}:
let
selfPkgs = inputs.self.packages.${system};
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers volumes networks;
arrMapping = {
radarr = {
implementation = "Radarr";
configContract = "RadarrSettings";
};
sonarr = {
implementation = "Sonarr";
configContract = "SonarrSettings";
};
};
in
{
home-manager.users.${user} = {
sops = {
secrets."prowlarr/apiKey".sopsFile = ../../../../../../../../secrets/secrets.yaml;
templates =
{
prowlarr-env.content = ''
API_KEY=${hmConfig.sops.placeholder."prowlarr/apiKey"}
'';
}
// builtins.listToAttrs (
builtins.map (arr: {
name = "prowlarr-${arr.shortName}";
value.content = builtins.readFile (
(pkgs.formats.json { }).generate "${arr.shortName}.json" {
enable = true;
name = arr.name;
inherit (arrMapping.${arr.type}) implementation configContract;
syncLevel = "fullSync";
fields = [
{
name = "prowlarrUrl";
value = "http://prowlarr:9696";
}
{
name = "baseUrl";
value = "http://${arr.shortName}:${builtins.toString arr.port}";
}
{
name = "apiKey";
value = hmConfig.sops.placeholder."${arr.shortName}/apiKey";
}
];
}
);
}) arrs
);
};
virtualisation.quadlet = {
networks.flaresolverr = { };
volumes.prowlarr = { };
containers = (
let
arrServices = builtins.map (arr: "${containers.${arr.shortName}._serviceName}.service") arrs;
in
{
flaresolverr.containerConfig = {
image = "docker-archive:${selfPkgs.docker-flaresolverr}";
networks = [ networks.flaresolverr.ref ];
};
prowlarr = {
containerConfig = {
image = "docker-archive:${selfPkgs.docker-prowlarr}";
networks = [
networks.media.ref
networks.transmission.ref
networks.flaresolverr.ref
networks.traefik.ref
];
volumes =
let
postStart = pkgs.writeTextFile {
name = "post-start.sh";
executable = true;
text = ''
${builtins.readFile ../common.sh}
${builtins.readFile ./post-start.sh}
'';
};
in
[
"${postStart}:/etc/prowlarr/post-start.sh:ro"
"${volumes.prowlarr.ref}:/var/lib/prowlarr"
]
++ builtins.map (
arr:
"${
hmConfig.sops.templates."prowlarr-${arr.shortName}".path
}:/etc/prowlarr/apps/${arr.shortName}.json:ro"
) arrs;
environments.URL_BASE = "/manage/indexers";
environmentFiles = [ hmConfig.sops.templates.prowlarr-env.path ];
labels = [
"traefik.enable=true"
"traefik.http.routers.prowlarr.rule=Host(`media.karaolidis.com`) && PathPrefix(`/manage/indexers`)"
"traefik.http.routers.prowlarr.middlewares=authelia@docker"
];
};
unitConfig.After = [
"${containers.transmission._serviceName}.service"
"${containers.flaresolverr._serviceName}.service"
"sops-nix.service"
] ++ arrServices;
};
}
);
};
};
}

View File

@@ -1,58 +0,0 @@
# shellcheck shell=sh
# shellcheck disable=SC2034
HOST="http://localhost:7878$URL_BASE"
# shellcheck disable=SC2034
API_SUBPATH="/api/v3"
DOWNLOAD_CATEGORY="${DOWNLOAD_CATEGORY:-radarr}"
build_transmission_payload() {
cat <<-EOF
{
"enable": true,
"protocol": "torrent",
"priority": 1,
"name": "Transmission",
"fields": [
{ "name": "host", "value": "transmission" },
{ "name": "port", "value": 9091 },
{ "name": "urlBase", "value": "" },
{ "name": "movieCategory", "value": "$DOWNLOAD_CATEGORY" }
],
"implementation": "Transmission",
"configContract": "TransmissionSettings"
}
EOF
}
build_rootfolder_payload() {
cat <<-EOF
{
"path": "$ROOT_FOLDER"
}
EOF
}
wait_for_api
if [ -n "$ROOT_FOLDER" ]; then
insert_or_skip_resource "rootfolder" "path" "$ROOT_FOLDER" "$(build_rootfolder_payload)"
prune_resources "rootfolder" "path" "$ROOT_FOLDER"
fi
try_forever upsert_resource "downloadclient" "name" "Transmission" "$(build_transmission_payload)"
prune_resources "downloadclient" "name" "Transmission"
delete_resource "qualityprofile" "name" "SD"
delete_resource "qualityprofile" "name" "HD-720p"
delete_resource "qualityprofile" "name" "HD-1080p"
delete_resource "qualityprofile" "name" "HD - 720p/1080p"
delete_resource "qualityprofile" "name" "Ultra-HD"
try_forever sh -c "[ $(get_resources qualityprofile | jq length) -eq 2 ]"
get_resources qualityprofile | jq -c '.[]' | while IFS= read -r profile; do
id="$(printf '%s' "$profile" | jq -r '.id')"
patched="$(printf '%s' "$profile" | jq '.language.id = -2 | .language.Name = "Original"')"
echo "Patching qualityprofile id=$id with language = Original"
call PUT "qualityprofile/$id?forceSave=true" "$patched"
done

View File

@@ -1,55 +0,0 @@
{ base_url, api_key }:
{
radarr.radarr_anime = {
inherit base_url api_key;
delete_old_custom_formats = true;
replace_existing_custom_formats = true;
include = [
{ template = "radarr-quality-definition-anime"; }
{ template = "radarr-quality-profile-anime"; }
{ template = "radarr-custom-formats-anime"; }
];
custom_formats = [
{
trash_ids = [
"064af5f084a0a24458cc8ecd3220f93f" # Uncensored
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
{
trash_ids = [
"a5d148168c4506b55cf53984107c396e" # 10bit
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
{
trash_ids = [
"4a3b087eea2ce012fcc1ce319259a3be" # Anime Dual Audio
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
];
media_naming = {
folder = "default";
movie = {
rename = true;
standard = "anime";
};
};
};
}

View File

@@ -1,74 +0,0 @@
{ base_url, api_key }:
{
radarr.radarr_uhd = {
inherit base_url api_key;
delete_old_custom_formats = true;
replace_existing_custom_formats = true;
include = [
{ template = "radarr-quality-definition-movie"; }
{ template = "radarr-quality-profile-uhd-bluray-web"; }
{ template = "radarr-custom-formats-uhd-bluray-web"; }
];
custom_formats = [
{
trash_ids = [
"570bc9ebecd92723d2d21500f4be314c" # Remaster
"eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
"e0c07d59beb37348e975a930d5e50319" # Criterion Collection
"9d27d9d2181838f76dee150882bdc58c" # Masters of Cinema
"db9b4c4b53d312a3ca5f1378f6440fc9" # Vinegar Syndrome
"957d0f44b592285f26449575e8b1167e" # Special Edition
"eecf3a857724171f968a66cb5719e152" # IMAX
"9f6cbff8cfe4ebbc1bde14c7b7bec0de" # IMAX Enhanced
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
{
trash_ids = [
"b6832f586342ef70d9c128d40c07b872" # Bad Dual Groups
"cc444569854e9de0b084ab2b8b1532b2" # Black and White Editions
"ae9b7c9ebde1f3bd336a8cbd1ec4c5e5" # No-RlsGroup
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
{
trash_ids = [
"dc98083864ea246d05a42df0d05f81cc" # x265 (HD)
];
assign_scores_to = [
{
name = "UHD Bluray + WEB";
score = 0;
}
];
}
{
trash_ids = [
"839bea857ed2c0a8e084f3cbdbd65ecb" # x265 (no HDR/DV)
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
{
trash_ids = [
"923b6abef9b17f937fab56cfcf89e1f1" # DV (WEBDL)
"b17886cb4158d9fea189859409975758" # HDR10Plus Boost
"55a5b50cb416dea5a50c4955896217ab" # DV HDR10+ Boost
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
{
trash_ids = [
"9c38ebb7384dada637be8899efa68e6f" # SDR
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
];
media_naming = {
folder = "default";
movie = {
rename = true;
standard = "default";
};
};
};
}

View File

@@ -1,43 +0,0 @@
{ base_url, api_key }:
{
radarr.radarr = {
inherit base_url api_key;
delete_old_custom_formats = true;
replace_existing_custom_formats = true;
include = [
{ template = "radarr-quality-definition-movie"; }
{ template = "radarr-quality-profile-hd-bluray-web"; }
{ template = "radarr-custom-formats-hd-bluray-web"; }
];
custom_formats = [
{
trash_ids = [
"570bc9ebecd92723d2d21500f4be314c" # Remaster
"eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
"e0c07d59beb37348e975a930d5e50319" # Criterion Collection
"9d27d9d2181838f76dee150882bdc58c" # Masters of Cinema
"db9b4c4b53d312a3ca5f1378f6440fc9" # Vinegar Syndrome
"957d0f44b592285f26449575e8b1167e" # Special Edition
"eecf3a857724171f968a66cb5719e152" # IMAX
"9f6cbff8cfe4ebbc1bde14c7b7bec0de" # IMAX Enhanced
];
assign_scores_to = [ { name = "HD Bluray + WEB"; } ];
}
{
trash_ids = [
"b6832f586342ef70d9c128d40c07b872" # Bad Dual Groups
"cc444569854e9de0b084ab2b8b1532b2" # Black and White Editions
"ae9b7c9ebde1f3bd336a8cbd1ec4c5e5" # No-RlsGroup
];
assign_scores_to = [ { name = "HD Bluray + WEB"; } ];
}
];
media_naming = {
folder = "default";
movie = {
rename = true;
standard = "default";
};
};
};
}

View File

@@ -1,58 +0,0 @@
{ base_url, api_key }:
{
sonarr.sonarr_anime = {
inherit base_url api_key;
delete_old_custom_formats = true;
replace_existing_custom_formats = true;
include = [
{ template = "sonarr-quality-definition-anime"; }
{ template = "sonarr-v4-quality-profile-anime"; }
{ template = "sonarr-v4-custom-formats-anime"; }
];
custom_formats = [
{
trash_ids = [
"026d5aadd1a6b4e550b134cb6c72b3ca" # Uncensored
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
{
trash_ids = [
"b2550eb333d27b75833e25b8c2557b38" # 10bit
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
{
trash_ids = [
"418f50b10f1907201b6cfdf881f467b7" # Anime Dual Audio
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
];
media_naming = {
series = "default";
season = "default";
episodes = {
rename = true;
standard = "default";
daily = "default";
anime = "default";
};
};
};
}

View File

@@ -1,63 +0,0 @@
{ base_url, api_key }:
{
sonarr.sonarr_uhd = {
inherit base_url api_key;
delete_old_custom_formats = true;
replace_existing_custom_formats = true;
include = [
{ template = "sonarr-quality-definition-series"; }
{ template = "sonarr-v4-quality-profile-web-2160p"; }
{ template = "sonarr-v4-custom-formats-web-2160p"; }
];
custom_formats = [
{
trash_ids = [
"9b27ab6498ec0f31a3353992e19434ca" # DV (WEBDL)
"0dad0a507451acddd754fe6dc3a7f5e7" # HDR10+ Boost
"385e9e8581d33133c3961bdcdeffb7b4" # DV HDR10+ Boost
];
assign_scores_to = [ { name = "WEB-2160p"; } ];
}
{
trash_ids = [
"32b367365729d530ca1c124a0b180c64" # Bad Dual Groups
"82d40da2bc6923f41e14394075dd4b03" # No-RlsGroup
];
assign_scores_to = [ { name = "WEB-2160p"; } ];
}
{
trash_ids = [
"47435ece6b99a0b477caf360e79ba0bb" # x265 (HD)
];
assign_scores_to = [
{
name = "WEB-2160p";
score = 0;
}
];
}
{
trash_ids = [
"9b64dff695c2115facf1b6ea59c9bd07" # x265 (no HDR/DV)
];
assign_scores_to = [ { name = "WEB-2160p"; } ];
}
{
trash_ids = [
"2016d1676f5ee13a5b7257ff86ac9a93" # SDR
];
assign_scores_to = [ { name = "WEB-2160p"; } ];
}
];
media_naming = {
series = "default";
season = "default";
episodes = {
rename = true;
standard = "default";
daily = "default";
anime = "default";
};
};
};
}

View File

@@ -1,49 +0,0 @@
{ base_url, api_key }:
{
sonarr.sonarr = {
inherit base_url api_key;
delete_old_custom_formats = true;
replace_existing_custom_formats = true;
include = [
{ template = "sonarr-quality-definition-series"; }
{ template = "sonarr-v4-quality-profile-web-1080p"; }
{ template = "sonarr-v4-custom-formats-web-1080p"; }
];
custom_formats = [
{
trash_ids = [
"32b367365729d530ca1c124a0b180c64" # Bad Dual Groups
"82d40da2bc6923f41e14394075dd4b03" # No-RlsGroup
];
assign_scores_to = [ { name = "WEB-1080p"; } ];
}
{
trash_ids = [
"47435ece6b99a0b477caf360e79ba0bb" # x265 (HD)
];
assign_scores_to = [
{
name = "WEB-1080p";
score = 0;
}
];
}
{
trash_ids = [
"9b64dff695c2115facf1b6ea59c9bd07" # x265 (no HDR/DV)
];
assign_scores_to = [ { name = "WEB-1080p"; } ];
}
];
media_naming = {
series = "default";
season = "default";
episodes = {
rename = true;
standard = "default";
daily = "default";
anime = "default";
};
};
};
}

View File

@@ -1,57 +0,0 @@
{
user,
home,
arrs,
}:
{
config,
inputs,
pkgs,
system,
...
}:
let
selfPkgs = inputs.self.packages.${system};
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers networks;
in
{
home-manager.users.${user} = {
sops.templates = builtins.listToAttrs (
builtins.map (arr: {
name = "recyclarr-${arr.shortName}";
value.content = builtins.readFile (
(pkgs.formats.yaml { }).generate "${arr.shortName}.yaml" (
import ./apps/${arr.shortName}.nix {
base_url = "http://${arr.shortName}:${builtins.toString arr.port}${arr.urlBase}/";
api_key = hmConfig.sops.placeholder."${arr.shortName}/apiKey";
}
)
);
}) arrs
);
virtualisation.quadlet.containers = (
let
arrServices = builtins.map (arr: "${containers.${arr.shortName}._serviceName}.service") arrs;
in
{
# FIXME: https://recyclarr.dev/wiki/behavior/quality-profiles/#language
recyclarr = {
containerConfig = {
image = "docker-archive:${selfPkgs.docker-recyclarr}";
networks = [ networks.media.ref ];
volumes = builtins.map (
arr:
"${
hmConfig.sops.templates."recyclarr-${arr.shortName}".path
}:/var/lib/recyclarr/configs/${arr.shortName}.yaml:ro"
) arrs;
};
unitConfig.After = [ "sops-nix.service" ] ++ arrServices;
};
}
);
};
}

View File

@@ -1,50 +0,0 @@
# shellcheck shell=sh
# shellcheck disable=SC2034
HOST="http://localhost:8989$URL_BASE"
# shellcheck disable=SC2034
API_SUBPATH="/api/v3"
DOWNLOAD_CATEGORY="${DOWNLOAD_CATEGORY:-sonarr}"
build_transmission_payload() {
cat <<-EOF
{
"enable": true,
"protocol": "torrent",
"priority": 1,
"name": "Transmission",
"fields": [
{ "name": "host", "value": "transmission" },
{ "name": "port", "value": 9091 },
{ "name": "urlBase", "value": "" },
{ "name": "tvCategory", "value": "$DOWNLOAD_CATEGORY" }
],
"implementation": "Transmission",
"configContract": "TransmissionSettings"
}
EOF
}
build_rootfolder_payload() {
cat <<-EOF
{
"path": "$ROOT_FOLDER"
}
EOF
}
wait_for_api
if [ -n "$ROOT_FOLDER" ]; then
insert_or_skip_resource "rootfolder" "path" "$ROOT_FOLDER" "$(build_rootfolder_payload)"
prune_resources "rootfolder" "path" "$ROOT_FOLDER"
fi
try_forever upsert_resource "downloadclient" "name" "Transmission" "$(build_transmission_payload)"
prune_resources "downloadclient" "name" "Transmission"
delete_resource "qualityprofile" "name" "SD"
delete_resource "qualityprofile" "name" "HD-720p"
delete_resource "qualityprofile" "name" "HD-1080p"
delete_resource "qualityprofile" "name" "HD - 720p/1080p"
delete_resource "qualityprofile" "name" "Ultra-HD"

View File

@@ -1,9 +1,54 @@
{ user, home }:
{ ... }:
{ config, ... }:
let
hmConfig = config.home-manager.users.${user};
radarrs = [
(import ./radarr/apps/radarr.nix { inherit hmConfig; })
(import ./radarr/apps/radarr-uhd.nix { inherit hmConfig; })
(import ./radarr/apps/radarr-anime.nix { inherit hmConfig; })
];
sonarrs = [
(import ./sonarr/apps/sonarr.nix { inherit hmConfig; })
(import ./sonarr/apps/sonarr-uhd.nix { inherit hmConfig; })
(import ./sonarr/apps/sonarr-anime.nix { inherit hmConfig; })
];
in
{
imports = [
(import ./jellyfin { inherit user home; })
(import ./arr { inherit user home; })
(import ./jellyseerr {
inherit
user
home
radarrs
sonarrs
;
})
(import ./prowlarr {
inherit
user
home
radarrs
sonarrs
;
})
(import ./recyclarr {
inherit
user
home
radarrs
sonarrs
;
})
(import ./radarr { inherit user home radarrs; })
(import ./sonarr { inherit user home sonarrs; })
];
home-manager.users.${user} = {

View File

@@ -1,5 +1,8 @@
# shellcheck shell=sh
JELLYFIN_HOST="${JELLYFIN_HOST:-http://localhost:8096}"
JELLYFIN_ADMIN_USERNAME="${JELLYFIN_ADMIN_USERNAME:-admin}"
until response="$(curl -sf "$JELLYFIN_HOST/System/Info/Public")"; do
echo "Waiting for Jellyfin to be ready..."
sleep 1

View File

@@ -0,0 +1,204 @@
{
user,
home,
radarrs,
sonarrs,
}:
{
config,
inputs,
pkgs,
lib,
system,
...
}:
let
selfPkgs = inputs.self.packages.${system};
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers volumes networks;
arrs = radarrs ++ sonarrs;
jellyseerrAutheliaClientId = "s8QyVqBdiEStH5WXeEYNSrEh8ls2xHif0qyTGbC7V8nHNcqHi5NhqHUapCHuVFT4kEtngqgLry2SKOKepQl3AiqCWlhTjlIxr7LI";
in
{
home-manager.users.${user} = {
sops = {
secrets = {
"jellyseerr/smtp".sopsFile = ../../../../../../../secrets/secrets.yaml;
"jellyseerr/authelia/password".sopsFile = ../../../../../../../secrets/secrets.yaml;
"jellyseerr/authelia/digest".sopsFile = ../../../../../../../secrets/secrets.yaml;
};
templates =
{
jellyseerr-env.content = ''
JELLYFIN_ADMIN_PASSWORD=${hmConfig.sops.placeholder."jellyfin/admin"}
'';
jellyseerr.content = builtins.readFile (
(pkgs.formats.json { }).generate "setings.json" {
main = {
applicationTitle = "Jellyseerr";
applicationUrl = "https://request.karaolidis.com";
cacheImages = true;
# https://github.com/fallenbagel/jellyseerr/blob/d53ffca5db9b09bd1055936c2472d54a78f937b8/server/lib/permissions.ts#L7
# REQUEST | CREATE_ISSUES | RECENT_VIEW
# 32 | 4194304 | 67108864
defaultPermissions = 71303200;
localLogin = false;
mediaServerLogin = false;
oidcLogin = true;
newPlexLogin = false;
mediaServerType = 2;
partialRequestsEnabled = true;
enableSpecialEpisodes = true;
};
jellyfin = {
name = "jupiter";
ip = "jellyfin";
port = 8096;
externalHostname = "https://media.karaolidis.com";
jellyfinForgotPasswordUrl = "https://id.karaolidis.com/reset-password/step1";
};
oidc.providers = [
{
slug = "authelia";
name = "Authelia";
issuerUrl = "https://id.karaolidis.com";
clientId = jellyseerrAutheliaClientId;
clientSecret = hmConfig.sops.placeholder."jellyseerr/authelia/password";
scopes = lib.strings.concatStringsSep " " [
"openid"
"profile"
"email"
"groups"
];
newUserLogin = true;
}
];
radarr = [ ];
sonarr = [ ];
public.initialized = true;
notifications.agents.email = {
enabled = true;
options = {
emailFrom = "jupiter@karaolidis.com";
smtpHost = "smtp.protonmail.ch";
smtpPort = 587;
authUser = "jupiter@karaolidis.com";
authPass = hmConfig.sops.placeholder."jellyseerr/smtp";
senderName = "Jellyseerr";
};
};
network.trustProxy = true;
}
);
authelia-jellyseerr.content = builtins.readFile (
(pkgs.formats.yaml { }).generate "jellyseerr.yaml" {
identity_providers.oidc = {
authorization_policies.jellyseerr = {
default_policy = "deny";
rules = [
{
policy = "one_factor";
subject = "group:jellyfin";
}
];
};
clients = [
{
client_id = jellyseerrAutheliaClientId;
client_name = "jellyseerr";
client_secret = hmConfig.sops.placeholder."jellyseerr/authelia/digest";
redirect_uris = [ "https://request.karaolidis.com/login?provider=authelia&callback=true" ];
authorization_policy = "jellyseerr";
scopes = [
"openid"
"email"
"profile"
"groups"
];
token_endpoint_auth_method = "client_secret_post";
}
];
};
}
);
}
// builtins.listToAttrs (
builtins.map (arr: {
name = "jellyseerr-${arr.hostName}";
value.content = builtins.readFile (
(pkgs.formats.json { }).generate "${arr.hostName}.json" arr.jellyseerrConfig
);
}) arrs
);
};
virtualisation.quadlet = {
volumes.jellyseerr = { };
containers = {
jellyseerr = {
containerConfig = {
image = "docker-archive:${selfPkgs.docker-jellyseerr}";
networks = [
networks.jellyfin.ref
networks.media.ref
networks.traefik.ref
];
volumes =
let
preStart = pkgs.writeTextFile {
name = "pre-start.sh";
executable = true;
text = builtins.readFile ./pre-start.sh;
};
in
[
"${preStart}:/etc/jellyseerr/pre-start.sh:ro"
"${hmConfig.sops.templates.jellyseerr.path}:/etc/jellyseerr/settings.default.json:ro"
"${volumes.jellyseerr.ref}:/var/lib/jellyseerr"
]
++ builtins.map (
radarr:
"${
hmConfig.sops.templates."jellyseerr-${radarr.hostName}".path
}:/etc/jellyseerr/apps/radarr/${radarr.hostName}.json:ro"
) radarrs
++ builtins.map (
sonarr:
"${
hmConfig.sops.templates."jellyseerr-${sonarr.hostName}".path
}:/etc/jellyseerr/apps/sonarr/${sonarr.hostName}.json:ro"
) sonarrs;
environmentFiles = [ hmConfig.sops.templates.jellyseerr-env.path ];
labels = [
"traefik.enable=true"
"traefik.http.routers.jellyseerr.rule=Host(`request.karaolidis.com`)"
];
};
unitConfig.After =
let
arrServices = builtins.map (arr: "${containers.${arr.hostName}._serviceName}.service") arrs;
in
[ "sops-nix.service" ] ++ arrServices;
};
authelia.containerConfig.volumes = [
"${hmConfig.sops.templates.authelia-jellyseerr.path}:/etc/authelia/conf.d/jellyseerr.yaml:ro"
];
};
};
};
}

View File

@@ -0,0 +1,158 @@
# shellcheck shell=sh
JELLYFIN_HOST="${JELLYFIN_HOST:-http://jellyfin:8096}"
JELLYFIN_ADMIN_USERNAME="${JELLYFIN_ADMIN_USERNAME:-admin}"
until public="$(curl -sf "$JELLYFIN_HOST/System/Info/Public")"; do
echo "Waiting for Jellyfin to be ready..."
sleep 1
done
until [ "$(echo "$public" | jq -r '.StartupWizardCompleted')" = "true" ]; do
echo "Waiting for Jellyfin setup wizard to finish..."
sleep 1
public="$(curl -sf "${JELLYFIN_HOST}/System/Info/Public")"
done
token="$(curl -sf "$JELLYFIN_HOST/Users/AuthenticateByName" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: MediaBrowser Client="jellyseerr-init", Device="sh", DeviceId="sh", Version="1.0"' \
--data-raw '{"Username":"'"$JELLYFIN_ADMIN_USERNAME"'","Pw":"'"$JELLYFIN_ADMIN_PASSWORD"'"}' \
| jq -r '.AccessToken')"
keys="$(curl -sf "$JELLYFIN_HOST/Auth/Keys" \
-H 'Authorization: MediaBrowser Token="'"$token"'"')"
jellyseerr_key="$(echo "$keys" | jq -r '.Items[] | select(.AppName=="Jellyseerr") | .AccessToken')"
if [ -z "$jellyseerr_key" ] || [ "$jellyseerr_key" = "null" ]; then
curl -sf "$JELLYFIN_HOST/Auth/Keys?App=Jellyseerr" \
-X POST \
-H 'Authorization: MediaBrowser Token="'"$token"'"'
keys="$(curl -sf "$JELLYFIN_HOST/Auth/Keys" \
-H 'Authorization: MediaBrowser Token="'"$token"'"')"
jellyseerr_key="$(echo "$keys" | jq -r '.Items[] | select(.AppName=="Jellyseerr") | .AccessToken')"
fi
serverId="$(echo "$public" | jq -r '.Id')"
libraries="$(curl -sf "$JELLYFIN_HOST/Library/VirtualFolders" \
-H 'Authorization: MediaBrowser Token="'"$token"'"')"
libraries="$(
echo "$libraries" | jq '[
.[]
| {
id: .ItemId,
name: .Name,
enabled: .LibraryOptions.Enabled,
type: ( .CollectionType | if . == "movies" then "movie" elif . == "tvshows" then "show" else . end )
}
]'
)"
tmpfile="$(mktemp)"
jq -s \
--arg serverId "$serverId" \
--arg apiKey "$jellyseerr_key" \
--argjson libraries "$libraries" \
'.[0] * .[1] # merge default + existing
| .jellyfin.serverId = $serverId
| .jellyfin.apiKey = $apiKey
| .jellyfin.libraries = $libraries' \
/var/lib/jellyseerr/settings.json \
/etc/jellyseerr/settings.default.json \
> "$tmpfile"
mv "$tmpfile" /var/lib/jellyseerr/settings.json
try_forever() {
until "$@" 2>&1; do
echo "Try failed: $* - retrying in 1s"
sleep 1
done
}
wait_for_api() {
try_forever curl -sf -H "X-Api-Key: $API_KEY" "$HOST$API_SUBPATH/health"
echo "$HOST$API_SUBPATH is up!"
}
call() {
method="$1"
path="$2"
data="${3:-}"
if [ -n "$data" ]; then
curl -sf \
-X "$method" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
--data-raw "$data" \
"$HOST$API_SUBPATH/$path"
else
curl -sf \
-X "$method" \
-H "X-Api-Key: $API_KEY" \
"$HOST$API_SUBPATH/$path"
fi
}
get_resources() {
call GET "$1"
}
get_resource_id() {
endpoint="$1"
ident_field="$2"
name="$3"
get_resources "$endpoint" | jq -r --arg field "$ident_field" --arg name "$name" '.[] | select(.[$field] == $name) | .id // empty'
}
fetch_profile() {
cfg="$(cat "$1")"
HOST="http://$(echo "$cfg" | jq -r '.hostname'):$(echo "$cfg" | jq -r '.port')$(echo "$cfg" | jq -r '.baseUrl')"
API_SUBPATH="/api/v3"
API_KEY="$(echo "$cfg" | jq -r '.apiKey')"
wait_for_api
profile_name="$(echo "$cfg" | jq -r '.activeProfileName')"
profile_id="$(get_resource_id qualityProfile name "$profile_name")"
}
radarr_json='[]'
for f in /etc/jellyseerr/apps/radarr/*.json; do
fetch_profile "$f"
enriched="$(echo "$cfg" | jq --argjson profile_id "$profile_id" '. + { activeProfileId: $profile_id }')"
radarr_json="$(echo "$radarr_json" "$enriched" | jq -s '.[0] + [.[1]]')"
done
sonarr_json='[]'
for f in /etc/jellyseerr/apps/sonarr/*.json; do
fetch_profile "$f"
enriched="$(echo "$cfg" | jq --argjson profile_id "$profile_id" '. + { activeProfileId: $profile_id, activeAnimeProfileId: $profile_id }')"
sonarr_json="$(echo "$sonarr_json" "$enriched" | jq -s '.[0] + [.[1]]')"
done
tmpfile=$(mktemp)
jq -s \
--argjson libs "$libraries" \
--argjson radarr "$radarr_json" \
--argjson sonarr "$sonarr_json" \
--arg serverId "$serverId" \
--arg apiKey "$jellyseerr_key" \
'.[0] * .[1]
| .jellyfin.serverId = $serverId
| .jellyfin.apiKey = $apiKey
| .jellyfin.libraries = $libs
| .radarr = $radarr
| .sonarr = $sonarr' \
/var/lib/jellyseerr/settings.json \
/etc/jellyseerr/settings.default.json \
> "$tmpfile"
mv "$tmpfile" /var/lib/jellyseerr/settings.json

View File

@@ -0,0 +1,102 @@
{
user,
home,
radarrs,
sonarrs,
}:
{
config,
inputs,
pkgs,
system,
...
}:
let
selfPkgs = inputs.self.packages.${system};
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers volumes networks;
arrs = radarrs ++ sonarrs;
in
{
home-manager.users.${user} = {
sops = {
secrets."prowlarr/apiKey".sopsFile = ../../../../../../../secrets/secrets.yaml;
templates =
{
prowlarr-env.content = ''
API_KEY=${hmConfig.sops.placeholder."prowlarr/apiKey"}
'';
}
// builtins.listToAttrs (
builtins.map (arr: {
name = "prowlarr-${arr.hostName}";
value.content = builtins.readFile (
(pkgs.formats.json { }).generate "${arr.hostName}.json" arr.prowlarrConfig
);
}) arrs
);
};
virtualisation.quadlet = {
networks.flaresolverr = { };
volumes.prowlarr = { };
containers = {
flaresolverr.containerConfig = {
image = "docker-archive:${selfPkgs.docker-flaresolverr}";
networks = [ networks.flaresolverr.ref ];
};
prowlarr = {
containerConfig = {
image = "docker-archive:${selfPkgs.docker-prowlarr}";
networks = [
networks.media.ref
networks.transmission.ref
networks.flaresolverr.ref
networks.traefik.ref
];
volumes =
let
postStart = pkgs.writeTextFile {
name = "post-start.sh";
executable = true;
text = builtins.readFile ./post-start.sh;
};
in
[
"${postStart}:/etc/prowlarr/post-start.sh:ro"
"${volumes.prowlarr.ref}:/var/lib/prowlarr"
]
++ builtins.map (
arr:
"${
hmConfig.sops.templates."prowlarr-${arr.hostName}".path
}:/etc/prowlarr/apps/${arr.hostName}.json:ro"
) arrs;
environments.URL_BASE = "/manage/indexers";
environmentFiles = [ hmConfig.sops.templates.prowlarr-env.path ];
labels = [
"traefik.enable=true"
"traefik.http.routers.prowlarr.rule=Host(`media.karaolidis.com`) && PathPrefix(`/manage/indexers`)"
"traefik.http.routers.prowlarr.middlewares=authelia@docker"
];
};
unitConfig.After =
let
arrServices = builtins.map (arr: "${containers.${arr.hostName}._serviceName}.service") arrs;
in
[
"${containers.transmission._serviceName}.service"
"${containers.flaresolverr._serviceName}.service"
"sops-nix.service"
]
++ arrServices;
};
};
};
};
}

View File

@@ -5,6 +5,130 @@ HOST="http://localhost:9696$URL_BASE"
# shellcheck disable=SC2034
API_SUBPATH="/api/v1"
try_forever() {
until "$@" 2>&1; do
echo "Try failed: $* - retrying in 1s"
sleep 1
done
}
try() {
attempts="$1"
shift
delay=1
count=1
while [ "$count" -le "$attempts" ]; do
if "$@" 2>&1; then
return 0
fi
if [ "$count" -lt "$attempts" ]; then
echo "Attempt $count/$attempts failed, retrying in ${delay}s..."
sleep "$delay"
delay="$(( delay * 2 ))"
fi
count="$(( count + 1 ))"
done
echo "All $attempts attempts failed for: $*"
}
wait_for_api() {
try_forever curl -sf -H "X-Api-Key: $API_KEY" "$HOST$API_SUBPATH/health"
echo "API is up!"
}
call() {
method="$1"
path="$2"
data="${3:-}"
if [ -n "$data" ]; then
curl -sf \
-X "$method" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
--data-raw "$data" \
"$HOST$API_SUBPATH/$path"
else
curl -sf \
-X "$method" \
-H "X-Api-Key: $API_KEY" \
"$HOST$API_SUBPATH/$path"
fi
}
get_resources() {
call GET "$1"
}
get_resource_id() {
endpoint="$1"
ident_field="$2"
name="$3"
get_resources "$endpoint" | jq -r --arg field "$ident_field" --arg name "$name" '.[] | select(.[$field] == $name) | .id // empty'
}
insert_or_skip_resource() {
endpoint="$1"
ident_field="$2"
name="$3"
json="$4"
all="$(get_resources "$endpoint")"
id="$(printf '%s' "$all" | jq -r --arg field "$ident_field" --arg name "$name" '.[] | select(.[$field]==$name) | .id')"
if [ -n "$id" ] && [ "$id" != "null" ]; then
echo "Skipping existing $endpoint '$name' (id=$id)"
else
echo "Creating $endpoint '$name'"
call POST "$endpoint?forceSave=true" "$json"
fi
}
upsert_resource() {
endpoint="$1"
ident_field="$2"
name="$3"
json="$4"
all="$(get_resources "$endpoint")"
id="$(printf '%s' "$all" | jq -r --arg field "$ident_field" --arg name "$name" '.[] | select(.[$field]==$name) | .id')"
if [ -n "$id" ] && [ "$id" != "null" ]; then
echo "Updating $endpoint '$name' (id=$id)"
call PUT "$endpoint/$id?forceSave=true" "$json"
else
echo "Creating $endpoint '$name'"
call POST "$endpoint?forceSave=true" "$json"
fi
}
prune_resources() {
endpoint="$1"
ident_field="$2"
shift 2
keep_list="$(printf '%s\n' "$@" | jq -R . | jq -s .)"
resources="$(get_resources "$endpoint")"
printf '%s' "$resources" | jq -c '.[]' | while IFS= read -r client; do
name="$(printf '%s' "$client" | jq -r --arg field "$ident_field" '.[$field]')"
id="$(printf '%s' "$client" | jq -r '.id')"
found="$(printf '%s' "$keep_list" | jq -r --arg name "$name" 'index($name)')"
if [ "$found" = "null" ]; then
echo "Deleting extra $endpoint '$name' (id=$id)"
call DELETE "$endpoint/$id" || echo "failed to delete $name, continuing"
fi
done
}
build_transmission_payload() {
cat <<-EOF
{
@@ -62,8 +186,8 @@ build_cardigann_indexer_payload() {
--argjson priority "$priority" \
--arg definition_file "$definition_file" \
--arg base_url "$base_url" \
--argjson extra_fields "${extra_fields_json:-[]}" \
--argjson tags "${tags_json:-[]}" '
--argjson extra_fields "$extra_fields_json" \
--argjson tags "$tags_json" '
{
enable: true,
appProfileId: 1,
@@ -104,18 +228,18 @@ done
# shellcheck disable=SC2086
eval "prune_resources \"applications\" \"name\" $app_names"
try 5 upsert_resource "indexer" "name" "1337x" "$(build_cardigann_indexer_payload "1337x" 25 "1337x" "https://1337x.to/" "" "[$flaresolverr_id]")"
try 5 upsert_resource "indexer" "name" "Internet Archive" "$(build_cardigann_indexer_payload "Internet Archive" 25 "internetarchive" "https://archive.org/")"
try 5 upsert_resource "indexer" "name" "kickasstorrents.to" "$(build_cardigann_indexer_payload "kickasstorrents.to" 25 "kickasstorrents-to" "https://kickass.torrentbay.st/" "" "[$flaresolverr_id]")"
try 5 upsert_resource "indexer" "name" "kickasstorrents.ws" "$(build_cardigann_indexer_payload "kickasstorrents.ws" 25 "kickasstorrents-ws" "https://kickass.ws/")"
try 5 upsert_resource "indexer" "name" "LimeTorrents" "$(build_cardigann_indexer_payload "LimeTorrents" 25 "limetorrents" "https://www.limetorrents.lol/")"
try 5 upsert_resource "indexer" "name" "Nyaa.si" "$(build_cardigann_indexer_payload "Nyaa.si" 25 "nyaasi" "https://nyaa.si/" '[{"name":"sonarr_compatibility","value":true},{"name":"strip_s01","value":true},{"name":"radarr_compatibility","value":true}]')"
try 5 upsert_resource "indexer" "name" "The Pirate Bay" "$(build_cardigann_indexer_payload "The Pirate Bay" 25 "thepiratebay" "https://thepiratebay.org/")"
try 5 upsert_resource "indexer" "name" "TheRARBG" "$(build_cardigann_indexer_payload "TheRARBG" 25 "therarbg" "https://therarbg.to/")"
try 5 upsert_resource "indexer" "name" "Torlock" "$(build_cardigann_indexer_payload "Torlock" 25 "torlock" "https://www.torlock.com/")"
try 5 upsert_resource "indexer" "name" "TorrentDownload" "$(build_cardigann_indexer_payload "TorrentDownload" 25 "torrentdownload" "https://www.torrentdownload.info/")"
try 5 upsert_resource "indexer" "name" "Torrent Downloads" "$(build_cardigann_indexer_payload "Torrent Downloads" 25 "torrentdownloads" "https://www.torrentdownloads.pro/")"
try 5 upsert_resource "indexer" "name" "YourBittorrent" "$(build_cardigann_indexer_payload "YourBittorrent" 25 "yourbittorrent" "https://yourbittorrent.com/")"
try 5 upsert_resource "indexer" "name" "1337x" "$(build_cardigann_indexer_payload "1337x" 25 "1337x" "https://1337x.to/" "[]" "[$flaresolverr_id]")"
try 5 upsert_resource "indexer" "name" "Internet Archive" "$(build_cardigann_indexer_payload "Internet Archive" 25 "internetarchive" "https://archive.org/")" "[]" "[]"
try 5 upsert_resource "indexer" "name" "kickasstorrents.to" "$(build_cardigann_indexer_payload "kickasstorrents.to" 25 "kickasstorrents-to" "https://kickass.torrentbay.st/" "[]" "[$flaresolverr_id]")"
try 5 upsert_resource "indexer" "name" "kickasstorrents.ws" "$(build_cardigann_indexer_payload "kickasstorrents.ws" 25 "kickasstorrents-ws" "https://kickass.ws/")" "[]" "[]"
try 5 upsert_resource "indexer" "name" "LimeTorrents" "$(build_cardigann_indexer_payload "LimeTorrents" 25 "limetorrents" "https://www.limetorrents.lol/")" "[]" "[]"
try 5 upsert_resource "indexer" "name" "Nyaa.si" "$(build_cardigann_indexer_payload "Nyaa.si" 25 "nyaasi" "https://nyaa.si/" '[{"name":"sonarr_compatibility","value":true},{"name":"strip_s01","value":true},{"name":"radarr_compatibility","value":true}]')" "[]"
try 5 upsert_resource "indexer" "name" "The Pirate Bay" "$(build_cardigann_indexer_payload "The Pirate Bay" 25 "thepiratebay" "https://thepiratebay.org/")" "[]" "[]"
try 5 upsert_resource "indexer" "name" "TheRARBG" "$(build_cardigann_indexer_payload "TheRARBG" 25 "therarbg" "https://therarbg.to/")" "[]" "[]"
try 5 upsert_resource "indexer" "name" "Torlock" "$(build_cardigann_indexer_payload "Torlock" 25 "torlock" "https://www.torlock.com/")" "[]" "[]"
try 5 upsert_resource "indexer" "name" "TorrentDownload" "$(build_cardigann_indexer_payload "TorrentDownload" 25 "torrentdownload" "https://www.torrentdownload.info/")" "[]" "[]"
try 5 upsert_resource "indexer" "name" "Torrent Downloads" "$(build_cardigann_indexer_payload "Torrent Downloads" 25 "torrentdownloads" "https://www.torrentdownloads.pro/")" "[]" "[]"
try 5 upsert_resource "indexer" "name" "YourBittorrent" "$(build_cardigann_indexer_payload "YourBittorrent" 25 "yourbittorrent" "https://yourbittorrent.com/")" "[]" "[]"
prune_resources "indexer" "name" "1337x" "Internet Archive" "kickasstorrents.to" "kickasstorrents.ws" "LimeTorrents" "Nyaa.si" "The Pirate Bay" "TheRARBG" "Torlock" "TorrentDownload" "Torrent Downloads" "YourBittorrent"

View File

@@ -0,0 +1,88 @@
rec {
mkUrl =
{
hostName,
port,
urlBase,
}:
"http://${hostName}:${builtins.toString port}${urlBase}/";
mkProwlarrConfig =
{
name,
hostName,
port,
urlBase,
apiKey,
}:
{
inherit name;
enable = true;
implementation = "Radarr";
configContract = "RadarrSettings";
syncLevel = "fullSync";
fields = [
{
name = "prowlarrUrl";
value = "http://prowlarr:9696";
}
{
name = "baseUrl";
value = mkUrl { inherit hostName port urlBase; };
}
{
name = "apiKey";
value = apiKey;
}
];
};
mkRecyclarrConfig =
{
hostName,
port,
urlBase,
apiKey,
extraConfig,
}:
{
radarr.${hostName} = {
base_url = mkUrl { inherit hostName port urlBase; };
api_key = apiKey;
delete_old_custom_formats = true;
replace_existing_custom_formats = true;
} // extraConfig;
};
mkJellyseerrConfig =
{
name,
hostName,
port,
urlBase,
mediaFolderBase,
apiKey,
id,
recyclarrProfile,
is4k,
isAnime,
}:
{
inherit
name
port
apiKey
id
is4k
;
hostname = hostName;
baseUrl = urlBase;
activeProfileName = recyclarrProfile;
activeDirectory = "/var/lib/media${mediaFolderBase}";
minimumAvailability = "released";
isDefault = !isAnime;
externalUrl = "https://media.karaolidis.com${urlBase}";
syncEnabled = true;
};
}

View File

@@ -0,0 +1,96 @@
{ hmConfig }:
let
inherit (import ./common.nix) mkProwlarrConfig mkRecyclarrConfig mkJellyseerrConfig;
in
rec {
name = "Radarr (Anime)";
hostName = "radarr-anime";
urlBase = "/manage/anime/films";
mediaFolderBase = "/anime/films";
port = 7878;
prowlarrConfig = mkProwlarrConfig {
inherit
name
hostName
port
urlBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
};
recyclarrConfig = mkRecyclarrConfig {
inherit hostName port urlBase;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
extraConfig = {
include = [
{ template = "radarr-quality-definition-anime"; }
{ template = "radarr-quality-profile-anime"; }
{ template = "radarr-custom-formats-anime"; }
];
custom_formats = [
{
trash_ids = [
"064af5f084a0a24458cc8ecd3220f93f" # Uncensored
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
{
trash_ids = [
"a5d148168c4506b55cf53984107c396e" # 10bit
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
{
trash_ids = [
"4a3b087eea2ce012fcc1ce319259a3be" # Anime Dual Audio
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
];
media_naming = {
folder = "default";
movie = {
rename = true;
standard = "anime";
};
};
};
};
jellyseerrConfig = mkJellyseerrConfig {
inherit
name
hostName
port
urlBase
mediaFolderBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
id = 2;
recyclarrProfile = "Remux-1080p - Anime";
is4k = false;
isAnime = true;
};
}

View File

@@ -0,0 +1,115 @@
{ hmConfig }:
let
inherit (import ./common.nix) mkProwlarrConfig mkRecyclarrConfig mkJellyseerrConfig;
in
rec {
name = "Radarr (UHD)";
hostName = "radarr-uhd";
urlBase = "/manage/films/uhd";
mediaFolderBase = "/films";
port = 7878;
prowlarrConfig = mkProwlarrConfig {
inherit
name
hostName
port
urlBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
};
recyclarrConfig = mkRecyclarrConfig {
inherit hostName port urlBase;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
extraConfig = {
include = [
{ template = "radarr-quality-definition-movie"; }
{ template = "radarr-quality-profile-uhd-bluray-web"; }
{ template = "radarr-custom-formats-uhd-bluray-web"; }
];
custom_formats = [
{
trash_ids = [
"570bc9ebecd92723d2d21500f4be314c" # Remaster
"eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
"e0c07d59beb37348e975a930d5e50319" # Criterion Collection
"9d27d9d2181838f76dee150882bdc58c" # Masters of Cinema
"db9b4c4b53d312a3ca5f1378f6440fc9" # Vinegar Syndrome
"957d0f44b592285f26449575e8b1167e" # Special Edition
"eecf3a857724171f968a66cb5719e152" # IMAX
"9f6cbff8cfe4ebbc1bde14c7b7bec0de" # IMAX Enhanced
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
{
trash_ids = [
"b6832f586342ef70d9c128d40c07b872" # Bad Dual Groups
"cc444569854e9de0b084ab2b8b1532b2" # Black and White Editions
"ae9b7c9ebde1f3bd336a8cbd1ec4c5e5" # No-RlsGroup
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
{
trash_ids = [
"dc98083864ea246d05a42df0d05f81cc" # x265 (HD)
];
assign_scores_to = [
{
name = "UHD Bluray + WEB";
score = 0;
}
];
}
{
trash_ids = [
"839bea857ed2c0a8e084f3cbdbd65ecb" # x265 (no HDR/DV)
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
{
trash_ids = [
"923b6abef9b17f937fab56cfcf89e1f1" # DV (WEBDL)
"b17886cb4158d9fea189859409975758" # HDR10Plus Boost
"55a5b50cb416dea5a50c4955896217ab" # DV HDR10+ Boost
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
{
trash_ids = [
"9c38ebb7384dada637be8899efa68e6f" # SDR
];
assign_scores_to = [ { name = "UHD Bluray + WEB"; } ];
}
];
media_naming = {
folder = "default";
movie = {
rename = true;
standard = "default";
};
};
};
};
jellyseerrConfig = mkJellyseerrConfig {
inherit
name
hostName
port
urlBase
mediaFolderBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
id = 1;
recyclarrProfile = "UHD Bluray + WEB";
is4k = true;
isAnime = false;
};
}

View File

@@ -0,0 +1,84 @@
{ hmConfig }:
let
inherit (import ./common.nix) mkProwlarrConfig mkRecyclarrConfig mkJellyseerrConfig;
in
rec {
name = "Radarr";
hostName = "radarr";
urlBase = "/manage/films";
mediaFolderBase = "/films";
port = 7878;
prowlarrConfig = mkProwlarrConfig {
inherit
name
hostName
port
urlBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
};
recyclarrConfig = mkRecyclarrConfig {
inherit hostName port urlBase;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
extraConfig = {
include = [
{ template = "radarr-quality-definition-movie"; }
{ template = "radarr-quality-profile-hd-bluray-web"; }
{ template = "radarr-custom-formats-hd-bluray-web"; }
];
custom_formats = [
{
trash_ids = [
"570bc9ebecd92723d2d21500f4be314c" # Remaster
"eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
"e0c07d59beb37348e975a930d5e50319" # Criterion Collection
"9d27d9d2181838f76dee150882bdc58c" # Masters of Cinema
"db9b4c4b53d312a3ca5f1378f6440fc9" # Vinegar Syndrome
"957d0f44b592285f26449575e8b1167e" # Special Edition
"eecf3a857724171f968a66cb5719e152" # IMAX
"9f6cbff8cfe4ebbc1bde14c7b7bec0de" # IMAX Enhanced
];
assign_scores_to = [ { name = "HD Bluray + WEB"; } ];
}
{
trash_ids = [
"b6832f586342ef70d9c128d40c07b872" # Bad Dual Groups
"cc444569854e9de0b084ab2b8b1532b2" # Black and White Editions
"ae9b7c9ebde1f3bd336a8cbd1ec4c5e5" # No-RlsGroup
];
assign_scores_to = [ { name = "HD Bluray + WEB"; } ];
}
];
media_naming = {
folder = "default";
movie = {
rename = true;
standard = "default";
};
};
};
};
jellyseerrConfig = mkJellyseerrConfig {
inherit
name
hostName
port
urlBase
mediaFolderBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
id = 0;
recyclarrProfile = "HD Bluray + WEB";
is4k = false;
isAnime = false;
};
}

View File

@@ -0,0 +1,96 @@
{
user,
home,
radarrs,
}:
{
config,
inputs,
pkgs,
system,
...
}:
let
selfPkgs = inputs.self.packages.${system};
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers volumes networks;
in
{
home-manager.users.${user} = {
sops = {
secrets = builtins.listToAttrs (
builtins.map (radarr: {
name = "${radarr.hostName}/apiKey";
value.sopsFile = ../../../../../../../secrets/secrets.yaml;
}) radarrs
);
templates = builtins.listToAttrs (
builtins.map (radarr: {
name = "${radarr.hostName}-env";
value.content = ''
API_KEY=${hmConfig.sops.placeholder."${radarr.hostName}/apiKey"}
'';
}) radarrs
);
};
virtualisation.quadlet = {
networks.media = { };
volumes = builtins.listToAttrs (
builtins.map (radarr: {
name = radarr.hostName;
value = { };
}) radarrs
);
containers = builtins.listToAttrs (
builtins.map (radarr: {
name = radarr.hostName;
value = {
containerConfig = {
image = "docker-archive:${selfPkgs.docker-radarr}";
networks = [
networks.media.ref
networks.transmission.ref
networks.traefik.ref
];
volumes =
let
postStart = pkgs.writeTextFile {
name = "post-start.sh";
executable = true;
text = builtins.readFile ./post-start.sh;
};
in
[
"${postStart}:/etc/radarr/post-start.sh:ro"
"${volumes.${radarr.hostName}.ref}:/var/lib/radarr"
"/mnt/storage/private/storm/containers/storage/volumes/transmission-data/_data:/var/lib/transmission"
"/mnt/storage/private/storm/containers/storage/volumes/media/_data:/var/lib/media"
];
environments = {
INSTANCE_NAME = radarr.name;
URL_BASE = radarr.urlBase;
ROOT_FOLDER = "/var/lib/media${radarr.mediaFolderBase}";
DOWNLOAD_CATEGORY = radarr.hostName;
};
environmentFiles = [ hmConfig.sops.templates."${radarr.hostName}-env".path ];
labels = [
"traefik.enable=true"
"traefik.http.routers.${radarr.hostName}.rule=Host(`media.karaolidis.com`) && PathPrefix(`${radarr.urlBase}`)"
"traefik.http.routers.${radarr.hostName}.middlewares=authelia@docker"
];
};
unitConfig.After = [
"${containers.transmission._serviceName}.service"
"sops-nix.service"
];
};
}) radarrs
);
};
};
}

View File

@@ -0,0 +1,171 @@
# shellcheck shell=sh
HOST="http://localhost:7878$URL_BASE"
API_SUBPATH="/api/v3"
DOWNLOAD_CATEGORY="${DOWNLOAD_CATEGORY:-radarr}"
try_forever() {
until "$@" 2>&1; do
echo "Try failed: $* - retrying in 1s"
sleep 1
done
}
wait_for_api() {
try_forever curl -sf -H "X-Api-Key: $API_KEY" "$HOST$API_SUBPATH/health"
echo "API is up!"
}
call() {
method="$1"
path="$2"
data="${3:-}"
if [ -n "$data" ]; then
curl -sf \
-X "$method" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
--data-raw "$data" \
"$HOST$API_SUBPATH/$path"
else
curl -sf \
-X "$method" \
-H "X-Api-Key: $API_KEY" \
"$HOST$API_SUBPATH/$path"
fi
}
get_resources() {
call GET "$1"
}
get_resource_id() {
endpoint="$1"
ident_field="$2"
name="$3"
get_resources "$endpoint" | jq -r --arg field "$ident_field" --arg name "$name" '.[] | select(.[$field] == $name) | .id // empty'
}
insert_or_skip_resource() {
endpoint="$1"
ident_field="$2"
name="$3"
json="$4"
all="$(get_resources "$endpoint")"
id="$(printf '%s' "$all" | jq -r --arg field "$ident_field" --arg name "$name" '.[] | select(.[$field]==$name) | .id')"
if [ -n "$id" ] && [ "$id" != "null" ]; then
echo "Skipping existing $endpoint '$name' (id=$id)"
else
echo "Creating $endpoint '$name'"
call POST "$endpoint?forceSave=true" "$json"
fi
}
upsert_resource() {
endpoint="$1"
ident_field="$2"
name="$3"
json="$4"
all="$(get_resources "$endpoint")"
id="$(printf '%s' "$all" | jq -r --arg field "$ident_field" --arg name "$name" '.[] | select(.[$field]==$name) | .id')"
if [ -n "$id" ] && [ "$id" != "null" ]; then
echo "Updating $endpoint '$name' (id=$id)"
call PUT "$endpoint/$id?forceSave=true" "$json"
else
echo "Creating $endpoint '$name'"
call POST "$endpoint?forceSave=true" "$json"
fi
}
prune_resources() {
endpoint="$1"
ident_field="$2"
shift 2
keep_list="$(printf '%s\n' "$@" | jq -R . | jq -s .)"
resources="$(get_resources "$endpoint")"
printf '%s' "$resources" | jq -c '.[]' | while IFS= read -r client; do
name="$(printf '%s' "$client" | jq -r --arg field "$ident_field" '.[$field]')"
id="$(printf '%s' "$client" | jq -r '.id')"
found="$(printf '%s' "$keep_list" | jq -r --arg name "$name" 'index($name)')"
if [ "$found" = "null" ]; then
echo "Deleting extra $endpoint '$name' (id=$id)"
call DELETE "$endpoint/$id" || echo "failed to delete $name, continuing"
fi
done
}
delete_resource() {
endpoint="$1"
ident_field="$2"
name="$3"
id="$(get_resource_id "$endpoint" "$ident_field" "$name")"
if [ -n "$id" ]; then
echo "Deleting $endpoint '$name' (id=$id)"
call DELETE "$endpoint/$id" || echo "Failed to delete $name, continuing"
else
echo "No $endpoint resource named '$name' found - skipping"
fi
}
build_transmission_payload() {
cat <<-EOF
{
"enable": true,
"protocol": "torrent",
"priority": 1,
"name": "Transmission",
"fields": [
{ "name": "host", "value": "transmission" },
{ "name": "port", "value": 9091 },
{ "name": "urlBase", "value": "" },
{ "name": "movieCategory", "value": "$DOWNLOAD_CATEGORY" }
],
"implementation": "Transmission",
"configContract": "TransmissionSettings"
}
EOF
}
build_rootfolder_payload() {
cat <<-EOF
{
"path": "$ROOT_FOLDER"
}
EOF
}
wait_for_api
if [ -n "$ROOT_FOLDER" ]; then
insert_or_skip_resource "rootfolder" "path" "$ROOT_FOLDER" "$(build_rootfolder_payload)"
prune_resources "rootfolder" "path" "$ROOT_FOLDER"
fi
try_forever upsert_resource "downloadclient" "name" "Transmission" "$(build_transmission_payload)"
prune_resources "downloadclient" "name" "Transmission"
delete_resource "qualityprofile" "name" "SD"
delete_resource "qualityprofile" "name" "HD-720p"
delete_resource "qualityprofile" "name" "HD-1080p"
delete_resource "qualityprofile" "name" "HD - 720p/1080p"
delete_resource "qualityprofile" "name" "Ultra-HD"
try_forever sh -c "[ $(get_resources qualityprofile | jq length) -eq 2 ]"
get_resources qualityprofile | jq -c '.[]' | while IFS= read -r profile; do
id="$(printf '%s' "$profile" | jq -r '.id')"
patched="$(printf '%s' "$profile" | jq '.language.id = -2 | .language.Name = "Original"')"
echo "Patching qualityprofile id=$id with language = Original"
call PUT "qualityprofile/$id?forceSave=true" "$patched"
done

View File

@@ -0,0 +1,53 @@
{
user,
home,
radarrs,
sonarrs,
}:
{
config,
inputs,
pkgs,
system,
...
}:
let
selfPkgs = inputs.self.packages.${system};
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers networks;
arrs = radarrs ++ sonarrs;
in
{
home-manager.users.${user} = {
sops.templates = builtins.listToAttrs (
builtins.map (arr: {
name = "recyclarr-${arr.hostName}";
value.content = builtins.readFile (
(pkgs.formats.yaml { }).generate "${arr.hostName}.yaml" arr.recyclarrConfig
);
}) arrs
);
virtualisation.quadlet.containers = {
# FIXME: https://recyclarr.dev/wiki/behavior/quality-profiles/#language
recyclarr = {
containerConfig = {
image = "docker-archive:${selfPkgs.docker-recyclarr}";
networks = [ networks.media.ref ];
volumes = builtins.map (
arr:
"${
hmConfig.sops.templates."recyclarr-${arr.hostName}".path
}:/var/lib/recyclarr/configs/${arr.hostName}.yaml:ro"
) arrs;
};
unitConfig.After =
let
arrServices = builtins.map (arr: "${containers.${arr.hostName}._serviceName}.service") arrs;
in
[ "sops-nix.service" ] ++ arrServices;
};
};
};
}

View File

@@ -0,0 +1,90 @@
rec {
mkUrl =
{
hostName,
port,
urlBase,
}:
"http://${hostName}:${builtins.toString port}${urlBase}/";
mkProwlarrConfig =
{
name,
hostName,
port,
urlBase,
apiKey,
}:
{
inherit name;
enable = true;
implementation = "Sonarr";
configContract = "SonarrSettings";
syncLevel = "fullSync";
fields = [
{
name = "prowlarrUrl";
value = "http://prowlarr:9696";
}
{
name = "baseUrl";
value = mkUrl { inherit hostName port urlBase; };
}
{
name = "apiKey";
value = apiKey;
}
];
};
mkRecyclarrConfig =
{
hostName,
port,
urlBase,
apiKey,
extraConfig,
}:
{
sonarr.${hostName} = {
base_url = mkUrl { inherit hostName port urlBase; };
api_key = apiKey;
delete_old_custom_formats = true;
replace_existing_custom_formats = true;
} // extraConfig;
};
mkJellyseerrConfig =
{
name,
hostName,
port,
urlBase,
mediaFolderBase,
apiKey,
id,
recyclarrProfile,
is4k,
isAnime,
}:
{
inherit
name
port
apiKey
id
is4k
;
hostname = hostName;
baseUrl = urlBase;
activeProfileName = recyclarrProfile;
activeDirectory = "/var/lib/media${mediaFolderBase}";
activeAnimeProfileName = recyclarrProfile;
activeAnimeDirectory = "/var/lib/media${mediaFolderBase}";
isDefault = !isAnime;
enableSeasonFolders = true;
externalUrl = "https://media.karaolidis.com${urlBase}";
syncEnabled = true;
};
}

View File

@@ -0,0 +1,99 @@
{ hmConfig }:
let
inherit (import ./common.nix) mkProwlarrConfig mkRecyclarrConfig mkJellyseerrConfig;
in
rec {
name = "Sonarr (Anime)";
hostName = "sonarr-anime";
urlBase = "/manage/anime/shows";
mediaFolderBase = "/anime/shows";
port = 8989;
prowlarrConfig = mkProwlarrConfig {
inherit
name
hostName
port
urlBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
};
recyclarrConfig = mkRecyclarrConfig {
inherit hostName port urlBase;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
extraConfig = {
include = [
{ template = "sonarr-quality-definition-anime"; }
{ template = "sonarr-v4-quality-profile-anime"; }
{ template = "sonarr-v4-custom-formats-anime"; }
];
custom_formats = [
{
trash_ids = [
"026d5aadd1a6b4e550b134cb6c72b3ca" # Uncensored
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
{
trash_ids = [
"b2550eb333d27b75833e25b8c2557b38" # 10bit
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
{
trash_ids = [
"418f50b10f1907201b6cfdf881f467b7" # Anime Dual Audio
];
assign_scores_to = [
{
name = "Remux-1080p - Anime";
score = 10;
}
];
}
];
media_naming = {
series = "default";
season = "default";
episodes = {
rename = true;
standard = "default";
daily = "default";
anime = "default";
};
};
};
};
jellyseerrConfig = mkJellyseerrConfig {
inherit
name
hostName
port
urlBase
mediaFolderBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
id = 2;
recyclarrProfile = "Remux-1080p - Anime";
is4k = false;
isAnime = true;
};
}

View File

@@ -0,0 +1,104 @@
{ hmConfig }:
let
inherit (import ./common.nix) mkProwlarrConfig mkRecyclarrConfig mkJellyseerrConfig;
in
rec {
name = "Sonarr (UHD)";
hostName = "sonarr-uhd";
urlBase = "/manage/shows/uhd";
mediaFolderBase = "/shows";
port = 8989;
prowlarrConfig = mkProwlarrConfig {
inherit
name
hostName
port
urlBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
};
recyclarrConfig = mkRecyclarrConfig {
inherit hostName port urlBase;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
extraConfig = {
include = [
{ template = "sonarr-quality-definition-series"; }
{ template = "sonarr-v4-quality-profile-web-2160p"; }
{ template = "sonarr-v4-custom-formats-web-2160p"; }
];
custom_formats = [
{
trash_ids = [
"9b27ab6498ec0f31a3353992e19434ca" # DV (WEBDL)
"0dad0a507451acddd754fe6dc3a7f5e7" # HDR10+ Boost
"385e9e8581d33133c3961bdcdeffb7b4" # DV HDR10+ Boost
];
assign_scores_to = [ { name = "WEB-2160p"; } ];
}
{
trash_ids = [
"32b367365729d530ca1c124a0b180c64" # Bad Dual Groups
"82d40da2bc6923f41e14394075dd4b03" # No-RlsGroup
];
assign_scores_to = [ { name = "WEB-2160p"; } ];
}
{
trash_ids = [
"47435ece6b99a0b477caf360e79ba0bb" # x265 (HD)
];
assign_scores_to = [
{
name = "WEB-2160p";
score = 0;
}
];
}
{
trash_ids = [
"9b64dff695c2115facf1b6ea59c9bd07" # x265 (no HDR/DV)
];
assign_scores_to = [ { name = "WEB-2160p"; } ];
}
{
trash_ids = [
"2016d1676f5ee13a5b7257ff86ac9a93" # SDR
];
assign_scores_to = [ { name = "WEB-2160p"; } ];
}
];
media_naming = {
series = "default";
season = "default";
episodes = {
rename = true;
standard = "default";
daily = "default";
anime = "default";
};
};
};
};
jellyseerrConfig = mkJellyseerrConfig {
inherit
name
hostName
port
urlBase
mediaFolderBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
id = 1;
recyclarrProfile = "WEB-2160p";
is4k = true;
isAnime = false;
};
}

View File

@@ -0,0 +1,90 @@
{ hmConfig }:
let
inherit (import ./common.nix) mkProwlarrConfig mkRecyclarrConfig mkJellyseerrConfig;
in
rec {
name = "Sonarr";
hostName = "sonarr";
urlBase = "/manage/shows";
mediaFolderBase = "/shows";
port = 8989;
prowlarrConfig = mkProwlarrConfig {
inherit
name
hostName
port
urlBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
};
recyclarrConfig = mkRecyclarrConfig {
inherit hostName port urlBase;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
extraConfig = {
include = [
{ template = "sonarr-quality-definition-series"; }
{ template = "sonarr-v4-quality-profile-web-1080p"; }
{ template = "sonarr-v4-custom-formats-web-1080p"; }
];
custom_formats = [
{
trash_ids = [
"32b367365729d530ca1c124a0b180c64" # Bad Dual Groups
"82d40da2bc6923f41e14394075dd4b03" # No-RlsGroup
];
assign_scores_to = [ { name = "WEB-1080p"; } ];
}
{
trash_ids = [
"47435ece6b99a0b477caf360e79ba0bb" # x265 (HD)
];
assign_scores_to = [
{
name = "WEB-1080p";
score = 0;
}
];
}
{
trash_ids = [
"9b64dff695c2115facf1b6ea59c9bd07" # x265 (no HDR/DV)
];
assign_scores_to = [ { name = "WEB-1080p"; } ];
}
];
media_naming = {
series = "default";
season = "default";
episodes = {
rename = true;
standard = "default";
daily = "default";
anime = "default";
};
};
};
};
jellyseerrConfig = mkJellyseerrConfig {
inherit
name
hostName
port
urlBase
mediaFolderBase
;
apiKey = hmConfig.sops.placeholder."${hostName}/apiKey";
id = 0;
recyclarrProfile = "WEB-1080p";
is4k = false;
isAnime = false;
};
}

View File

@@ -0,0 +1,96 @@
{
user,
home,
sonarrs,
}:
{
config,
inputs,
pkgs,
system,
...
}:
let
selfPkgs = inputs.self.packages.${system};
hmConfig = config.home-manager.users.${user};
inherit (hmConfig.virtualisation.quadlet) containers volumes networks;
in
{
home-manager.users.${user} = {
sops = {
secrets = builtins.listToAttrs (
builtins.map (sonarr: {
name = "${sonarr.hostName}/apiKey";
value.sopsFile = ../../../../../../../secrets/secrets.yaml;
}) sonarrs
);
templates = builtins.listToAttrs (
builtins.map (sonarr: {
name = "${sonarr.hostName}-env";
value.content = ''
API_KEY=${hmConfig.sops.placeholder."${sonarr.hostName}/apiKey"}
'';
}) sonarrs
);
};
virtualisation.quadlet = {
networks.media = { };
volumes = builtins.listToAttrs (
builtins.map (sonarr: {
name = sonarr.hostName;
value = { };
}) sonarrs
);
containers = builtins.listToAttrs (
builtins.map (sonarr: {
name = sonarr.hostName;
value = {
containerConfig = {
image = "docker-archive:${selfPkgs.docker-sonarr}";
networks = [
networks.media.ref
networks.transmission.ref
networks.traefik.ref
];
volumes =
let
postStart = pkgs.writeTextFile {
name = "post-start.sh";
executable = true;
text = builtins.readFile ./post-start.sh;
};
in
[
"${postStart}:/etc/sonarr/post-start.sh:ro"
"${volumes.${sonarr.hostName}.ref}:/var/lib/sonarr"
"/mnt/storage/private/storm/containers/storage/volumes/transmission-data/_data:/var/lib/transmission"
"/mnt/storage/private/storm/containers/storage/volumes/media/_data:/var/lib/media"
];
environments = {
INSTANCE_NAME = sonarr.name;
URL_BASE = sonarr.urlBase;
ROOT_FOLDER = "/var/lib/media${sonarr.mediaFolderBase}";
DOWNLOAD_CATEGORY = sonarr.hostName;
};
environmentFiles = [ hmConfig.sops.templates."${sonarr.hostName}-env".path ];
labels = [
"traefik.enable=true"
"traefik.http.routers.${sonarr.hostName}.rule=Host(`media.karaolidis.com`) && PathPrefix(`${sonarr.urlBase}`)"
"traefik.http.routers.${sonarr.hostName}.middlewares=authelia@docker"
];
};
unitConfig.After = [
"${containers.transmission._serviceName}.service"
"sops-nix.service"
];
};
}) sonarrs
);
};
};
}

View File

@@ -1,5 +1,9 @@
# shellcheck shell=sh
HOST="http://localhost:8989$URL_BASE"
API_SUBPATH="/api/v3"
DOWNLOAD_CATEGORY="${DOWNLOAD_CATEGORY:-sonarr}"
try_forever() {
until "$@" 2>&1; do
echo "Try failed: $* - retrying in 1s"
@@ -7,30 +11,6 @@ try_forever() {
done
}
try() {
attempts="$1"
shift
delay=1
count=1
while [ "$count" -le "$attempts" ]; do
if "$@" 2>&1; then
return 0
fi
if [ "$count" -lt "$attempts" ]; then
echo "Attempt $count/$attempts failed, retrying in ${delay}s..."
sleep "$delay"
delay="$(( delay * 2 ))"
fi
count="$(( count + 1 ))"
done
echo "All $attempts attempts failed for: $*"
}
wait_for_api() {
try_forever curl -sf -H "X-Api-Key: $API_KEY" "$HOST$API_SUBPATH/health"
echo "API is up!"
@@ -138,3 +118,46 @@ delete_resource() {
echo "No $endpoint resource named '$name' found - skipping"
fi
}
build_transmission_payload() {
cat <<-EOF
{
"enable": true,
"protocol": "torrent",
"priority": 1,
"name": "Transmission",
"fields": [
{ "name": "host", "value": "transmission" },
{ "name": "port", "value": 9091 },
{ "name": "urlBase", "value": "" },
{ "name": "tvCategory", "value": "$DOWNLOAD_CATEGORY" }
],
"implementation": "Transmission",
"configContract": "TransmissionSettings"
}
EOF
}
build_rootfolder_payload() {
cat <<-EOF
{
"path": "$ROOT_FOLDER"
}
EOF
}
wait_for_api
if [ -n "$ROOT_FOLDER" ]; then
insert_or_skip_resource "rootfolder" "path" "$ROOT_FOLDER" "$(build_rootfolder_payload)"
prune_resources "rootfolder" "path" "$ROOT_FOLDER"
fi
try_forever upsert_resource "downloadclient" "name" "Transmission" "$(build_transmission_payload)"
prune_resources "downloadclient" "name" "Transmission"
delete_resource "qualityprofile" "name" "SD"
delete_resource "qualityprofile" "name" "HD-720p"
delete_resource "qualityprofile" "name" "HD-1080p"
delete_resource "qualityprofile" "name" "HD - 720p/1080p"
delete_resource "qualityprofile" "name" "Ultra-HD"