Add cross-seed module
This commit is contained in:
parent
8a1bec8673
commit
8a7e77d5f9
1 changed files with 346 additions and 0 deletions
346
modules/cross-seed.nix
Normal file
346
modules/cross-seed.nix
Normal 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}" ]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue