Skip to content

Commit

Permalink
Merge pull request #24 from rlad78/sabnzbd
Browse files Browse the repository at this point in the history
WIP: SABnzbd module for usenet integration
  • Loading branch information
rasmus-kirk authored Jul 19, 2024
2 parents 37d728f + f9d626b commit f5a6859
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 0 deletions.
21 changes: 21 additions & 0 deletions nixarr/nixarr.nix
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ with lib; let
fi
chown -R torrenter:media "${cfg.mediaDir}/torrents"
chown -R usenet:media "${cfg.mediaDir}/usenet"
chown -R streamer:media "${cfg.mediaDir}/library"
find "${cfg.mediaDir}" \( -type d -exec chmod 0775 {} + -true \) -o \( -exec chmod 0664 {} + \)
'' + strings.optionalString cfg.jellyfin.enable ''
Expand All @@ -35,6 +36,9 @@ with lib; let
'' + strings.optionalString cfg.transmission.enable ''
chown -R torrenter:cross-seed "${cfg.transmission.stateDir}"
find "${cfg.transmission.stateDir}" \( -type d -exec chmod 0750 {} + -true \) -o \( -exec chmod 0640 {} + \)
'' + strings.optionalString cfg.sabnzbd.enable ''
chown -R usenet:root "${cfg.sabnzbd.stateDir}"
find "${cfg.sabnzbd.stateDir}" \( -type d -exec chmod 0700 {} + -true \) -o \( -exec chmod 0600 {} + \)
'' + strings.optionalString cfg.transmission.privateTrackers.cross-seed.enable ''
chown -R cross-seed:root "${cfg.transmission.privateTrackers.cross-seed.stateDir}"
find "${cfg.transmission.privateTrackers.cross-seed.stateDir}" \( -type d -exec chmod 0700 {} + -true \) -o \( -exec chmod 0600 {} + \)
Expand Down Expand Up @@ -70,6 +74,7 @@ in {
./openssh
./prowlarr
./transmission
./sabnzbd
../util
];

Expand Down Expand Up @@ -105,6 +110,7 @@ in {
- [Readarr](#nixarr.readarr.enable)
- [Sonarr](#nixarr.sonarr.enable)
- [Transmission](#nixarr.transmission.enable)
- [SABnzbd](#nixarr.sabnzbd.enable)
Remember to read the options.
'';
Expand Down Expand Up @@ -231,6 +237,7 @@ in {
media.members = cfg.mediaUsers;
streamer = {};
torrenter = {};
usenet = {};
};
users.users = {
streamer = {
Expand All @@ -241,6 +248,10 @@ in {
isSystemUser = true;
group = "torrenter";
};
usenet = {
isSystemUser = true;
group = "usenet";
};
};

systemd.tmpfiles.rules = [
Expand All @@ -259,6 +270,16 @@ in {
"d '${cfg.mediaDir}/torrents/radarr' 0755 torrenter media - -"
"d '${cfg.mediaDir}/torrents/sonarr' 0755 torrenter media - -"
"d '${cfg.mediaDir}/torrents/readarr' 0755 torrenter media - -"
] ++ lists.optionals cfg.sabnzbd.enable [
# only create usenet dirs if sabnzbd is enabled
"d '${cfg.mediaDir}/usenet' 0755 usenet media - -"
"d '${cfg.mediaDir}/usenet/.incomplete' 0755 usenet media - -"
"d '${cfg.mediaDir}/usenet/.watch' 0755 usenet media - -"
"d '${cfg.mediaDir}/usenet/manual' 0775 usenet media - -"
"d '${cfg.mediaDir}/usenet/liadarr' 0775 usenet media - -"
"d '${cfg.mediaDir}/usenet/radarr' 0775 usenet media - -"
"d '${cfg.mediaDir}/usenet/sonarr' 0775 usenet media - -"
"d '${cfg.mediaDir}/usenet/readarr' 0775 usenet media - -"
];

environment.systemPackages = with pkgs; [
Expand Down
85 changes: 85 additions & 0 deletions nixarr/sabnzbd/config.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
config,
pkgs,
lib,
...
}: let
cfg = config.nixarr.sabnzbd;
nixarr = config.nixarr;
ini-file-target = "${cfg.stateDir}/sabnzbd.ini";
concatStringsCommaIfExists = with lib.strings;
stringList: (
optionalString (builtins.length stringList > 0) (
concatStringsSep "," stringList
)
);

user-configs = {
misc = {
host =
if cfg.openFirewall
then "0.0.0.0"
else "127.0.0.1";
port = cfg.guiPort;
download_dir = "${nixarr.mediaDir}/usenet/.incomplete";
complete_dir = "${nixarr.mediaDir}/usenet/manual";
dirscan_dir = "${nixarr.mediaDir}/usenet/watch";
host_whitelist = concatStringsCommaIfExists cfg.whitelistHostnames;
local_ranges = concatStringsCommaIfExists cfg.whitelistRanges;
permissions = "775";
};
};

ini-base-config-file = pkgs.writeTextFile {
name = "base-config.ini";
text = lib.generators.toINI {} user-configs;
};

fix-config-permissions-script = pkgs.writeShellApplication {
name = "sabnzbd-fix-config-permissions";
runtimeInputs = with pkgs; [util-linux];
text = ''
if [ ! -f ${ini-file-target} ]; then
echo 'FAILURE: cannot change permissions of ${ini-file-target}, file does not exist'
exit 1
fi
chmod 600 ${ini-file-target}
chown usenet:media ${ini-file-target}
'';
};

user-configs-to-python-list = with lib;
attrsets.collect (f: !builtins.isAttrs f) (
attrsets.mapAttrsRecursive (
path: value:
"sab_config_map['"
+ (lib.strings.concatStringsSep "']['" path)
+ "'] = '"
+ (builtins.toString value)
+ "'"
)
user-configs
);
apply-user-configs-script = with lib; (pkgs.writers.writePython3Bin
"sabnzbd-set-user-values" {libraries = [pkgs.python3Packages.configobj];} ''
from pathlib import Path
from configobj import ConfigObj
sab_config_path = Path("${ini-file-target}")
if not sab_config_path.is_file() or sab_config_path.suffix != ".ini":
raise Exception(f"{sab_config_path} is not a valid config file path.")
sab_config_map = ConfigObj(str(sab_config_path))
${lib.strings.concatStringsSep "\n" user-configs-to-python-list}
sab_config_map.write()
'');
in {
systemd.tmpfiles.rules = ["C ${cfg.stateDir}/sabnzbd.ini - - - - ${ini-base-config-file}"];
systemd.services.sabnzbd.serviceConfig.ExecStartPre = lib.mkBefore [
("+" + fix-config-permissions-script + "/bin/sabnzbd-fix-config-permissions")
(apply-user-configs-script + "/bin/sabnzbd-set-user-values")
];
}
154 changes: 154 additions & 0 deletions nixarr/sabnzbd/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
{
config,
lib,
...
}:
with lib; let
cfg = config.nixarr.sabnzbd;
nixarr = config.nixarr;
in {
options.nixarr.sabnzbd = {
enable = mkEnableOption "Enable the SABnzbd service.";

stateDir = mkOption {
type = types.path;
default = "${nixarr.stateDir}/sabnzbd";
defaultText = literalExpression ''"''${nixarr.stateDir}/sabnzbd"'';
example = "/nixarr/.state/sabnzbd";
description = ''
The location of the state directory for the SABnzbd service.
**Warning:** Setting this to any path, where the subpath is not
owned by root, will fail! For example:
```nix
stateDir = /home/user/nixarr/.state/sabnzbd
```
Is not supported, because `/home/user` is owned by `user`.
'';
};

guiPort = mkOption {
type = types.port;
default = 8080;
example = 9999;
description = ''
The port that SABnzbd's GUI will listen on for incomming connections.
'';
};

openFirewall = mkOption {
type = types.bool;
defaultText = literalExpression ''!nixarr.SABnzbd.vpn.enable'';
default = !cfg.vpn.enable;
example = true;
description = "Open firewall for SABnzbd";
};

whitelistHostnames = mkOption {
type = types.listOf types.str;
default = [config.networking.hostName];
defaultText = "[ config.networking.hostName ]";
example = ''[ "mediaserv" "media.example.com" ]'';
description = ''
A list that specifies what URLs that are allowed to represent your
SABnzbd instance. If you see an error message like this when
trying to connect to SABnzbd from another device...
```
Refused connection with hostname "your.hostname.com"
```
...then you should add your hostname(s) to this list.
SABnzbd only allows connections matching these URLs in order to prevent
DNS hijacking. See <https://sabnzbd.org/wiki/extra/hostname-check.html>
for more info.
'';
};

whitelistRanges = mkOption {
type = types.listOf types.str;
default = [];
defaultText = "[ ]";
example = ''[ "192.168.1.0/24" "10.0.0.0/23" ]'';
description = ''
A list of IP ranges that will be allowed to connect to SABnzbd's
web GUI. This only needs to be set if SABnzbd needs to be accessed
from another machine besides its host.
'';
};

vpn.enable = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
**Required options:** [`nixarr.vpn.enable`](#nixarr.vpn.enable)
Route SABnzbd traffic through the VPN.
'';
};
};

imports = [./config.nix];

config = mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' 0700 usenet root - -"
];

services.sabnzbd = {
enable = true;
user = "usenet";
group = "media";
configFile = "${cfg.stateDir}/sabnzbd.ini";
};

networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.guiPort];

systemd.services.sabnzbd.serviceConfig = {
Restart = "on-failure";
StartLimitBurst = 5;
};

# Enable and specify VPN namespace to confine service in.
systemd.services.sabnzbd.vpnconfinement = mkIf cfg.vpn.enable {
enable = true;
vpnnamespace = "wg";
};

# Port mappings
vpnnamespaces.wg = mkIf cfg.vpn.enable {
portMappings = [
{
from = cfg.guiPort;
to = cfg.guiPort;
}
];
};

services.nginx = mkIf cfg.vpn.enable {
enable = true;

recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;

virtualHosts."127.0.0.1:${builtins.toString cfg.guiPort}" = {
listen = [
{
addr = "0.0.0.0";
port = cfg.guiPort;
}
];
locations."/" = {
recommendedProxySettings = true;
proxyWebsockets = true;
proxyPass = "http://192.168.15.1:${builtins.toString cfg.guiPort}";
};
};
};
};
}

0 comments on commit f5a6859

Please sign in to comment.