From 8a7e77d5f912c867f08c77ff66969b7438afcc39 Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Thu, 14 Nov 2024 22:16:41 +0200 Subject: [PATCH 1/9] Add cross-seed module --- modules/cross-seed.nix | 346 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 modules/cross-seed.nix diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix new file mode 100644 index 0000000..8add807 --- /dev/null +++ b/modules/cross-seed.nix @@ -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}" ] + ); + }; + }; + }; +} -- 2.49.0 From 5d0731135364b3037ff028dbfdabe6e534b68c29 Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Fri, 15 Nov 2024 00:15:12 +0200 Subject: [PATCH 2/9] Replace cli flags with config file --- modules/cross-seed.nix | 79 ++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix index 8add807..8dc4c0d 100644 --- a/modules/cross-seed.nix +++ b/modules/cross-seed.nix @@ -11,6 +11,45 @@ let mkPackageOption mkIf types; + + configFile = pkgs.writeText "cross-seed-config.js" '' + module.exports = { + apiKey: ${if cfg.settings.apiKey != null then "'${cfg.settings.apiKey}'" else "undefined"}, + torznab: ${if cfg.settings.torznab != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.torznab}]" else "[]"}, + sonarr: ${if cfg.settings.sonarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.sonarr}]" else "undefined"}, + radarr: ${if cfg.settings.radarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.radarr}]" else "undefined"}, + host: ${if cfg.settings.host != null then "'${cfg.settings.host}'" else "undefined"}, + port: ${toString cfg.settings.port}, + notificationWebhookUrl: ${if cfg.settings.notificationWebhookUrl != null then "'${cfg.settings.notificationWebhookUrl}'" else "undefined"}, + rtorrentRpcUrl: ${if cfg.settings.rtorrentRpcUrl != null then "'${cfg.settings.rtorrentRpcUrl}'" else "undefined"}, + qbittorrentUrl: ${if cfg.settings.qbittorrentUrl != null then "'${cfg.settings.qbittorrentUrl}'" else "undefined"}, + transmissionRpcUrl: ${if cfg.settings.transmissionRpcUrl != null then "'${cfg.settings.transmissionRpcUrl}'" else "undefined"}, + delugeRpcUrl: ${if cfg.settings.delugeRpcUrl != null then "'${cfg.settings.delugeRpcUrl}'" else "undefined"}, + delay: ${toString cfg.settings.delay}, + dataDirs: ${if cfg.settings.dataDirs != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.dataDirs}]" else "[]"}, + linkCategory: "${cfg.settings.linkCategory}", + linkDir: ${if cfg.settings.linkDir != null then "'${cfg.settings.linkDir}'" else "undefined"}, + linkType: "${cfg.settings.linkType}", + flatLinking: ${if cfg.settings.flatLinking then "true" else "false"}, + matchMode: "${cfg.settings.matchMode}", + maxDataDepth: ${toString cfg.settings.maxDataDepth}, + torrentDir: "${cfg.settings.torrentDir}", + outputDir: "${cfg.settings.outputDir}", + includeSingleEpisodes: ${if cfg.settings.includeSingleEpisodes then "true" else "false"}, + includeNonVideos: ${if cfg.settings.includeNonVideos then "true" else "false"}, + fuzzySizeThreshold: ${toString cfg.settings.fuzzySizeThreshold}, + excludeOlder: ${if cfg.settings.excludeOlder != null then "'${cfg.settings.excludeOlder}'" else "undefined"}, + excludeRecentSearch: ${if cfg.settings.excludeRecentSearch != null then "'${cfg.settings.excludeRecentSearch}'" else "undefined"}, + action: "${cfg.settings.action}", + duplicateCategories: ${if cfg.settings.duplicateCategories then "true" else "false"}, + rssCadence: ${if cfg.settings.rssCadence != null then "'${cfg.settings.rssCadence}'" else "undefined"}, + searchCadence: ${if cfg.settings.searchCadence != null then "'${cfg.settings.searchCadence}'" else "undefined"}, + snatchTimeout: ${if cfg.settings.snatchTimeout != null then "'${cfg.settings.snatchTimeout}'" else "null"}, + searchTimeout: ${if cfg.settings.searchTimeout != null then "'${cfg.settings.searchTimeout}'" else "null"}, + searchLimit: ${toString cfg.settings.searchLimit}, + blockList: ${if cfg.settings.blockList != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.blockList}]" else "undefined"}, + }; + ''; in { options.services.cross-seed = { @@ -293,6 +332,7 @@ in wantedBy = [ "multi-user.target" ]; preStart = '' mkdir -p ${cfg.settings.outputDir} + ln -s ${configFile} /var/lib/cross-seed/config.js ''; serviceConfig = { @@ -302,44 +342,7 @@ in "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}" ] - ); + ExecStart = "${lib.getExe cfg.package} daemon"; }; }; }; -- 2.49.0 From 22a879890e19c7c3080e59c3160e047df3be6b3c Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Fri, 15 Nov 2024 00:36:37 +0200 Subject: [PATCH 3/9] Add environment file option --- modules/cross-seed.nix | 136 ++++++++++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 35 deletions(-) diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix index 8dc4c0d..60c6571 100644 --- a/modules/cross-seed.nix +++ b/modules/cross-seed.nix @@ -12,42 +12,42 @@ let mkIf types; - configFile = pkgs.writeText "cross-seed-config.js" '' + configFile = pkgs.writeText "config.js" '' module.exports = { - apiKey: ${if cfg.settings.apiKey != null then "'${cfg.settings.apiKey}'" else "undefined"}, - torznab: ${if cfg.settings.torznab != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.torznab}]" else "[]"}, - sonarr: ${if cfg.settings.sonarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.sonarr}]" else "undefined"}, - radarr: ${if cfg.settings.radarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.radarr}]" else "undefined"}, - host: ${if cfg.settings.host != null then "'${cfg.settings.host}'" else "undefined"}, - port: ${toString cfg.settings.port}, - notificationWebhookUrl: ${if cfg.settings.notificationWebhookUrl != null then "'${cfg.settings.notificationWebhookUrl}'" else "undefined"}, - rtorrentRpcUrl: ${if cfg.settings.rtorrentRpcUrl != null then "'${cfg.settings.rtorrentRpcUrl}'" else "undefined"}, - qbittorrentUrl: ${if cfg.settings.qbittorrentUrl != null then "'${cfg.settings.qbittorrentUrl}'" else "undefined"}, - transmissionRpcUrl: ${if cfg.settings.transmissionRpcUrl != null then "'${cfg.settings.transmissionRpcUrl}'" else "undefined"}, - delugeRpcUrl: ${if cfg.settings.delugeRpcUrl != null then "'${cfg.settings.delugeRpcUrl}'" else "undefined"}, - delay: ${toString cfg.settings.delay}, - dataDirs: ${if cfg.settings.dataDirs != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.dataDirs}]" else "[]"}, - linkCategory: "${cfg.settings.linkCategory}", - linkDir: ${if cfg.settings.linkDir != null then "'${cfg.settings.linkDir}'" else "undefined"}, - linkType: "${cfg.settings.linkType}", - flatLinking: ${if cfg.settings.flatLinking then "true" else "false"}, - matchMode: "${cfg.settings.matchMode}", - maxDataDepth: ${toString cfg.settings.maxDataDepth}, - torrentDir: "${cfg.settings.torrentDir}", - outputDir: "${cfg.settings.outputDir}", - includeSingleEpisodes: ${if cfg.settings.includeSingleEpisodes then "true" else "false"}, - includeNonVideos: ${if cfg.settings.includeNonVideos then "true" else "false"}, - fuzzySizeThreshold: ${toString cfg.settings.fuzzySizeThreshold}, - excludeOlder: ${if cfg.settings.excludeOlder != null then "'${cfg.settings.excludeOlder}'" else "undefined"}, - excludeRecentSearch: ${if cfg.settings.excludeRecentSearch != null then "'${cfg.settings.excludeRecentSearch}'" else "undefined"}, - action: "${cfg.settings.action}", - duplicateCategories: ${if cfg.settings.duplicateCategories then "true" else "false"}, - rssCadence: ${if cfg.settings.rssCadence != null then "'${cfg.settings.rssCadence}'" else "undefined"}, - searchCadence: ${if cfg.settings.searchCadence != null then "'${cfg.settings.searchCadence}'" else "undefined"}, - snatchTimeout: ${if cfg.settings.snatchTimeout != null then "'${cfg.settings.snatchTimeout}'" else "null"}, - searchTimeout: ${if cfg.settings.searchTimeout != null then "'${cfg.settings.searchTimeout}'" else "null"}, - searchLimit: ${toString cfg.settings.searchLimit}, - blockList: ${if cfg.settings.blockList != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.blockList}]" else "undefined"}, + apiKey: process.env.CROSS_SEED_API_KEY || ${if cfg.settings.apiKey != null then "'${cfg.settings.apiKey}'" else "undefined"}, + torznab: process.env.CROSS_SEED_TORZNAB ? process.env.CROSS_SEED_TORZNAB.split(',') : ${if cfg.settings.torznab != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.torznab}]" else "[]"}, + sonarr: process.env.CROSS_SEED_SONARR ? process.env.CROSS_SEED_SONARR.split(',') : ${if cfg.settings.sonarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.sonarr}]" else "undefined"}, + radarr: process.env.CROSS_SEED_RADARR ? process.env.CROSS_SEED_RADARR.split(',') : ${if cfg.settings.radarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.radarr}]" else "undefined"}, + host: process.env.CROSS_SEED_HOST || ${if cfg.settings.host != null then "'${cfg.settings.host}'" else "undefined"}, + port: parseInt(process.env.CROSS_SEED_PORT) || ${toString cfg.settings.port}, + notificationWebhookUrl: process.env.CROSS_SEED_NOTIFICATION_WEBHOOK_URL || ${if cfg.settings.notificationWebhookUrl != null then "'${cfg.settings.notificationWebhookUrl}'" else "undefined"}, + rtorrentRpcUrl: process.env.CROSS_SEED_RTORRENT_RPC_URL || ${if cfg.settings.rtorrentRpcUrl != null then "'${cfg.settings.rtorrentRpcUrl}'" else "undefined"}, + qbittorrentUrl: process.env.CROSS_SEED_QBITTORRENT_URL || ${if cfg.settings.qbittorrentUrl != null then "'${cfg.settings.qbittorrentUrl}'" else "undefined"}, + transmissionRpcUrl: process.env.CROSS_SEED_TRANSMISSION_RPC_URL || ${if cfg.settings.transmissionRpcUrl != null then "'${cfg.settings.transmissionRpcUrl}'" else "undefined"}, + delugeRpcUrl: process.env.CROSS_SEED_DELUGE_RPC_URL || ${if cfg.settings.delugeRpcUrl != null then "'${cfg.settings.delugeRpcUrl}'" else "undefined"}, + delay: parseInt(process.env.CROSS_SEED_DELAY) || ${toString cfg.settings.delay}, + dataDirs: process.env.CROSS_SEED_DATA_DIRS ? process.env.CROSS_SEED_DATA_DIRS.split(',') : ${if cfg.settings.dataDirs != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.dataDirs}]" else "[]"}, + linkCategory: process.env.CROSS_SEED_LINK_CATEGORY || "${cfg.settings.linkCategory}", + linkDir: process.env.CROSS_SEED_LINK_DIR || ${if cfg.settings.linkDir != null then "'${cfg.settings.linkDir}'" else "undefined"}, + linkType: process.env.CROSS_SEED_LINK_TYPE || "${cfg.settings.linkType}", + flatLinking: process.env.CROSS_SEED_FLAT_LINKING ? process.env.CROSS_SEED_FLAT_LINKING === 'true' : ${if cfg.settings.flatLinking then "true" else "false"}, + matchMode: process.env.CROSS_SEED_MATCH_MODE || "${cfg.settings.matchMode}", + maxDataDepth: parseInt(process.env.CROSS_SEED_MAX_DATA_DEPTH) || ${toString cfg.settings.maxDataDepth}, + torrentDir: process.env.CROSS_SEED_TORRENT_DIR || "${cfg.settings.torrentDir}", + outputDir: process.env.CROSS_SEED_OUTPUT_DIR || "${cfg.settings.outputDir}", + includeSingleEpisodes: process.env.CROSS_SEED_INCLUDE_SINGLE_EPISODES ? process.env.CROSS_SEED_INCLUDE_SINGLE_EPISODES === 'true' : ${if cfg.settings.includeSingleEpisodes then "true" else "false"}, + includeNonVideos: process.env.CROSS_SEED_INCLUDE_NON_VIDEOS ? process.env.CROSS_SEED_INCLUDE_NON_VIDEOS === 'true' : ${if cfg.settings.includeNonVideos then "true" else "false"}, + fuzzySizeThreshold: parseFloat(process.env.CROSS_SEED_FUZZY_SIZE_THRESHOLD) || ${toString cfg.settings.fuzzySizeThreshold}, + excludeOlder: process.env.CROSS_SEED_EXCLUDE_OLDER || ${if cfg.settings.excludeOlder != null then "'${cfg.settings.excludeOlder}'" else "undefined"}, + excludeRecentSearch: process.env.CROSS_SEED_EXCLUDE_RECENT_SEARCH || ${if cfg.settings.excludeRecentSearch != null then "'${cfg.settings.excludeRecentSearch}'" else "undefined"}, + action: process.env.CROSS_SEED_ACTION || "${cfg.settings.action}", + duplicateCategories: process.env.CROSS_SEED_DUPLICATE_CATEGORIES ? process.env.CROSS_SEED_DUPLICATE_CATEGORIES === 'true' : ${if cfg.settings.duplicateCategories then "true" else "false"}, + rssCadence: process.env.CROSS_SEED_RSS_CADENCE || ${if cfg.settings.rssCadence != null then "'${cfg.settings.rssCadence}'" else "undefined"}, + searchCadence: process.env.CROSS_SEED_SEARCH_CADENCE || ${if cfg.settings.searchCadence != null then "'${cfg.settings.searchCadence}'" else "undefined"}, + snatchTimeout: process.env.CROSS_SEED_SNATCH_TIMEOUT || ${if cfg.settings.snatchTimeout != null then "'${cfg.settings.snatchTimeout}'" else "null"}, + searchTimeout: process.env.CROSS_SEED_SEARCH_TIMEOUT || ${if cfg.settings.searchTimeout != null then "'${cfg.settings.searchTimeout}'" else "null"}, + searchLimit: parseInt(process.env.CROSS_SEED_SEARCH_LIMIT) || ${toString cfg.settings.searchLimit}, + blockList: process.env.CROSS_SEED_BLOCK_LIST ? process.env.CROSS_SEED_BLOCK_LIST.split(',') : ${if cfg.settings.blockList != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.blockList}]" else "undefined"}, }; ''; in @@ -77,6 +77,71 @@ in description = lib.mdDoc "Group under which cross-seed runs."; }; + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to an environment file containing configuration values. + This file can contain the following variables: + + Sensitive Information: + * `CROSS_SEED_API_KEY`: Override the autogenerated API key + * `CROSS_SEED_TORZNAB`: Comma-separated Torznab URLs with apikey included + * `CROSS_SEED_SONARR`: Comma-separated Sonarr API URLs with apikey + * `CROSS_SEED_RADARR`: Comma-separated Radarr API URLs with apikey + * `CROSS_SEED_NOTIFICATION_WEBHOOK_URL`: Webhook URL for notifications + * `CROSS_SEED_RTORRENT_RPC_URL`: rTorrent XMLRPC URL (with credentials) + * `CROSS_SEED_QBITTORRENT_URL`: qBittorrent WebUI URL (with credentials) + * `CROSS_SEED_TRANSMISSION_RPC_URL`: Transmission RPC URL (with credentials) + * `CROSS_SEED_DELUGE_RPC_URL`: Deluge JSON-RPC URL (with credentials) + + Network Configuration: + * `CROSS_SEED_HOST`: Bind address (e.g., "127.0.0.1") + * `CROSS_SEED_PORT`: Listen port (default: 2468) + + Directory Settings: + * `CROSS_SEED_DATA_DIRS`: Comma-separated paths to content directories + * `CROSS_SEED_LINK_DIR`: Directory for creating matched file links + * `CROSS_SEED_TORRENT_DIR`: Directory containing .torrent files + * `CROSS_SEED_OUTPUT_DIR`: Directory to save found torrents + + Linking Configuration: + * `CROSS_SEED_LINK_CATEGORY`: Category for linked torrents (default: "cross-seed-link") + * `CROSS_SEED_LINK_TYPE`: Type of links to create ("symlink" or "hardlink") + * `CROSS_SEED_FLAT_LINKING`: Use flat linking structure ("true" or "false") + + Matching Behavior: + * `CROSS_SEED_MATCH_MODE`: Matching strictness ("safe", "risky", or "partial") + * `CROSS_SEED_MAX_DATA_DEPTH`: Max directory depth for searching + * `CROSS_SEED_INCLUDE_SINGLE_EPISODES`: Include single episodes ("true" or "false") + * `CROSS_SEED_INCLUDE_NON_VIDEOS`: Include non-video content ("true" or "false") + * `CROSS_SEED_FUZZY_SIZE_THRESHOLD`: Size difference threshold (e.g., "0.02") + * `CROSS_SEED_BLOCK_LIST`: Comma-separated list of excluded terms or infohashes + + Timing and Limits: + * `CROSS_SEED_DELAY`: Minimum seconds between searches + * `CROSS_SEED_EXCLUDE_OLDER`: Exclude torrents older than (e.g., "2 weeks") + * `CROSS_SEED_EXCLUDE_RECENT_SEARCH`: Exclude recently searched (e.g., "3 days") + * `CROSS_SEED_RSS_CADENCE`: RSS scan schedule (e.g., "30 minutes") + * `CROSS_SEED_SEARCH_CADENCE`: Search schedule (e.g., "1 day") + * `CROSS_SEED_SNATCH_TIMEOUT`: Snatch request timeout (e.g., "30 seconds") + * `CROSS_SEED_SEARCH_TIMEOUT`: Search request timeout (e.g., "2 minutes") + * `CROSS_SEED_SEARCH_LIMIT`: Maximum searches per batch + + Client Integration: + * `CROSS_SEED_ACTION`: Action on match ("save" or "inject") + * `CROSS_SEED_DUPLICATE_CATEGORIES`: Use original categories ("true" or "false") + + Example: + ``` + CROSS_SEED_API_KEY=your-api-key + CROSS_SEED_TORZNAB=http://indexer1/api?apikey=key1,http://indexer2/api?apikey=key2 + CROSS_SEED_QBITTORRENT_URL=http://username:password@localhost:8080 + CROSS_SEED_DATA_DIRS=/downloads/movies,/downloads/tv + ``` + ''; + }; + settings = { torznab = mkOption { type = types.listOf types.str; @@ -341,6 +406,7 @@ in Environment = [ "CONFIG_DIR=${cfg.dataDir}" ]; + EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; WorkingDirectory = cfg.dataDir; ExecStart = "${lib.getExe cfg.package} daemon"; }; -- 2.49.0 From 4f62fe840d92022e85b22894e39b8dc28f2a0aff Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Fri, 15 Nov 2024 00:48:17 +0200 Subject: [PATCH 4/9] Make torznab and torrentDir nullable --- modules/cross-seed.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix index 60c6571..2dd020a 100644 --- a/modules/cross-seed.nix +++ b/modules/cross-seed.nix @@ -15,7 +15,7 @@ let configFile = pkgs.writeText "config.js" '' module.exports = { apiKey: process.env.CROSS_SEED_API_KEY || ${if cfg.settings.apiKey != null then "'${cfg.settings.apiKey}'" else "undefined"}, - torznab: process.env.CROSS_SEED_TORZNAB ? process.env.CROSS_SEED_TORZNAB.split(',') : ${if cfg.settings.torznab != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.torznab}]" else "[]"}, + torznab: process.env.CROSS_SEED_TORZNAB ? process.env.CROSS_SEED_TORZNAB.split(',') : ${if cfg.settings.torznab != null then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.torznab}]" else "undefined"}, sonarr: process.env.CROSS_SEED_SONARR ? process.env.CROSS_SEED_SONARR.split(',') : ${if cfg.settings.sonarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.sonarr}]" else "undefined"}, radarr: process.env.CROSS_SEED_RADARR ? process.env.CROSS_SEED_RADARR.split(',') : ${if cfg.settings.radarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.radarr}]" else "undefined"}, host: process.env.CROSS_SEED_HOST || ${if cfg.settings.host != null then "'${cfg.settings.host}'" else "undefined"}, @@ -33,7 +33,7 @@ let flatLinking: process.env.CROSS_SEED_FLAT_LINKING ? process.env.CROSS_SEED_FLAT_LINKING === 'true' : ${if cfg.settings.flatLinking then "true" else "false"}, matchMode: process.env.CROSS_SEED_MATCH_MODE || "${cfg.settings.matchMode}", maxDataDepth: parseInt(process.env.CROSS_SEED_MAX_DATA_DEPTH) || ${toString cfg.settings.maxDataDepth}, - torrentDir: process.env.CROSS_SEED_TORRENT_DIR || "${cfg.settings.torrentDir}", + torrentDir: process.env.CROSS_SEED_TORRENT_DIR || ${if cfg.settings.torrentDir != null then "'${cfg.settings.torrentDir}'" else "undefined"}, outputDir: process.env.CROSS_SEED_OUTPUT_DIR || "${cfg.settings.outputDir}", includeSingleEpisodes: process.env.CROSS_SEED_INCLUDE_SINGLE_EPISODES ? process.env.CROSS_SEED_INCLUDE_SINGLE_EPISODES === 'true' : ${if cfg.settings.includeSingleEpisodes then "true" else "false"}, includeNonVideos: process.env.CROSS_SEED_INCLUDE_NON_VIDEOS ? process.env.CROSS_SEED_INCLUDE_NON_VIDEOS === 'true' : ${if cfg.settings.includeNonVideos then "true" else "false"}, @@ -144,7 +144,7 @@ in settings = { torznab = mkOption { - type = types.listOf types.str; + type = types.nullOr (types.listOf types.str); example = [ "http://localhost:9696/1/api?apikey=1234" ]; description = "Torznab URLs with apikey included"; }; @@ -193,7 +193,7 @@ in }; torrentDir = mkOption { - type = types.path; + type = types.nullOr types.path; example = "~/.local/share/qBittorrent/BT_backup"; description = "Directory containing .torrent files"; }; -- 2.49.0 From 75b5f1257f688a17b39a4f008ff69ef4bb0b29f7 Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Fri, 15 Nov 2024 00:59:01 +0200 Subject: [PATCH 5/9] Move js to value logic to mkOption --- modules/cross-seed.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix index 2dd020a..897ded9 100644 --- a/modules/cross-seed.nix +++ b/modules/cross-seed.nix @@ -14,8 +14,8 @@ let configFile = pkgs.writeText "config.js" '' module.exports = { - apiKey: process.env.CROSS_SEED_API_KEY || ${if cfg.settings.apiKey != null then "'${cfg.settings.apiKey}'" else "undefined"}, - torznab: process.env.CROSS_SEED_TORZNAB ? process.env.CROSS_SEED_TORZNAB.split(',') : ${if cfg.settings.torznab != null then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.torznab}]" else "undefined"}, + apiKey: process.env.CROSS_SEED_API_KEY || ${cfg.settings.apiKey}, + torznab: process.env.CROSS_SEED_TORZNAB ? process.env.CROSS_SEED_TORZNAB.split(',') : ${cfg.settings.torznab}, sonarr: process.env.CROSS_SEED_SONARR ? process.env.CROSS_SEED_SONARR.split(',') : ${if cfg.settings.sonarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.sonarr}]" else "undefined"}, radarr: process.env.CROSS_SEED_RADARR ? process.env.CROSS_SEED_RADARR.split(',') : ${if cfg.settings.radarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.radarr}]" else "undefined"}, host: process.env.CROSS_SEED_HOST || ${if cfg.settings.host != null then "'${cfg.settings.host}'" else "undefined"}, @@ -144,6 +144,7 @@ in settings = { torznab = mkOption { + apply = v: if v != null then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; type = types.nullOr (types.listOf types.str); example = [ "http://localhost:9696/1/api?apikey=1234" ]; description = "Torznab URLs with apikey included"; @@ -368,6 +369,7 @@ in }; apiKey = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; description = "Provide your own API key here to override the autogenerated one."; -- 2.49.0 From 135a9c0901b3633422c15044a9d29b6988c68539 Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Fri, 15 Nov 2024 01:05:52 +0200 Subject: [PATCH 6/9] Add missing defaults --- modules/cross-seed.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix index 897ded9..96ca35c 100644 --- a/modules/cross-seed.nix +++ b/modules/cross-seed.nix @@ -146,6 +146,7 @@ in torznab = mkOption { apply = v: if v != null then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; type = types.nullOr (types.listOf types.str); + default = null; example = [ "http://localhost:9696/1/api?apikey=1234" ]; description = "Torznab URLs with apikey included"; }; @@ -195,6 +196,7 @@ in torrentDir = mkOption { type = types.nullOr types.path; + default = null; example = "~/.local/share/qBittorrent/BT_backup"; description = "Directory containing .torrent files"; }; -- 2.49.0 From 834866eb5f6194d0f2283547f55fdbcd31df19e7 Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Fri, 15 Nov 2024 01:07:37 +0200 Subject: [PATCH 7/9] Remove link --- modules/cross-seed.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix index 96ca35c..a4d9fd6 100644 --- a/modules/cross-seed.nix +++ b/modules/cross-seed.nix @@ -401,6 +401,7 @@ in wantedBy = [ "multi-user.target" ]; preStart = '' mkdir -p ${cfg.settings.outputDir} + rm /var/lib/cross-seed/config.js || true ln -s ${configFile} /var/lib/cross-seed/config.js ''; -- 2.49.0 From 896e4d6ef746610e0b9e05ea889156026b01235b Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Fri, 15 Nov 2024 01:26:37 +0200 Subject: [PATCH 8/9] Clean up --- modules/cross-seed.nix | 62 ++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix index a4d9fd6..6e71e74 100644 --- a/modules/cross-seed.nix +++ b/modules/cross-seed.nix @@ -16,40 +16,42 @@ let module.exports = { apiKey: process.env.CROSS_SEED_API_KEY || ${cfg.settings.apiKey}, torznab: process.env.CROSS_SEED_TORZNAB ? process.env.CROSS_SEED_TORZNAB.split(',') : ${cfg.settings.torznab}, - sonarr: process.env.CROSS_SEED_SONARR ? process.env.CROSS_SEED_SONARR.split(',') : ${if cfg.settings.sonarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.sonarr}]" else "undefined"}, - radarr: process.env.CROSS_SEED_RADARR ? process.env.CROSS_SEED_RADARR.split(',') : ${if cfg.settings.radarr != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.radarr}]" else "undefined"}, - host: process.env.CROSS_SEED_HOST || ${if cfg.settings.host != null then "'${cfg.settings.host}'" else "undefined"}, + sonarr: process.env.CROSS_SEED_SONARR ? process.env.CROSS_SEED_SONARR.split(',') : ${cfg.settings.sonarr}, + radarr: process.env.CROSS_SEED_RADARR ? process.env.CROSS_SEED_RADARR.split(',') : ${cfg.settings.radarr}, + host: process.env.CROSS_SEED_HOST || ${cfg.settings.host}, port: parseInt(process.env.CROSS_SEED_PORT) || ${toString cfg.settings.port}, - notificationWebhookUrl: process.env.CROSS_SEED_NOTIFICATION_WEBHOOK_URL || ${if cfg.settings.notificationWebhookUrl != null then "'${cfg.settings.notificationWebhookUrl}'" else "undefined"}, - rtorrentRpcUrl: process.env.CROSS_SEED_RTORRENT_RPC_URL || ${if cfg.settings.rtorrentRpcUrl != null then "'${cfg.settings.rtorrentRpcUrl}'" else "undefined"}, - qbittorrentUrl: process.env.CROSS_SEED_QBITTORRENT_URL || ${if cfg.settings.qbittorrentUrl != null then "'${cfg.settings.qbittorrentUrl}'" else "undefined"}, - transmissionRpcUrl: process.env.CROSS_SEED_TRANSMISSION_RPC_URL || ${if cfg.settings.transmissionRpcUrl != null then "'${cfg.settings.transmissionRpcUrl}'" else "undefined"}, - delugeRpcUrl: process.env.CROSS_SEED_DELUGE_RPC_URL || ${if cfg.settings.delugeRpcUrl != null then "'${cfg.settings.delugeRpcUrl}'" else "undefined"}, + notificationWebhookUrl: process.env.CROSS_SEED_NOTIFICATION_WEBHOOK_URL || ${cfg.settings.notificationWebhookUrl}, + rtorrentRpcUrl: process.env.CROSS_SEED_RTORRENT_RPC_URL || ${cfg.settings.rtorrentRpcUrl}, + qbittorrentUrl: process.env.CROSS_SEED_QBITTORRENT_URL || ${cfg.settings.qbittorrentUrl}, + transmissionRpcUrl: process.env.CROSS_SEED_TRANSMISSION_RPC_URL || ${cfg.settings.transmissionRpcUrl}, + delugeRpcUrl: process.env.CROSS_SEED_DELUGE_RPC_URL || ${cfg.settings.delugeRpcUrl}, delay: parseInt(process.env.CROSS_SEED_DELAY) || ${toString cfg.settings.delay}, dataDirs: process.env.CROSS_SEED_DATA_DIRS ? process.env.CROSS_SEED_DATA_DIRS.split(',') : ${if cfg.settings.dataDirs != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.dataDirs}]" else "[]"}, linkCategory: process.env.CROSS_SEED_LINK_CATEGORY || "${cfg.settings.linkCategory}", linkDir: process.env.CROSS_SEED_LINK_DIR || ${if cfg.settings.linkDir != null then "'${cfg.settings.linkDir}'" else "undefined"}, linkType: process.env.CROSS_SEED_LINK_TYPE || "${cfg.settings.linkType}", - flatLinking: process.env.CROSS_SEED_FLAT_LINKING ? process.env.CROSS_SEED_FLAT_LINKING === 'true' : ${if cfg.settings.flatLinking then "true" else "false"}, + flatLinking: process.env.CROSS_SEED_FLAT_LINKING ? process.env.CROSS_SEED_FLAT_LINKING === 'true' : ${cfg.settings.flatLinking}, matchMode: process.env.CROSS_SEED_MATCH_MODE || "${cfg.settings.matchMode}", maxDataDepth: parseInt(process.env.CROSS_SEED_MAX_DATA_DEPTH) || ${toString cfg.settings.maxDataDepth}, - torrentDir: process.env.CROSS_SEED_TORRENT_DIR || ${if cfg.settings.torrentDir != null then "'${cfg.settings.torrentDir}'" else "undefined"}, + torrentDir: process.env.CROSS_SEED_TORRENT_DIR || ${cfg.settings.torrentDir}, outputDir: process.env.CROSS_SEED_OUTPUT_DIR || "${cfg.settings.outputDir}", - includeSingleEpisodes: process.env.CROSS_SEED_INCLUDE_SINGLE_EPISODES ? process.env.CROSS_SEED_INCLUDE_SINGLE_EPISODES === 'true' : ${if cfg.settings.includeSingleEpisodes then "true" else "false"}, - includeNonVideos: process.env.CROSS_SEED_INCLUDE_NON_VIDEOS ? process.env.CROSS_SEED_INCLUDE_NON_VIDEOS === 'true' : ${if cfg.settings.includeNonVideos then "true" else "false"}, + includeSingleEpisodes: process.env.CROSS_SEED_INCLUDE_SINGLE_EPISODES ? process.env.CROSS_SEED_INCLUDE_SINGLE_EPISODES === 'true' : ${cfg.settings.includeSingleEpisodes}, + includeNonVideos: process.env.CROSS_SEED_INCLUDE_NON_VIDEOS ? process.env.CROSS_SEED_INCLUDE_NON_VIDEOS === 'true' : ${cfg.settings.includeNonVideos}, fuzzySizeThreshold: parseFloat(process.env.CROSS_SEED_FUZZY_SIZE_THRESHOLD) || ${toString cfg.settings.fuzzySizeThreshold}, - excludeOlder: process.env.CROSS_SEED_EXCLUDE_OLDER || ${if cfg.settings.excludeOlder != null then "'${cfg.settings.excludeOlder}'" else "undefined"}, - excludeRecentSearch: process.env.CROSS_SEED_EXCLUDE_RECENT_SEARCH || ${if cfg.settings.excludeRecentSearch != null then "'${cfg.settings.excludeRecentSearch}'" else "undefined"}, + excludeOlder: process.env.CROSS_SEED_EXCLUDE_OLDER || ${cfg.settings.excludeOlder}, + excludeRecentSearch: process.env.CROSS_SEED_EXCLUDE_RECENT_SEARCH || ${cfg.settings.excludeRecentSearch}, action: process.env.CROSS_SEED_ACTION || "${cfg.settings.action}", - duplicateCategories: process.env.CROSS_SEED_DUPLICATE_CATEGORIES ? process.env.CROSS_SEED_DUPLICATE_CATEGORIES === 'true' : ${if cfg.settings.duplicateCategories then "true" else "false"}, - rssCadence: process.env.CROSS_SEED_RSS_CADENCE || ${if cfg.settings.rssCadence != null then "'${cfg.settings.rssCadence}'" else "undefined"}, - searchCadence: process.env.CROSS_SEED_SEARCH_CADENCE || ${if cfg.settings.searchCadence != null then "'${cfg.settings.searchCadence}'" else "undefined"}, - snatchTimeout: process.env.CROSS_SEED_SNATCH_TIMEOUT || ${if cfg.settings.snatchTimeout != null then "'${cfg.settings.snatchTimeout}'" else "null"}, - searchTimeout: process.env.CROSS_SEED_SEARCH_TIMEOUT || ${if cfg.settings.searchTimeout != null then "'${cfg.settings.searchTimeout}'" else "null"}, + duplicateCategories: process.env.CROSS_SEED_DUPLICATE_CATEGORIES ? process.env.CROSS_SEED_DUPLICATE_CATEGORIES === 'true' : ${cfg.settings.duplicateCategories}, + rssCadence: process.env.CROSS_SEED_RSS_CADENCE || ${cfg.settings.rssCadence}, + searchCadence: process.env.CROSS_SEED_SEARCH_CADENCE || ${cfg.settings.searchCadence}, + snatchTimeout: process.env.CROSS_SEED_SNATCH_TIMEOUT || ${cfg.settings.snatchTimeout}, + searchTimeout: process.env.CROSS_SEED_SEARCH_TIMEOUT || ${cfg.settings.searchTimeout}, searchLimit: parseInt(process.env.CROSS_SEED_SEARCH_LIMIT) || ${toString cfg.settings.searchLimit}, - blockList: process.env.CROSS_SEED_BLOCK_LIST ? process.env.CROSS_SEED_BLOCK_LIST.split(',') : ${if cfg.settings.blockList != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.blockList}]" else "undefined"}, + blockList: process.env.CROSS_SEED_BLOCK_LIST ? process.env.CROSS_SEED_BLOCK_LIST.split(',') : ${cfg.settings.blockList}, }; ''; + + jsBool = v: if v then "true" else "false"; in { options.services.cross-seed = { @@ -177,6 +179,7 @@ in }; flatLinking = mkOption { + apply = jsBool; type = types.bool; default = false; description = "Use flat linking directory structure"; @@ -195,6 +198,7 @@ in }; torrentDir = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.path; default = null; example = "~/.local/share/qBittorrent/BT_backup"; @@ -209,12 +213,14 @@ in }; includeNonVideos = mkOption { + apply = jsBool; type = types.bool; default = false; description = "Include torrents which contain non-video files"; }; includeSingleEpisodes = mkOption { + apply = jsBool; type = types.bool; default = false; description = "Include single episode torrents in the search"; @@ -227,6 +233,7 @@ in }; excludeOlder = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; example = "2 weeks"; @@ -234,6 +241,7 @@ in }; excludeRecentSearch = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; example = "3 days"; @@ -253,6 +261,7 @@ in }; rtorrentRpcUrl = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; example = "http://username:password@localhost:1234/RPC2"; @@ -260,6 +269,7 @@ in }; qbittorrentUrl = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; example = "http://username:password@localhost:8080"; @@ -267,6 +277,7 @@ in }; transmissionRpcUrl = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; example = "http://username:password@localhost:9091/transmission/rpc"; @@ -274,6 +285,7 @@ in }; delugeRpcUrl = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; example = "http://:password@localhost:8112/json"; @@ -281,12 +293,14 @@ in }; duplicateCategories = mkOption { + apply = jsBool; type = types.bool; default = false; description = "Create and inject using categories with same save paths"; }; notificationWebhookUrl = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; description = "Webhook URL for notifications. Conforms to the caronc/apprise REST api."; @@ -299,12 +313,14 @@ in }; snatchTimeout = mkOption { + apply = v: if v != null then "'${v}'" else "null"; type = types.str; default = "30 seconds"; description = "Timeout for unresponsive snatches"; }; searchTimeout = mkOption { + apply = v: if v != null then "'${v}'" else "null"; type = types.str; default = "30 seconds"; description = "Timeout for unresponsive searches"; @@ -317,6 +333,7 @@ in }; blockList = mkOption { + apply = v: if v != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; type = types.listOf types.str; default = [ ]; example = [ @@ -330,6 +347,7 @@ in }; sonarr = mkOption { + apply = v: if v != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; type = types.listOf types.str; default = [ ]; example = [ "http://sonarr:8989/?apikey=12345" ]; @@ -337,6 +355,7 @@ in }; radarr = mkOption { + apply = v: if v != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; type = types.listOf types.str; default = [ ]; example = [ "http://radarr:7878/?apikey=12345" ]; @@ -350,6 +369,7 @@ in }; host = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = "0.0.0.0"; example = "127.0.0.1"; @@ -357,6 +377,7 @@ in }; searchCadence = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; example = "1 day"; @@ -364,6 +385,7 @@ in }; rssCadence = mkOption { + apply = v: if v != null then "'${v}'" else "undefined"; type = types.nullOr types.str; default = null; example = "30 minutes"; -- 2.49.0 From c454f2d312e712d98df3ef2edad11707d17986a5 Mon Sep 17 00:00:00 2001 From: batteredbunny Date: Fri, 15 Nov 2024 01:39:18 +0200 Subject: [PATCH 9/9] More clean up --- modules/cross-seed.nix | 43 +++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/modules/cross-seed.nix b/modules/cross-seed.nix index 6e71e74..e341f3b 100644 --- a/modules/cross-seed.nix +++ b/modules/cross-seed.nix @@ -26,9 +26,9 @@ let transmissionRpcUrl: process.env.CROSS_SEED_TRANSMISSION_RPC_URL || ${cfg.settings.transmissionRpcUrl}, delugeRpcUrl: process.env.CROSS_SEED_DELUGE_RPC_URL || ${cfg.settings.delugeRpcUrl}, delay: parseInt(process.env.CROSS_SEED_DELAY) || ${toString cfg.settings.delay}, - dataDirs: process.env.CROSS_SEED_DATA_DIRS ? process.env.CROSS_SEED_DATA_DIRS.split(',') : ${if cfg.settings.dataDirs != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") cfg.settings.dataDirs}]" else "[]"}, + dataDirs: process.env.CROSS_SEED_DATA_DIRS ? process.env.CROSS_SEED_DATA_DIRS.split(',') : ${cfg.settings.dataDirs}, linkCategory: process.env.CROSS_SEED_LINK_CATEGORY || "${cfg.settings.linkCategory}", - linkDir: process.env.CROSS_SEED_LINK_DIR || ${if cfg.settings.linkDir != null then "'${cfg.settings.linkDir}'" else "undefined"}, + linkDir: process.env.CROSS_SEED_LINK_DIR || ${cfg.settings.linkDir}, linkType: process.env.CROSS_SEED_LINK_TYPE || "${cfg.settings.linkType}", flatLinking: process.env.CROSS_SEED_FLAT_LINKING ? process.env.CROSS_SEED_FLAT_LINKING === 'true' : ${cfg.settings.flatLinking}, matchMode: process.env.CROSS_SEED_MATCH_MODE || "${cfg.settings.matchMode}", @@ -52,6 +52,9 @@ let ''; jsBool = v: if v then "true" else "false"; + jsStrUndefined = v: if v != null then "'${v}'" else "undefined"; + jsStrNull = v: if v != null then "'${v}'" else "null"; + jsListStrUndefined = v: if v != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; in { options.services.cross-seed = { @@ -154,6 +157,7 @@ in }; dataDirs = mkOption { + apply = v: if v != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "[]"; type = types.listOf types.path; default = [ ]; example = [ "/downloads/movies" "/downloads/packs" ]; @@ -173,6 +177,7 @@ in }; linkDir = mkOption { + apply = jsStrUndefined; 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."; @@ -198,7 +203,7 @@ in }; torrentDir = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.path; default = null; example = "~/.local/share/qBittorrent/BT_backup"; @@ -233,7 +238,7 @@ in }; excludeOlder = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; example = "2 weeks"; @@ -241,7 +246,7 @@ in }; excludeRecentSearch = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; example = "3 days"; @@ -261,7 +266,7 @@ in }; rtorrentRpcUrl = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; example = "http://username:password@localhost:1234/RPC2"; @@ -269,7 +274,7 @@ in }; qbittorrentUrl = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; example = "http://username:password@localhost:8080"; @@ -277,7 +282,7 @@ in }; transmissionRpcUrl = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; example = "http://username:password@localhost:9091/transmission/rpc"; @@ -285,7 +290,7 @@ in }; delugeRpcUrl = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; example = "http://:password@localhost:8112/json"; @@ -300,7 +305,7 @@ in }; notificationWebhookUrl = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; description = "Webhook URL for notifications. Conforms to the caronc/apprise REST api."; @@ -313,14 +318,14 @@ in }; snatchTimeout = mkOption { - apply = v: if v != null then "'${v}'" else "null"; + apply = jsStrNull; type = types.str; default = "30 seconds"; description = "Timeout for unresponsive snatches"; }; searchTimeout = mkOption { - apply = v: if v != null then "'${v}'" else "null"; + apply = jsStrNull; type = types.str; default = "30 seconds"; description = "Timeout for unresponsive searches"; @@ -333,7 +338,7 @@ in }; blockList = mkOption { - apply = v: if v != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; + apply = jsListStrUndefined; type = types.listOf types.str; default = [ ]; example = [ @@ -347,7 +352,7 @@ in }; sonarr = mkOption { - apply = v: if v != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; + apply = jsListStrUndefined; type = types.listOf types.str; default = [ ]; example = [ "http://sonarr:8989/?apikey=12345" ]; @@ -355,7 +360,7 @@ in }; radarr = mkOption { - apply = v: if v != [] then "[${lib.concatMapStringsSep ", " (x: "'${x}'") v}]" else "undefined"; + apply = jsListStrUndefined; type = types.listOf types.str; default = [ ]; example = [ "http://radarr:7878/?apikey=12345" ]; @@ -369,7 +374,7 @@ in }; host = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = "0.0.0.0"; example = "127.0.0.1"; @@ -377,7 +382,7 @@ in }; searchCadence = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; example = "1 day"; @@ -385,7 +390,7 @@ in }; rssCadence = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; example = "30 minutes"; @@ -393,7 +398,7 @@ in }; apiKey = mkOption { - apply = v: if v != null then "'${v}'" else "undefined"; + apply = jsStrUndefined; type = types.nullOr types.str; default = null; description = "Provide your own API key here to override the autogenerated one."; -- 2.49.0