Add cross-seed module

This commit is contained in:
batteredbunny 2024-11-14 22:16:41 +02:00
parent 8a1bec8673
commit 8a7e77d5f9

346
modules/cross-seed.nix Normal file
View file

@ -0,0 +1,346 @@
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.services.cross-seed;
inherit (lib)
mkOption
mkEnableOption
mkPackageOption
mkIf
types;
in
{
options.services.cross-seed = {
enable = mkEnableOption "cross-seed";
package = mkPackageOption pkgs "cross-seed" { };
dataDir = mkOption {
type = types.path;
default = "/var/lib/cross-seed";
description = ''
Path to a folder that will contain cross-seed working directory.
'';
};
user = mkOption {
type = types.str;
default = "cross-seed";
description = lib.mdDoc "User account under which cross-seed runs.";
};
group = mkOption {
type = types.str;
default = "cross-seed";
description = lib.mdDoc "Group under which cross-seed runs.";
};
settings = {
torznab = mkOption {
type = types.listOf types.str;
example = [ "http://localhost:9696/1/api?apikey=1234" ];
description = "Torznab URLs with apikey included";
};
dataDirs = mkOption {
type = types.listOf types.path;
default = [ ];
example = [ "/downloads/movies" "/downloads/packs" ];
description = "Directories to use if searching by data instead of torrents";
};
matchMode = mkOption {
type = types.enum [ "safe" "risky" "partial" ];
default = "safe";
description = "Matching mode for torrent comparison";
};
linkCategory = mkOption {
type = types.str;
default = "cross-seed-link";
description = "Torrent client category for linked torrents";
};
linkDir = mkOption {
type = types.nullOr types.path;
default = null;
description = "If this is specified, cross-seed will create links to matched files in the specified directory. It will create a different link for every changed file name or directory structure.";
};
flatLinking = mkOption {
type = types.bool;
default = false;
description = "Use flat linking directory structure";
};
linkType = mkOption {
type = types.enum [ "symlink" "hardlink" ];
default = "symlink";
description = "Type of links for data-based matches";
};
maxDataDepth = mkOption {
type = types.int;
default = 2;
description = "Max depth to look for searchees in dataDirs";
};
torrentDir = mkOption {
type = types.path;
example = "~/.local/share/qBittorrent/BT_backup";
description = "Directory containing .torrent files";
};
outputDir = mkOption {
type = types.path;
default = "${cfg.dataDir}/output";
example = "/cross-seeds";
description = "Where to save the torrent files that cross-seed finds for you.";
};
includeNonVideos = mkOption {
type = types.bool;
default = false;
description = "Include torrents which contain non-video files";
};
includeSingleEpisodes = mkOption {
type = types.bool;
default = false;
description = "Include single episode torrents in the search";
};
fuzzySizeThreshold = mkOption {
type = types.float;
default = 0.02;
description = "You should NOT modify this unless you have good reason. The following option is the preliminary value to compare sizes of releases for further comparison.";
};
excludeOlder = mkOption {
type = types.nullOr types.str;
default = null;
example = "2 weeks";
description = "Exclude torrents first seen more than n minutes ago";
};
excludeRecentSearch = mkOption {
type = types.nullOr types.str;
default = null;
example = "3 days";
description = "Exclude torrents searched more recently than n minutes ago";
};
verbose = mkOption {
type = types.bool;
default = false;
description = "Enable verbose logging";
};
action = mkOption {
type = types.enum [ "save" "inject" ];
default = "save";
description = "Action to take with found torrents";
};
rtorrentRpcUrl = mkOption {
type = types.nullOr types.str;
default = null;
example = "http://username:password@localhost:1234/RPC2";
description = "rTorrent XMLRPC interface URL";
};
qbittorrentUrl = mkOption {
type = types.nullOr types.str;
default = null;
example = "http://username:password@localhost:8080";
description = "qBittorrent WebUI URL";
};
transmissionRpcUrl = mkOption {
type = types.nullOr types.str;
default = null;
example = "http://username:password@localhost:9091/transmission/rpc";
description = "Transmission RPC interface URL";
};
delugeRpcUrl = mkOption {
type = types.nullOr types.str;
default = null;
example = "http://:password@localhost:8112/json";
description = "Deluge JSON-RPC interface URL";
};
duplicateCategories = mkOption {
type = types.bool;
default = false;
description = "Create and inject using categories with same save paths";
};
notificationWebhookUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = "Webhook URL for notifications. Conforms to the caronc/apprise REST api.";
};
delay = mkOption {
type = types.int;
default = 30;
description = "Pause duration (seconds) between searches";
};
snatchTimeout = mkOption {
type = types.str;
default = "30 seconds";
description = "Timeout for unresponsive snatches";
};
searchTimeout = mkOption {
type = types.str;
default = "30 seconds";
description = "Timeout for unresponsive searches";
};
searchLimit = mkOption {
type = types.int;
default = 0;
description = "Number of searches before stopping (0 for unlimited)";
};
blockList = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"-excludedGroup"
"-excludedGroup2"
"x265"
"Release.Name"
"3317e6485454354751555555366a8308c1e92093"
];
description = "Infohashes and/or strings in torrent names to block";
};
sonarr = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "http://sonarr:8989/?apikey=12345" ];
description = "Sonarr API URLs";
};
radarr = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "http://radarr:7878/?apikey=12345" ];
description = "Radarr API URLs";
};
port = mkOption {
type = types.nullOr types.port;
default = 2468;
description = "Port to listen on";
};
host = mkOption {
type = types.nullOr types.str;
default = "0.0.0.0";
example = "127.0.0.1";
description = "IP address to bind to";
};
searchCadence = mkOption {
type = types.nullOr types.str;
default = null;
example = "1 day";
description = "Schedule for running searches (https://github.com/vercel/ms format)";
};
rssCadence = mkOption {
type = types.nullOr types.str;
default = null;
example = "30 minutes";
description = "Schedule for running RSS scans (https://github.com/vercel/ms format)";
};
apiKey = mkOption {
type = types.nullOr types.str;
default = null;
description = "Provide your own API key here to override the autogenerated one.";
};
};
};
config = mkIf cfg.enable {
users.groups = mkIf (cfg.group == "cross-seed") {
cross-seed = { };
};
users.users = mkIf (cfg.user == "cross-seed") {
cross-seed = {
description = "cross-seed service user";
group = cfg.group;
home = cfg.dataDir;
createHome = true;
isSystemUser = true;
};
};
systemd.services.cross-seed = {
enable = true;
description = "cross-seed in daemon mode";
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -p ${cfg.settings.outputDir}
'';
serviceConfig = {
User = cfg.user;
Group = cfg.group;
Environment = [
"CONFIG_DIR=${cfg.dataDir}"
];
WorkingDirectory = cfg.dataDir;
ExecStart = lib.concatStringsSep " " ([
"${lib.getExe cfg.package}"
"daemon"
"--match-mode ${cfg.settings.matchMode}"
"--link-category ${cfg.settings.linkCategory}"
"--link-type ${cfg.settings.linkType}"
"--max-data-depth ${toString cfg.settings.maxDataDepth}"
"--torrent-dir ${cfg.settings.torrentDir}"
"--output-dir ${cfg.settings.outputDir}"
"--delay ${toString cfg.settings.delay}"
"--snatch-timeout '${cfg.settings.snatchTimeout}'"
"--search-timeout '${cfg.settings.searchTimeout}'"
"--search-limit ${toString cfg.settings.searchLimit}"
]
++ lib.optionals (cfg.settings.torznab != [ ]) [ "--torznab ${lib.concatStringsSep " " cfg.settings.torznab}" ]
++ lib.optionals (cfg.settings.dataDirs != [ ]) [ "--data-dirs ${lib.concatStringsSep " " cfg.settings.dataDirs}" ]
++ lib.optionals (cfg.settings.linkDir != null) [ "--link-dir ${cfg.settings.linkDir}" ]
++ lib.optionals cfg.settings.flatLinking [ "--flat-linking" ]
++ lib.optionals cfg.settings.includeNonVideos [ "--include-non-videos" ]
++ lib.optionals cfg.settings.includeSingleEpisodes [ "--include-single-episodes" ]
++ lib.optionals (cfg.settings.excludeOlder != null) [ "--exclude-older ${cfg.settings.excludeOlder}" ]
++ lib.optionals (cfg.settings.excludeRecentSearch != null) [ "--exclude-recent-search ${cfg.settings.excludeRecentSearch}" ]
++ lib.optionals cfg.settings.verbose [ "--verbose" ]
++ lib.optionals (cfg.settings.rtorrentRpcUrl != null) [ "--rtorrent-rpc-url ${cfg.settings.rtorrentRpcUrl}" ]
++ lib.optionals (cfg.settings.qbittorrentUrl != null) [ "--qbittorrent-url ${cfg.settings.qbittorrentUrl}" ]
++ lib.optionals (cfg.settings.transmissionRpcUrl != null) [ "--transmission-rpc-url ${cfg.settings.transmissionRpcUrl}" ]
++ lib.optionals (cfg.settings.delugeRpcUrl != null) [ "--deluge-rpc-url ${cfg.settings.delugeRpcUrl}" ]
++ lib.optionals cfg.settings.duplicateCategories [ "--duplicate-categories" ]
++ lib.optionals (cfg.settings.notificationWebhookUrl != null) [ "--notification-webhook-url ${cfg.settings.notificationWebhookUrl}" ]
++ lib.optionals (cfg.settings.blockList != [ ]) [ "--block-list ${lib.concatStringsSep " " cfg.settings.blockList}" ]
++ lib.optionals (cfg.settings.sonarr != [ ]) [ "--sonarr ${lib.concatStringsSep " " cfg.settings.sonarr}" ]
++ lib.optionals (cfg.settings.radarr != [ ]) [ "--radarr ${lib.concatStringsSep " " cfg.settings.radarr}" ]
++ lib.optionals (cfg.settings.port != null) [ "--port ${toString cfg.settings.port}" ]
++ lib.optionals (cfg.settings.host != null) [ "--host ${cfg.settings.host}" ]
++ lib.optionals (cfg.settings.searchCadence != null) [ "--search-cadence '${cfg.settings.searchCadence}'" ]
++ lib.optionals (cfg.settings.rssCadence != null) [ "--rss-cadence '${cfg.settings.rssCadence}'" ]
++ lib.optionals (cfg.settings.apiKey != null) [ "--api-key ${cfg.settings.apiKey}" ]
);
};
};
};
}