Compare commits

...

29 Commits

Author SHA1 Message Date
Alexander 4aff331d40 qbittorrent: set torrent dirs to 0775 for media group write access
Deploy / run-nix-build (push) Has been cancelled
Lint / fmt (push) Has been cancelled
Lint / build-website (push) Has been cancelled
Lint / test (push) Has been cancelled
Deploy / deploy-site (push) Has been cancelled
2026-04-27 22:17:41 +02:00
Alexander d2c890054a qbittorrent: use recursiveUpdate for extraSettings merge
Deploy / run-nix-build (push) Has been cancelled
Deploy / deploy-site (push) Has been cancelled
Lint / fmt (push) Has been cancelled
Lint / build-website (push) Has been cancelled
Lint / test (push) Has been cancelled
2026-04-27 19:01:37 +02:00
Alexander b99f33bf02 qbittorrent: set UMask 0002 to match transmission
Deploy / run-nix-build (push) Has been cancelled
Deploy / deploy-site (push) Has been cancelled
Lint / fmt (push) Has been cancelled
Lint / build-website (push) Has been cancelled
Lint / test (push) Has been cancelled
2026-04-27 18:23:48 +02:00
Alexander 05ff361f3c Formatted qbittorrent nix
Deploy / run-nix-build (push) Has been cancelled
Deploy / deploy-site (push) Has been cancelled
Lint / fmt (push) Has been cancelled
Lint / build-website (push) Has been cancelled
Lint / test (push) Has been cancelled
2026-04-26 15:57:41 +02:00
Alexander 8b4a4c67b2 Add qBittorrent
Deploy / run-nix-build (push) Has been cancelled
Deploy / deploy-site (push) Has been cancelled
Lint / fmt (push) Has been cancelled
Lint / build-website (push) Has been cancelled
Lint / test (push) Has been cancelled
2026-04-26 15:51:43 +02:00
Alexandra Østermark 7cc521933d Merge pull request #142 from vicgeentor/fix-package-option
Deploy / run-nix-build (push) Has been cancelled
Deploy / deploy-site (push) Has been cancelled
Lint / fmt (push) Has been cancelled
Lint / build-website (push) Has been cancelled
Lint / test (push) Has been cancelled
fix: use cfg.package instead of pkgs.bazarr in the bazarr module
2026-02-08 10:14:26 +01:00
vicgeentor c95a949b12 fix: use cfg.package instead of pkgs.bazarr in the bazarr module 2026-02-07 21:50:24 +01:00
Alexandra Østermark 96973b6a8c Merge pull request #140 from nix-media-server/org-changes
Org changes
2026-02-02 15:21:43 +01:00
rasmus-kirk ec44dbe4ff Org changes 2026-02-02 07:06:30 +01:00
Rasmus Kirk 204da9209a Merge pull request #115 from cramt/cramt/flake_update
flake update
2025-12-14 16:56:44 +00:00
Alexandra Østermark d6838844ca fixed 2025-12-14 16:46:46 +01:00
Alexandra Østermark b4b30f6c78 flake update 2025-12-14 16:03:20 +01:00
rasmus-kirk ac11a6d765 Added check for building website 2025-12-02 10:57:55 +01:00
Rasmus Kirk 3ea7ad8a1c Merge pull request #107 from cramt/fix_transmission_failing_in_ci
fix transmission failing in ci
2025-11-25 10:20:22 +00:00
Alexandra Østermark be6d4f50cd fix transmission failing in ci 2025-11-24 14:34:11 +01:00
Rasmus Kirk 66d0dcb3c3 Merge pull request #98 from pierzchalski/eap/rationalise-readarr
`readarr{,-audiobook}`: use `service.${service}` config pattern
2025-11-23 08:10:04 +00:00
Rasmus Kirk 86f6e7b617 Merge pull request #105 from sunaurus/fix/recyclarr-accept-strings
fix: allow strings in recyclarr yaml type
2025-11-23 08:05:13 +00:00
Edward Pierzchalski 4d1d1cfe76 lib, readarr-audiobook: copy *arr helpers from nixpkgs instead of downloading them 2025-11-22 10:00:09 +11:00
sunaurus c2af6384bd fix: recyclarr only match tags on strings 2025-11-21 18:18:40 +02:00
Sander Saarend 32d4c7002c fix: allow strings in recyclarr yaml type 2025-11-21 12:39:30 +02:00
rasmus-kirk 45f8838f3c Merge branch 'main' of github.com:rasmus-kirk/nixarr 2025-11-15 15:32:42 +01:00
rasmus-kirk eab22b92fa minor revision 2025-11-15 15:30:32 +01:00
Edward Pierzchalski d8ca911a13 [readarr-audiobook] fix one call of mkServarr utility functions, document which calls need readarr vs readarr-audiobook and why 2025-11-14 10:48:44 +11:00
Rasmus Kirk fd055b3af0 Merge pull request #100 from cramt/preserve_env_var_tags_for_recyclarr_configuration
preserve env var yaml tags for recyclarr config
2025-11-13 14:52:56 +00:00
Alexandra Østermark 74307df8e3 comments descriping the unreadable yaml generation 2025-11-13 11:30:23 +01:00
Alexandra Østermark 9a4dc02b20 fix minor mistake 2025-11-12 20:10:17 +01:00
Alexandra Østermark 709ab63d5a preserve env var yaml tags for recyclarr config 2025-11-12 19:55:42 +01:00
Edward Pierzchalski 4b2316b18a readarr-audiobook: add service.readarr-audiobook to mirror other *arr services
This lets us use `services.readarr-audiobook` to configure it the same
way we can use `services.{readarr,radarr,sonarr,prowlarr}`.
2025-11-11 10:12:22 +11:00
Edward Pierzchalski a5007d9d37 readarr: use the NixOS module as much as possible
See also this PR doing something similar for `prowlarr`: https://github.com/rasmus-kirk/nixarr/pull/95
2025-11-11 10:12:22 +11:00
20 changed files with 637 additions and 146 deletions
+12 -6
View File
@@ -13,21 +13,27 @@ jobs:
fmt:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: actions/checkout@v5
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
uses: DeterminateSystems/nix-installer-action@v3
- run: nix fmt -- --check .
build-website:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v3
- run: nix build
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
-1
View File
@@ -5,7 +5,6 @@
Added:
- `whisparr` service
- `komgarr` service
- `stash` service
Fixed:
- Cross-seed now uses `transmission` user.
+3 -2
View File
@@ -12,7 +12,8 @@ that I will remove or change options in a non-backwards-compatible way.
## Features
- **Run services through a VPN:** You can run any service that this module
supports through a VPN, fx `nixarr.transmission.vpn.enable = true;`
supports through a VPN, fx `nixarr.transmission.vpn.enable = true;` or
`nixarr.qbittorrent.vpn.enable = true;`
- **Automatic Directories, Users and Permissions:** The module automatically
creates directories and users for your media library. It also sets sane
permissions.
@@ -66,7 +67,7 @@ like shown in this example flake:
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nixarr.url = "github:rasmus-kirk/nixarr";
nixarr.url = "github:nix-media-server/nixarr";
};
outputs = {
+11 -1
View File
@@ -5,7 +5,7 @@ title: Basic Example
This example does the following:
- Runs a jellyfin server and exposes it to the internet with HTTPS support.
- Runs the transmission torrent client through a vpn
- Runs a torrent client (Transmission or qBittorrent) through a VPN
- Runs all "*Arrs" supported by this module
```nix {.numberLines}
@@ -52,3 +52,13 @@ This example does the following:
jellyseerr.enable = true;
};
```
To use qBittorrent instead of Transmission, replace the `transmission` block with:
```nix {.numberLines}
qbittorrent = {
enable = true;
vpn.enable = true;
peerPort = 50000; # Set this to the port forwarded by your VPN
};
```
+38 -7
View File
@@ -31,13 +31,42 @@ option, which by default is set to `/data/media`.
## Transmission
Transmission should already be setup and running since it's configured
with JSON, and can therefore be configured with nix. The most basic settings are already set. See the following links for more info:
Transmission should already be setup and running since it's configured with
JSON, and can therefore be configured with nix. The most basic settings are
already set. See the following links for more info:
- [The configured Nixarr defaults for transmission](https://github.com/rasmus-kirk/nixarr/blob/28d1be070deb1a064c1967889c11c8921752fa09/nixarr/transmission/default.nix#L355)
- [The configured Nixarr defaults for transmission](https://github.com/nix-media-server/nixarr/blob/28d1be070deb1a064c1967889c11c8921752fa09/nixarr/transmission/default.nix#L355)
- [The `nixarr.transmission` options](https://nixarr.com/nixos-options/#nixarr.transmission.enable)
- [Settings that can be passed through `nixarr.transmission.settings`]
## qBittorrent
- Open your browser and go to `{URL}:8080`.
- On first launch, qBittorrent generates a temporary admin password shown in the
systemd journal. Retrieve it with:
```
sudo journalctl -u qbittorrent -b | grep password
```
- Log in with username `admin` and the temporary password.
- Go to "Tools" > "Options" > "Web UI":
- Change the default password to something secure.
**Configuring download categories for the \*Arrs:**
Each \*Arr service needs its own category in qBittorrent so downloads are
sorted into the correct directories:
1. Right-click in the category panel on the left sidebar.
2. Click "Add category".
3. Add the following categories with their save paths:
- `radarr` → `/data/media/torrents/radarr`
- `sonarr` → `/data/media/torrents/sonarr`
- `lidarr` → `/data/media/torrents/lidarr`
- `readarr` → `/data/media/torrents/readarr`
When adding qBittorrent as a download client in each \*Arr, set the category
to the corresponding name.
## Radarr
- Open your browser and go to `{URL}:7878`.
@@ -50,8 +79,9 @@ with JSON, and can therefore be configured with nix. The most basic settings are
- Under `Permissions`, change `chmod Folder` to `775`
- Under `Root Folders`, click `Add Root Folder`. Add
`/data/media/library/movies/`, then click `Save Changes`.
- Go to "Settings" > "Download Clients" and add Transmission. Change the
category to `radarr`.
- Go to "Settings" > "Download Clients" and add your torrent client:
- **Transmission**: select Transmission, set the category to `radarr`.
- **qBittorrent**: select qBittorrent, set the category to `radarr`.
**Recommendations:**:
@@ -70,8 +100,9 @@ with JSON, and can therefore be configured with nix. The most basic settings are
- Under `Permissions`, change `chmod Folder` to `775`
- Under `Root Folders`, click `Add Root Folder`. Add
`/data/media/library/shows/`, then click `Save Changes`.
- Go to "Settings" > "Download Clients" and add Transmission. Change the
category to `sonarr`.
- Go to "Settings" > "Download Clients" and add your torrent client:
- **Transmission**: select Transmission, set the category to `sonarr`.
- **qBittorrent**: select qBittorrent, set the category to `sonarr`.
**Recommendations:**:
-3
View File
@@ -29,9 +29,6 @@ Then you can set that port for a service, for example
## Debugging Ports
> **Note:** See [this GH issue](https://github.com/rasmus-kirk/nixarr/issues/27)
> first, since it's a common problem
You can debug an open port using the
[nixarr.vpn.vpnTestService](https://nixarr.com/options.html#nixarr.vpn.vpntestservice.enable):
Generated
+7 -7
View File
@@ -2,16 +2,16 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1761016216,
"narHash": "sha256-G/iC4t/9j/52i/nm+0/4ybBmAF4hzR8CNHC75qEhjHo=",
"lastModified": 1765608474,
"narHash": "sha256-9Wx53UK0z8Di5iesJID0tS1dRKwGxI4i7tsSanOHhF0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "481cf557888e05d3128a76f14c76397b7d7cc869",
"rev": "28bb483c11a1214a73f9fd2d9928a6e2ea86ec71",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.05",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -25,11 +25,11 @@
},
"vpnconfinement": {
"locked": {
"lastModified": 1759956062,
"narHash": "sha256-NUZu0Rb0fwUjfdp51zMm0xM3lcK8Kw4c97LLog7+JjA=",
"lastModified": 1765634578,
"narHash": "sha256-Fujb9sn1cj+u/bzfo2RbQkcAvJ7Ch1pimJzFie4ptb4=",
"owner": "Maroka-chan",
"repo": "VPN-Confinement",
"rev": "fabe7247b720b5eb4c3c053e24a2b3b70e64c52b",
"rev": "f2989e1e3cb06c7185939e9ddc368f88b998616a",
"type": "github"
},
"original": {
+91 -80
View File
@@ -2,7 +2,7 @@
description = "The Nixarr Media Server Nixos Module";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
vpnconfinement.url = "github:Maroka-chan/VPN-Confinement";
@@ -27,91 +27,102 @@
# Helper to provide system-specific attributes
forAllSystems = f:
nixpkgs.lib.genAttrs supportedSystems (system:
f {
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
});
nixpkgs.lib.genAttrs supportedSystems (
system:
f {
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
}
);
in {
nixosModules.default.imports = [./nixarr vpnconfinement.nixosModules.default];
nixosModules.default.imports = [
./nixarr
vpnconfinement.nixosModules.default
];
# Add tests attribute to the flake outputs
# To run interactively run:
# > nix build .#checks.x86_64-linux.monitoring-test.driver -L
checks = forAllSystems ({pkgs}: {
permissions-test = pkgs.callPackage ./tests/permissions-test.nix {
inherit (self) nixosModules;
};
simple-test = pkgs.callPackage ./tests/simple-test.nix {
inherit (self) nixosModules;
};
# vpn-confinement-test = pkgs.callPackage ./tests/vpn-confinement-test.nix {
# inherit (self) nixosModules;
# };
});
devShells = forAllSystems ({pkgs}: {
default = pkgs.mkShell {
packages = with pkgs; [
alejandra
nixd
];
};
});
packages = forAllSystems ({pkgs}: let
website = website-builder.lib {
pkgs = pkgs;
src = "${self}";
timestamp = self.lastModified;
headerTitle = "Nixarr";
standalonePages = [
{
title = "Nixarr - Media Server Nixos Module";
inputFile = ./README.md;
outputFile = "index.html";
}
];
includedDirs = ["docs"];
articleDirs = ["docs/wiki"];
navbar = [
{
title = "Home";
location = "/";
}
{
title = "Options";
location = "/nixos-options";
}
{
title = "Wiki";
location = "/wiki";
}
{
title = "Github";
location = "https://github.com/rasmus-kirk/nixarr";
}
];
favicons = {
# For all browsers
"16x16" = "/docs/img/favicons/16x16.png";
"32x32" = "/docs/img/favicons/32x32.png";
# For Google and Android
"48x48" = "/docs/img/favicons/48x48.png";
"192x192" = "/docs/img/favicons/192x192.png";
# For iPad
"167x167" = "/docs/img/favicons/167x167.png";
# For iPhone
"180x180" = "/docs/img/favicons/180x180.png";
checks = forAllSystems (
{pkgs}: {
permissions-test = pkgs.callPackage ./tests/permissions-test.nix {
inherit (self) nixosModules;
};
nixosModules = ./nixarr;
};
in {
default = website.package;
debug = website.loop;
});
simple-test = pkgs.callPackage ./tests/simple-test.nix {
inherit (self) nixosModules;
};
# vpn-confinement-test = pkgs.callPackage ./tests/vpn-confinement-test.nix {
# inherit (self) nixosModules;
# };
}
);
devShells = forAllSystems (
{pkgs}: {
default = pkgs.mkShell {
packages = with pkgs; [
alejandra
nixd
];
};
}
);
packages = forAllSystems (
{pkgs}: let
website = website-builder.lib {
pkgs = pkgs;
src = "${self}";
timestamp = self.lastModified;
headerTitle = "Nixarr";
standalonePages = [
{
title = "Nixarr - Media Server Nixos Module";
inputFile = ./README.md;
outputFile = "index.html";
}
];
includedDirs = ["docs"];
articleDirs = ["docs/wiki"];
navbar = [
{
title = "Home";
location = "/";
}
{
title = "Options";
location = "/nixos-options";
}
{
title = "Wiki";
location = "/wiki";
}
{
title = "Github";
location = "https://github.com/nix-media-server/nixarr";
}
];
favicons = {
# For all browsers
"16x16" = "/docs/img/favicons/16x16.png";
"32x32" = "/docs/img/favicons/32x32.png";
# For Google and Android
"48x48" = "/docs/img/favicons/48x48.png";
"192x192" = "/docs/img/favicons/192x192.png";
# For iPad
"167x167" = "/docs/img/favicons/167x167.png";
# For iPhone
"180x180" = "/docs/img/favicons/180x180.png";
};
nixosModules = ./nixarr;
};
in {
default = website.package;
debug = website.loop;
}
);
formatter = forAllSystems ({pkgs}: pkgs.alejandra);
};
+1 -1
View File
@@ -93,7 +93,7 @@ in {
Group = globals.bazarr.group;
SyslogIdentifier = "bazarr";
ExecStart = pkgs.writeShellScript "start-bazarr" ''
${pkgs.bazarr}/bin/bazarr \
${cfg.package}/bin/bazarr \
--config '${cfg.stateDir}' \
--port ${toString cfg.port} \
--no-update True
+2
View File
@@ -22,6 +22,7 @@ in {
./openssh
./plex
./prowlarr
./qbittorrent
./radarr
./readarr
./readarr-audiobook
@@ -74,6 +75,7 @@ in {
- [SABnzbd](#nixarr.sabnzbd.enable)
- [Sonarr](#nixarr.sonarr.enable)
- [Transmission](#nixarr.transmission.enable)
- [qBittorrent](#nixarr.qbittorrent.enable)
Remember to read the options!
'';
+93
View File
@@ -0,0 +1,93 @@
# Utilities for defining *arr service settings, options, and configs.
#
# Copied from nixpkgs master as of 2025-11-11: https://raw.githubusercontent.com/NixOS/nixpkgs/cf540f8c9840457ed90a315dd635bceecb78495a/nixos/modules/services/misc/servarr/settings-options.nix
{
lib,
pkgs,
}: {
mkServarrSettingsOptions = name: port:
lib.mkOption {
type = lib.types.submodule {
freeformType = (pkgs.formats.ini {}).type;
options = {
update = {
mechanism = lib.mkOption {
type = with lib.types;
nullOr (enum [
"external"
"builtIn"
"script"
]);
description = "which update mechanism to use";
default = "external";
};
automatically = lib.mkOption {
type = lib.types.bool;
description = "Automatically download and install updates.";
default = false;
};
};
server = {
port = lib.mkOption {
type = lib.types.port;
description = "Port Number";
default = port;
};
};
log = {
analyticsEnabled = lib.mkOption {
type = lib.types.bool;
description = "Send Anonymous Usage Data";
default = false;
};
};
};
};
example = lib.options.literalExpression ''
{
update.mechanism = "internal";
server = {
urlbase = "localhost";
port = ${toString port};
bindaddress = "*";
};
}
'';
default = {};
description = ''
Attribute set of arbitrary config options.
Please consult the documentation at the [wiki](https://wiki.servarr.com/useful-tools#using-environment-variables-for-config).
WARNING: this configuration is stored in the world-readable Nix store!
For secrets use [](#opt-services.${name}.environmentFiles).
'';
};
mkServarrEnvironmentFiles = name:
lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [];
description = ''
Environment file to pass secret configuration values.
Each line must follow the `${lib.toUpper name}__SECTION__KEY=value` pattern.
Please consult the documentation at the [wiki](https://wiki.servarr.com/useful-tools#using-environment-variables-for-config).
'';
};
mkServarrSettingsEnvVars = name: settings:
lib.pipe settings [
(lib.mapAttrsRecursive (
path: value:
lib.optionalAttrs (value != null) {
name = lib.toUpper "${name}__${lib.concatStringsSep "__" path}";
value = toString (
if lib.isBool value
then lib.boolToString value
else value
);
}
))
(lib.collect (x: lib.isString x.name or false && lib.isString x.value or false))
lib.listToAttrs
];
}
+201
View File
@@ -0,0 +1,201 @@
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.nixarr.qbittorrent;
globals = config.util-nixarr.globals;
nixarr = config.nixarr;
downloadDir = "${nixarr.mediaDir}/torrents";
in {
options.nixarr.qbittorrent = {
enable = mkOption {
type = types.bool;
default = false;
example = true;
description = "Whether or not to enable the qBittorrent service.";
};
package = mkPackageOption pkgs "qbittorrent-nox" {};
stateDir = mkOption {
type = types.path;
default = "${nixarr.stateDir}/qbittorrent";
defaultText = literalExpression ''"''${nixarr.stateDir}/qbittorrent"'';
example = "/nixarr/.state/qbittorrent";
description = ''
The location of the state directory for the qBittorrent 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/qbittorrent
> ```
>
> Is not supported, because `/home/user` is owned by `user`.
'';
};
openFirewall = mkOption {
type = types.bool;
defaultText = literalExpression ''!nixarr.qbittorrent.vpn.enable'';
default = !cfg.vpn.enable;
example = true;
description = "Open firewall for `peerPort` and `uiPort`.";
};
vpn.enable = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
**Required options:** [`nixarr.vpn.enable`](#nixarr.vpn.enable)
Route qBittorrent traffic through the VPN.
'';
};
peerPort = mkOption {
type = types.port;
default = 50000;
example = 12345;
description = "qBittorrent peer traffic port.";
};
uiPort = mkOption {
type = types.port;
default = 8080;
example = 12345;
description = "qBittorrent web-UI port.";
};
extraSettings = mkOption {
type = types.attrs;
default = {};
description = ''
Extra config settings for the qBittorrent service passed to
`services.qbittorrent.serverConfig`.
See [Explanation-of-Options-in-qBittorrent](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent).
'';
};
};
config = mkIf (nixarr.enable && cfg.enable) {
assertions = [
{
assertion = cfg.vpn.enable -> nixarr.vpn.enable;
message = ''
The nixarr.qbittorrent.vpn.enable option requires the
nixarr.vpn.enable option to be set, but it was not.
'';
}
];
systemd.tmpfiles.rules = [
"d '${cfg.stateDir}' 0750 qbittorrent media - -"
"d '${nixarr.mediaDir}/torrents' 0775 qbittorrent media - -"
"d '${nixarr.mediaDir}/torrents/.incomplete' 0775 qbittorrent media - -"
"d '${nixarr.mediaDir}/torrents/manual' 0775 qbittorrent media - -"
"d '${nixarr.mediaDir}/torrents/lidarr' 0775 qbittorrent media - -"
"d '${nixarr.mediaDir}/torrents/radarr' 0775 qbittorrent media - -"
"d '${nixarr.mediaDir}/torrents/sonarr' 0775 qbittorrent media - -"
"d '${nixarr.mediaDir}/torrents/readarr' 0775 qbittorrent media - -"
];
services.qbittorrent = {
enable = true;
package = cfg.package;
user = "qbittorrent";
group = "media";
profileDir = cfg.stateDir;
webuiPort = cfg.uiPort;
torrentingPort = cfg.peerPort;
openFirewall = cfg.openFirewall;
serverConfig = lib.recursiveUpdate
{
LegalNotice.Accepted = true;
BitTorrent.Session = {
DefaultSavePath = downloadDir;
TempPath = "${downloadDir}/.incomplete";
TempPathEnabled = true;
Port = cfg.peerPort;
"BTProtocol" = "TCP";
};
Preferences = {
WebUI = {
Address =
if cfg.vpn.enable
then "192.168.15.1"
else "0.0.0.0";
Port = cfg.uiPort;
};
Downloads = {
SavePath = downloadDir;
TempPath = "${downloadDir}/.incomplete";
TempPathEnabled = true;
};
};
}
cfg.extraSettings;
};
users.users.qbittorrent = {
isSystemUser = true;
group = "media";
uid = 71;
};
systemd.services.qbittorrent = {
serviceConfig.IOSchedulingPriority = 7;
serviceConfig.UMask = "0002";
vpnConfinement = mkIf cfg.vpn.enable {
enable = true;
vpnNamespace = "wg";
};
};
vpnNamespaces.wg = mkIf cfg.vpn.enable {
portMappings = [
{
from = cfg.uiPort;
to = cfg.uiPort;
}
];
openVPNPorts = [
{
port = cfg.peerPort;
protocol = "both";
}
];
};
services.nginx = mkIf cfg.vpn.enable {
enable = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
virtualHosts."127.0.0.1:${builtins.toString cfg.uiPort}" = {
listen = [
{
addr = "0.0.0.0";
port = cfg.uiPort;
}
];
locations."/" = {
recommendedProxySettings = true;
proxyWebsockets = true;
proxyPass = "http://192.168.15.1:${builtins.toString cfg.uiPort}";
};
};
};
};
}
+60 -4
View File
@@ -6,9 +6,12 @@
}:
with lib; let
cfg = config.nixarr.readarr-audiobook;
service-cfg = config.services.readarr-audiobook;
globals = config.util-nixarr.globals;
nixarr = config.nixarr;
port = 9494;
arr-settings-options = import ../lib/arr-settings-options.nix {inherit lib pkgs;};
in {
options.nixarr.readarr-audiobook = {
enable = mkOption {
@@ -69,6 +72,46 @@ in {
};
};
# A tweaked copy of services.readarr from nixpkgs
options.services.readarr-audiobook = {
enable = lib.mkEnableOption "Readarr-Audiobook, a Usenet/BitTorrent audiobook downloader";
dataDir = lib.mkOption {
type = lib.types.str;
description = "The directory where Readarr-Audiobook stores its data files.";
};
package = lib.mkPackageOption pkgs "readarr" {};
openFirewall = lib.mkOption {
type = lib.types.bool;
description = ''
Open ports in the firewall for Readarr-Audiobook.
'';
};
# Uses name in description to refer to
# `services.readarr-audiobook.environmentFiles`.
settings = arr-settings-options.mkServarrSettingsOptions "readarr-audiobook" port;
# Uses name in description to document `READARR__*` environment variables.
environmentFiles = arr-settings-options.mkServarrEnvironmentFiles "readarr";
user = lib.mkOption {
type = lib.types.str;
description = ''
User account under which Readarr-Audiobook runs.
'';
};
group = lib.mkOption {
type = lib.types.str;
description = ''
Group under which Readarr-Audiobook runs.
'';
};
};
config = mkIf (nixarr.enable && cfg.enable) {
assertions = [
{
@@ -96,17 +139,30 @@ in {
"d '${nixarr.mediaDir}/library/audiobooks' 0775 ${globals.libraryOwner.user} ${globals.libraryOwner.group} - -"
];
services.readarr-audiobook = {
enable = cfg.enable;
package = cfg.package;
settings.server.port = cfg.port;
openFirewall = cfg.openFirewall;
dataDir = cfg.stateDir;
user = globals.readarr-audiobook.user;
group = globals.readarr-audiobook.group;
};
systemd.services.readarr-audiobook = {
description = "Readarr-Audiobook";
after = ["network.target"];
wantedBy = ["multi-user.target"];
environment.READARR__SERVER__PORT = builtins.toString cfg.port;
# Uses name to define `READARR__*` environment variables.
environment = arr-settings-options.mkServarrSettingsEnvVars "readarr" service-cfg.settings;
serviceConfig = {
Type = "simple";
User = globals.readarr-audiobook.user;
Group = globals.readarr-audiobook.group;
ExecStart = "${lib.getExe cfg.package} -nobrowser -data=${cfg.stateDir}";
User = service-cfg.user;
Group = service-cfg.group;
EnvironmentFile = service-cfg.environmentFiles;
ExecStart = "${lib.getExe service-cfg.package} -nobrowser -data=${service-cfg.dataDir}";
Restart = "on-failure";
};
};
+8 -13
View File
@@ -94,19 +94,14 @@ in {
"d '${nixarr.mediaDir}/library/books' 0775 ${globals.libraryOwner.user} ${globals.libraryOwner.group} - -"
];
systemd.services.readarr = {
description = "Readarr";
after = ["network.target"];
wantedBy = ["multi-user.target"];
environment.READARR__SERVER__PORT = builtins.toString cfg.port;
serviceConfig = {
Type = "simple";
User = globals.readarr.user;
Group = globals.readarr.group;
ExecStart = "${lib.getExe cfg.package} -nobrowser -data=${cfg.stateDir}";
Restart = "on-failure";
};
services.readarr = {
enable = cfg.enable;
package = cfg.package;
settings.server.port = cfg.port;
openFirewall = cfg.openFirewall;
dataDir = cfg.stateDir;
user = globals.readarr.user;
group = globals.readarr.group;
};
networking.firewall = mkIf cfg.openFirewall {
+51 -1
View File
@@ -8,7 +8,57 @@ with lib; let
cfg = config.nixarr.recyclarr;
globals = config.util-nixarr.globals;
nixarr = config.nixarr;
format = pkgs.formats.yaml {};
# This is a carbon copy of the yaml implementation in nixpkgs https://github.com/NixOS/nixpkgs/blob/fde6c4aec177afa2d0248b1c5983e2a72a231442/pkgs/pkgs-lib/formats.nix#L210-L231
# except we've replaced json2yaml for yq-go to allow it to parse custom yaml tags
# ideally this would some day be upstreamed, see https://github.com/NixOS/nix/issues/4910 and https://github.com/nix-media-server/nixarr/issues/91
yamlGenerator = {preserved-tags ? []}: let
selectors =
pkgs.lib.strings.concatStringsSep "|"
(builtins.map (
# this is yq for "for all the scalers, if they match this regex, do a regex substitution and set the tag"
x: ''
with((.. | select(kind == "scalar") | select(tag == "!!str") | select(test("^!${x} .*"))); . = sub("!${x} ", "") | . tag="!${x}")
''
)
preserved-tags);
in {
generate = name: value:
pkgs.callPackage (
{
runCommand,
yq-go,
}:
runCommand name
{
nativeBuildInputs = [yq-go];
value = builtins.toJSON value;
passAsFile = ["value"];
preferLocalBuild = true;
}
''
yq '${selectors}' "$valuePath" -o yaml > $out
''
) {};
type = let
baseType = pkgs.lib.types.oneOf [
pkgs.lib.types.bool
pkgs.lib.types.int
pkgs.lib.types.float
pkgs.lib.types.str
(pkgs.lib.types.attrsOf valueType)
(pkgs.lib.types.listOf valueType)
];
valueType =
(pkgs.lib.types.nullOr baseType)
// {
description = "Yaml value";
};
in
valueType;
};
format = yamlGenerator {
preserved-tags = ["env_var"];
};
# Generate configuration file from Nix attribute set if provided
generatedConfigFile = format.generate "recyclarr-config.yml" cfg.configuration;
+11
View File
@@ -29,6 +29,11 @@ with lib; let
runtimeInputs = with pkgs; [yq];
text = ''
while [ ! -f "${nixarr.prowlarr.stateDir}/config.xml" ]; do
echo "Waiting for prowlarr to start..."
sleep 1
done
touch ${cfg.stateDir}/prowlarr-api-key
chmod 400 ${cfg.stateDir}/prowlarr-api-key
chown ${globals.transmission.user} ${cfg.stateDir}/prowlarr-api-key
@@ -49,6 +54,12 @@ with lib; let
+ ''
TMP_JSON=$(mktemp)
CRED_FILE="/run/secrets/cross-seed/credentialsFile.json"
while [ ! -f "${nixarr.prowlarr.stateDir}/config.xml" ]; do
echo "Waiting for prowlarr to start..."
sleep 1
done
PROWLARR_API_KEY=$(xq -r '.Config.ApiKey' "${nixarr.prowlarr.stateDir}/config.xml")
# shellcheck disable=SC2034
CRED_DIR=$(dirname "$CRED_FILE")
+1 -1
View File
@@ -4,7 +4,7 @@
nixosModules,
lib ? pkgs.lib,
}:
pkgs.nixosTest {
pkgs.testers.nixosTest {
name = "nixarr-permissions-test";
nodes.machine = {
+1 -1
View File
@@ -3,7 +3,7 @@
nixosModules,
lib ? pkgs.lib,
}:
pkgs.nixosTest {
pkgs.testers.nixosTest {
name = "simple-test";
nodes.machine = {
+41 -18
View File
@@ -43,19 +43,27 @@ The test ensures that:
wgGatewayPort = 51820;
# Generate real WireGuard keys
wgGatewayPrivateKey = pkgs.runCommand "wg-gateway-private" {buildInputs = [pkgs.wireguard-tools];} ''
wg genkey > $out
'';
wgGatewayPublicKey = pkgs.runCommand "wg-gateway-public" {buildInputs = [pkgs.wireguard-tools];} ''
cat ${wgGatewayPrivateKey} | wg pubkey > $out
'';
wgGatewayPrivateKey =
pkgs.runCommand "wg-gateway-private" {buildInputs = [pkgs.wireguard-tools];}
''
wg genkey > $out
'';
wgGatewayPublicKey =
pkgs.runCommand "wg-gateway-public" {buildInputs = [pkgs.wireguard-tools];}
''
cat ${wgGatewayPrivateKey} | wg pubkey > $out
'';
wgClientPrivateKey = pkgs.runCommand "wg-client-private" {buildInputs = [pkgs.wireguard-tools];} ''
wg genkey > $out
'';
wgClientPublicKey = pkgs.runCommand "wg-client-public" {buildInputs = [pkgs.wireguard-tools];} ''
cat ${wgClientPrivateKey} | wg pubkey > $out
'';
wgClientPrivateKey =
pkgs.runCommand "wg-client-private" {buildInputs = [pkgs.wireguard-tools];}
''
wg genkey > $out
'';
wgClientPublicKey =
pkgs.runCommand "wg-client-public" {buildInputs = [pkgs.wireguard-tools];}
''
cat ${wgClientPrivateKey} | wg pubkey > $out
'';
# Network configuration
wgGatewayAddr = "10.100.0.1";
@@ -92,7 +100,7 @@ The test ensures that:
PersistentKeepalive = 25
'';
in
pkgs.nixosTest {
pkgs.testers.nixosTest {
name = "nixarr-vpn-confinement-test";
# Disable interactive mode to avoid hanging
@@ -128,7 +136,10 @@ in
"${internetClientIP}/24"
"${internetClientIPv6}/64"
];
gateway = ["${internetGatewayIP}" "${internetGatewayIPv6}"];
gateway = [
"${internetGatewayIP}"
"${internetGatewayIPv6}"
];
routes = [
{
Destination = "${wgSubnet}";
@@ -189,7 +200,10 @@ in
pkgs,
...
}: {
virtualisation.vlans = [1 2]; # VLAN 1 for LAN, VLAN 2 for Internet
virtualisation.vlans = [
1
2
]; # VLAN 1 for LAN, VLAN 2 for Internet
networking = {
interfaces.eth1 = {
@@ -224,19 +238,28 @@ in
firewall = {
enable = true;
allowedUDPPorts = [wgGatewayPort 51413];
allowedUDPPorts = [
wgGatewayPort
51413
];
allowedTCPPorts = [51413];
};
wireguard.interfaces.wg0 = {
ips = ["${wgGatewayAddr}/24" "${wgGatewayAddrV6}/64"];
ips = [
"${wgGatewayAddr}/24"
"${wgGatewayAddrV6}/64"
];
listenPort = wgGatewayPort;
privateKeyFile = "${wgGatewayPrivateKey}";
peers = [
{
publicKey = builtins.readFile wgClientPublicKey;
allowedIPs = ["${wgClientAddr}/32" "${wgClientAddrV6}/128"];
allowedIPs = [
"${wgClientAddr}/32"
"${wgClientAddrV6}/128"
];
}
];
};
+5
View File
@@ -35,6 +35,7 @@ in {
readarr-audiobook = 211;
recyclarr = 269;
sabnzbd = 38;
qbittorrent = 71;
transmission = 70;
# Removed 2025-10-29
# cross-seed = 183;
@@ -112,6 +113,10 @@ in {
user = "sonarr";
group = globals.libraryOwner.group;
};
qbittorrent = {
user = "qbittorrent";
group = globals.libraryOwner.group;
};
transmission = {
user = "transmission";
group = globals.libraryOwner.group;