diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 70ac9882958b..0e27de7945bf 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1728,6 +1728,7 @@ ./services/web-apps/suwayomi-server.nix ./services/web-apps/szurubooru.nix ./services/web-apps/trilium.nix + ./services/web-apps/tuliprox.nix ./services/web-apps/umami.nix ./services/web-apps/vikunja.nix ./services/web-apps/wakapi.nix diff --git a/nixos/modules/services/web-apps/tuliprox.nix b/nixos/modules/services/web-apps/tuliprox.nix new file mode 100644 index 000000000000..f7b82ca9b3f6 --- /dev/null +++ b/nixos/modules/services/web-apps/tuliprox.nix @@ -0,0 +1,336 @@ +{ + config, + pkgs, + lib, + utils, + ... +}: +let + cfg = config.services.tuliprox; + settingsFormat = pkgs.formats.yaml { }; + systemSettingsYaml = settingsFormat.generate "config.yml" cfg.systemSettings; + sourceSettingsYaml = settingsFormat.generate "source.yml" cfg.sourceSettings; + apiProxySettingsYaml = settingsFormat.generate "api-proxy.yml" cfg.apiProxySettings; + mappingSettingsYaml = settingsFormat.generate "mapping.yml" cfg.mappingSettings; +in +{ + meta.maintainers = with lib.maintainers; [ nyanloutre ]; + + options.services.tuliprox = { + enable = lib.mkEnableOption "Tuliprox IPTV playlist processor & proxy"; + + package = lib.mkPackageOption pkgs "tuliprox" { }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Additional command-line arguments for the systemd service. + + Refer to the [Tuliprox documentation] for available arguments. + + [Tuliprox documentation]: https://github.com/euzu/tuliprox?tab=readme-ov-file#command-line-arguments + ''; + }; + + systemSettings = lib.mkOption { + type = settingsFormat.type; + example = { + api = { + host = "0.0.0.0"; + port = 8901; + }; + }; + description = '' + Main config file + + Refer to the [Tuliprox documentation] for available attributes + + [Tuliprox documentation]: https://github.com/euzu/tuliprox?tab=readme-ov-file#1-configyml + ''; + }; + + sourceSettings = lib.mkOption { + type = settingsFormat.type; + example = { + templates = [ + { + name = "not_red_button"; + value = "NOT (Title ~ \"(?i).*red button.*\")"; + } + { + name = "not_low_resolution"; + value = "NOT (Title ~ \"(?i).*\(360p|240p\).*\")"; + } + { + name = "all_channels"; + value = "Title ~ \".*\""; + } + { + name = "final_channel_lineup"; + value = "!all_channels! AND !not_red_button! AND !not_low_resolution!"; + } + ]; + sources = [ + { + inputs = [ + { + name = "iptv-org"; + type = "m3u"; + url = "https://iptv-org.github.io/iptv/countries/uk.m3u"; + } + ]; + targets = [ + { + name = "iptv-org"; + output = [ + { + type = "xtream"; + } + { + type = "m3u"; + filename = "iptv.m3u"; + } + { + type = "hdhomerun"; + username = "local"; + device = "hdhr1"; + } + ]; + filter = "!final_channel_lineup!"; + options = { + ignore_logo = false; + share_live_streams = true; + }; + mapping = [ + "iptv-org" + ]; + } + ]; + } + ]; + }; + description = '' + Source definitions + + Refer to the [Tuliprox documentation] for available attributes + + [Tuliprox documentation]: https://github.com/euzu/tuliprox?tab=readme-ov-file#2-sourceyml + ''; + }; + + apiProxySettings = lib.mkOption { + type = settingsFormat.type; + example = { + server = [ + { + name = "default"; + protocol = "http"; + host = "192.169.1.9"; + port = 8901; + timezone = "Europe/Paris"; + message = "Welcome to tuliprox"; + } + { + name = "external"; + protocol = "https"; + host = "tuliprox.mydomain.tv"; + port = 443; + timezone = "Europe/Paris"; + message = "Welcome to tuliprox"; + } + ]; + user = [ + { + target = "xc_m3u"; + credentials = [ + { + username = "test1"; + password = "secret1"; + token = "token1"; + proxy = "reverse"; + server = "default"; + exp_date = 1672705545; + max_connections = 1; + status = "Active"; + } + ]; + } + ]; + }; + description = '' + Users and proxy configuration + + Refer to the [Tuliprox documentation] for available attributes + + [Tuliprox documentation]: https://github.com/euzu/tuliprox?tab=readme-ov-file#3-api-proxy-config + ''; + }; + + mappingSettings = lib.mkOption { + type = settingsFormat.type; + example = { + mappings = { + templates = [ + { + name = "bbc"; + value = "Title ~ \"^BBC\""; + } + { + name = "documentary"; + value = "(Group ~ \"(Documentary|Outdoor)\")"; + } + { + name = "entertainment"; + value = "Group ~ \"Entertainment\""; + } + { + name = "pluto_tv"; + value = "(Caption ~ \"Pluto TV\") AND NOT(Caption ~ \"Sports\")"; + } + { + name = "business"; + value = "Group ~ \"Business\""; + } + ]; + mapping = [ + { + id = "iptv-org"; + match_as_ascii = true; + mapper = [ + { + filter = "!bbc!"; + script = '' + @Group = "BBC" + ''; + } + { + filter = "!documentary!"; + script = '' + @Group = "Documentary" + ''; + } + { + filter = "!entertainment!"; + script = '' + @Group = "Entertainment" + ''; + } + { + filter = "!pluto_tv!"; + script = '' + @Group = "Pluto TV" + ''; + } + { + filter = "!business!"; + script = '' + @Group = "News" + ''; + } + { + filter = "Input ~ \"iptv-org\""; + script = '' + @Caption = concat(@Caption, " (iptv-org)") + ''; + } + ]; + } + ]; + }; + }; + description = '' + Templates configuration + + Refer to the [Tuliprox documentation] for available attributes + + [Tuliprox documentation]: https://github.com/euzu/tuliprox?tab=readme-ov-file#2-mappingyml + ''; + }; + }; + + config = lib.mkIf cfg.enable { + services.tuliprox.systemSettings = { + api = { + host = lib.mkDefault "127.0.0.1"; + port = lib.mkDefault 8901; + web_root = lib.mkDefault "${cfg.package}/web"; + }; + working_dir = lib.mkDefault "\${env:STATE_DIRECTORY}/data"; + backup_dir = lib.mkDefault "\${env:STATE_DIRECTORY}/backup"; + custom_stream_response_path = lib.mkDefault "${cfg.package}/resources"; + }; + services.tuliprox.sourceSettings.sources = lib.mkDefault [ ]; + services.tuliprox.apiProxySettings = { + server = lib.mkDefault [ + { + name = "default"; + protocol = "http"; + host = cfg.systemSettings.api.host; + port = cfg.systemSettings.api.port; + timezone = if config.time.timeZone != null then config.time.timeZone else "Etc/UTC"; + message = "Welcome to tuliprox"; + } + ]; + user = lib.mkDefault [ ]; + }; + services.tuliprox.mappingSettings.mappings.mapping = lib.mkDefault [ ]; + + systemd.services.tuliprox = { + description = "Tuliprox server"; + + serviceConfig = { + ExecStart = utils.escapeSystemdExecArgs ( + [ + (lib.getExe cfg.package) + "--server" + "--config" + systemSettingsYaml + "--source" + sourceSettingsYaml + "--api-proxy" + apiProxySettingsYaml + "--mapping" + mappingSettingsYaml + ] + ++ cfg.extraArgs + ); + Restart = "always"; + + DynamicUser = true; + StateDirectory = "tuliprox"; + + CapabilityBoundingSet = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + ]; + UMask = "0066"; + }; + + wantedBy = [ "multi-user.target" ]; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index c43a410170a4..c26c0d48644c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1582,6 +1582,7 @@ in trilium-server = runTestOn [ "x86_64-linux" ] ./trilium-server.nix; tsm-client-gui = runTest ./tsm-client-gui.nix; ttyd = runTest ./web-servers/ttyd.nix; + tuliprox = runTest ./tuliprox.nix; tuned = runTest ./tuned.nix; tuptime = runTest ./tuptime.nix; turbovnc-headless-server = runTest ./turbovnc-headless-server.nix; diff --git a/nixos/tests/tuliprox.nix b/nixos/tests/tuliprox.nix new file mode 100644 index 000000000000..425b06e0fd83 --- /dev/null +++ b/nixos/tests/tuliprox.nix @@ -0,0 +1,19 @@ +{ lib, pkgs, ... }: +{ + name = "tuliprox"; + meta.maintainers = with lib.maintainers; [ nyanloutre ]; + + nodes.machine = + { pkgs, ... }: + { + services.tuliprox = { + enable = true; + }; + }; + + testScript = '' + machine.wait_for_unit("multi-user.target") + machine.wait_for_open_port(8901) + machine.succeed("curl -v -s http://127.0.0.1:8901/") + ''; +} diff --git a/pkgs/by-name/tu/tuliprox/package.nix b/pkgs/by-name/tu/tuliprox/package.nix new file mode 100644 index 000000000000..5a1f1e47c853 --- /dev/null +++ b/pkgs/by-name/tu/tuliprox/package.nix @@ -0,0 +1,84 @@ +{ + lib, + fetchFromGitHub, + rustPlatform, + pkg-config, + openssl, + ffmpeg, + which, + rustc, + wasm-bindgen-cli_0_2_104, + trunk, + binaryen, + dart-sass, + nixosTests, + nix-update-script, +}: + +rustPlatform.buildRustPackage rec { + pname = "tuliprox"; + version = "3.2.0"; + + src = fetchFromGitHub { + owner = "euzu"; + repo = "tuliprox"; + tag = "v${version}"; + hash = "sha256-G+bVKBAxviyJShq2BG4vjMiTzHhoYaiP6FXrSWeTvkU="; + }; + + nativeBuildInputs = [ + pkg-config + ffmpeg + which + wasm-bindgen-cli_0_2_104 + trunk + rustc.llvmPackages.lld + binaryen + dart-sass + ]; + + # Needed to get openssl-sys to use pkg-config. + env = { + OPENSSL_NO_VENDOR = 1; + OPENSSL_LIB_DIR = "${lib.getLib openssl}/lib"; + OPENSSL_DIR = "${lib.getDev openssl}"; + }; + + cargoHash = "sha256-bDQ4pDDTINTgotTen1/SxOZBmkUmbmmwmR4/nSoSf/A="; + + cargoBuildFlags = "--package tuliprox"; + + postBuild = '' + patchShebangs ./bin/build_resources.sh + ./bin/build_resources.sh + pushd frontend + trunk build --offline --frozen --release + popd + ''; + + checkFlags = [ + "--skip=processing::parser::xmltv::tests::normalize" + "--skip=processing::parser::xtream::tests::test_read_json_file_into_struct" + "--skip=repository::indexed_document::tests::test_read_xt" + ]; + + postInstall = '' + cp -rf frontend/dist $out/web + mkdir -p $out/resources + cp -rf resources/*.ts $out/resources + ''; + + passthru = { + tests = { inherit (nixosTests) tuliprox; }; + updateScript = nix-update-script { }; + }; + + meta = { + description = "Flexible IPTV playlist processor & proxy in Rust"; + homepage = "https://github.com/euzu/tuliprox"; + changelog = "https://github.com/euzu/tuliprox/blob/${src.tag}/CHANGELOG.md"; + mainProgram = "tuliprox"; + license = with lib.licenses; [ mit ]; + maintainers = with lib.maintainers; [ nyanloutre ]; + }; +}