From 650f1cad929b743e8ddfdef3b1e1ff474cf433e8 Mon Sep 17 00:00:00 2001 From: Emanuel Johnson Godin Date: Wed, 28 Aug 2024 20:58:46 +0200 Subject: [PATCH] Add Nix (#177) * add Nix support * fix formatter output * mention Nix in README * fix common import * fix frontend old version import * clarified flake pkgs order * rm old dataDir option * comment typo * fix password assertion * rm old User/Group logic * rewrite assertion boolean expr * General flake touchup - Rewrite `callPackage` exprs to be more readable - Add pre-commit support for devShell - Add direnv support * add simple test * use correct test func --- .envrc | 1 + .gitignore | 4 + README.md | 4 + env.nix | 4 - flake.lock | 149 +++++++++++++++++++++++++++++ flake.nix | 51 ++++++++++ nix/common.nix | 9 ++ nix/devShell.nix | 9 ++ nix/frontend.nix | 37 ++++++++ nix/module.nix | 215 ++++++++++++++++++++++++++++++++++++++++++ nix/server.nix | 52 ++++++++++ nix/tests/default.nix | 20 ++++ 12 files changed, 551 insertions(+), 4 deletions(-) create mode 100644 .envrc delete mode 100644 env.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/common.nix create mode 100644 nix/devShell.nix create mode 100644 nix/frontend.nix create mode 100644 nix/module.nix create mode 100644 nix/server.nix create mode 100644 nix/tests/default.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index ec9b1ec..912c21d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.pre-commit-config.yaml +.direnv/ +result/ +result dist .pnpm-debug.log node_modules diff --git a/README.md b/README.md index e4f8d66..6c07f3d 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,10 @@ It is **planned** to also expose a **gRPC** server. For more information open an issue on GitHub and I will provide more info ASAP. +## Nix +This repo adds support for Nix(OS) in various ways through a `flake-parts` flake. +For more info, please refer to the [official documentation](https://nixos.org/learn/). + ## What yt-dlp-webui is not `yt-dlp-webui` isn't your ordinary website where to download stuff from the internet, so don't try asking for links of where this is hosted. It's a self hosted platform for a Linux NAS. diff --git a/env.nix b/env.nix deleted file mode 100644 index c7adca3..0000000 --- a/env.nix +++ /dev/null @@ -1,4 +0,0 @@ -{ pkgs ? import {} }: - pkgs.mkShell { - nativeBuildInputs = with pkgs.buildPackages; [ yt-dlp nodejs_22 yarn-berry go ]; -} \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1e7e43d --- /dev/null +++ b/flake.lock @@ -0,0 +1,149 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1722555600, + "narHash": "sha256-XOQkdLafnb/p9ij77byFQjDf5m5QYl9b2REiVClC+x4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "8471fe90ad337a8074e957b69ca4d0089218391d", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1723637854, + "narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1722555339, + "narHash": "sha256-uFf2QeW7eAHlYXuDktm9c25OxOyCoUOQmh5SZ9amE5Q=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/a5d394176e64ab29c852d03346c1fc9b0b7d33eb.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/a5d394176e64ab29c852d03346c1fc9b0b7d33eb.tar.gz" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1720386169, + "narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "194846768975b7ad2c4988bdb82572c00222c0d7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1719082008, + "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9693852a2070b398ee123a329e68f0dab5526681", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks-nix": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs_2", + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1723803910, + "narHash": "sha256-yezvUuFiEnCFbGuwj/bQcqg7RykIEqudOy/RBrId0pc=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "bfef0ada09e2c8ac55bbcd0831bd0c9d42e651ba", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "pre-commit-hooks-nix": "pre-commit-hooks-nix" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..64e85dd --- /dev/null +++ b/flake.nix @@ -0,0 +1,51 @@ +{ + description = "A terrible web ui for yt-dlp. Designed to be self-hosted."; + + inputs = { + flake-parts.url = "github:hercules-ci/flake-parts"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + pre-commit-hooks-nix.url = "github:cachix/pre-commit-hooks.nix"; + }; + + outputs = inputs@{ self, flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ + inputs.pre-commit-hooks-nix.flakeModule + ]; + systems = [ + "x86_64-linux" + ]; + perSystem = { config, self', pkgs, ... }: { + + packages = { + yt-dlp-web-ui-frontend = pkgs.callPackage ./nix/frontend.nix { }; + default = pkgs.callPackage ./nix/server.nix { + inherit (self'.packages) yt-dlp-web-ui-frontend; + }; + }; + + checks = import ./nix/tests { inherit self pkgs; }; + + pre-commit = { + check.enable = true; + settings = { + hooks = { + ${self'.formatter.pname}.enable = true; + deadnix.enable = true; + nil.enable = true; + statix.enable = true; + }; + }; + }; + + devShells.default = pkgs.callPackage ./nix/devShell.nix { + inputsFrom = [ config.pre-commit.devShell ]; + }; + + formatter = pkgs.nixpkgs-fmt; + }; + flake = { + nixosModules.default = import ./nix/module.nix self.packages; + }; + }; +} diff --git a/nix/common.nix b/nix/common.nix new file mode 100644 index 0000000..f0a67ae --- /dev/null +++ b/nix/common.nix @@ -0,0 +1,9 @@ +{ lib }: { + version = "v3.1.2"; + meta = { + description = "A terrible web ui for yt-dlp. Designed to be self-hosted."; + homepage = "https://github.com/marcopeocchi/yt-dlp-web-ui"; + license = lib.licenses.mpl20; + }; +} + diff --git a/nix/devShell.nix b/nix/devShell.nix new file mode 100644 index 0000000..8bb9d96 --- /dev/null +++ b/nix/devShell.nix @@ -0,0 +1,9 @@ +{ inputsFrom ? [ ], mkShell, yt-dlp, nodejs, go }: +mkShell { + inherit inputsFrom; + packages = [ + yt-dlp + nodejs + go + ]; +} diff --git a/nix/frontend.nix b/nix/frontend.nix new file mode 100644 index 0000000..17fc437 --- /dev/null +++ b/nix/frontend.nix @@ -0,0 +1,37 @@ +{ lib +, stdenv +, nodejs +, pnpm +}: +let common = import ./common.nix { inherit lib; }; in +stdenv.mkDerivation (finalAttrs: { + pname = "yt-dlp-web-ui-frontend"; + + inherit (common) version; + + src = lib.fileset.toSource { + root = ../frontend; + fileset = ../frontend; + }; + + buildPhase = '' + npm run build + ''; + + installPhase = '' + mkdir -p $out/dist + cp -r dist/* $out/dist + ''; + + nativeBuildInputs = [ + nodejs + pnpm.configHook + ]; + + pnpmDeps = pnpm.fetchDeps { + inherit (finalAttrs) pname version src; + hash = "sha256-NvXNDXkuoJ4vGeQA3bOhhc+KLBfke593qK0edcvzWTo="; + }; + + inherit (common) meta; +}) diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..0e2776d --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,215 @@ +packages: { config, lib, pkgs, ... }: +let + cfg = config.services.yt-dlp-web-ui; + inherit (pkgs.stdenv.hostPlatform) system; + pkg = packages.${system}.default; +in +{ + /* + Some notes on the module design: + - Usually, you don't map out all of the options like this in attrsets, + but due to the software's nonstandard "config file overrides CLI" behavior, + we don't want to expose a config file catchall, and as such don't use '-conf'. + + - Notably, '-driver' is missing as a configuration option. + This should instead be customized with idiomatic Nix, overriding 'cfg.package' with + the desired yt-dlp package. + + - The systemd service has been sandboxed as much as possible. This restricts configuration of + data and logs dir. If you really need a custom data and logs dir, use BindPaths (man systemd.exec) + */ + options.services.yt-dlp-web-ui = { + enable = lib.mkEnableOption "yt-dlp-web-ui"; + package = lib.mkOption { + type = lib.types.package; + default = pkg; + defaultText = lib.literalMD "`packages.default` from the yt-dlp-web-ui flake."; + description = '' + The yt-dlp-web-ui package to use. + ''; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "yt-dlp-web-ui"; + description = lib.mdDoc '' + User under which yt-dlp-web-ui runs. + ''; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "yt-dlp-web-ui"; + description = lib.mdDoc '' + Group under which yt-dlp-web-ui runs. + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Whether to open the TCP port in the firewall. + ''; + }; + + host = lib.mkOption { + default = "0.0.0.0"; + type = lib.types.str; + description = lib.mdDoc '' + Host where yt-dlp-web-ui will listen at. + ''; + }; + + port = lib.mkOption { + default = 3033; + type = lib.types.port; + description = lib.mdDoc '' + Port where yt-dlp-web-ui will listen at. + ''; + }; + + downloadDir = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + The directory where yt-dlp-web-ui stores downloads. + ''; + }; + + queueSize = lib.mkOption { + default = 2; + type = lib.types.ints.unsigned; # >= 0 + description = lib.mdDoc '' + Queue size (concurrent downloads). + ''; + }; + + logging = lib.mkEnableOption "logging"; + + rpcAuth = lib.mkOption { + description = lib.mdDoc '' + RPC Authentication settings. + ''; + default = { }; + type = lib.types.submodule { + options = { + enable = lib.mkEnableOption "RPC authentication"; + user = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + Username required for auth. + ''; + }; + passwordFile = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = lib.mdDoc '' + Path to the file containing the password required for auth. + ''; + }; + insecurePasswordText = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = lib.mdDoc '' + Raw password required for auth. + + It's strongly recommended to use 'passwordFile' instead of this option. + + **Don't use this option unless you know what you're doing!**. + It writes the password to the world-readable Nix store, which is a big security risk. + More info: https://wiki.nixos.org/wiki/Comparison_of_secret_managing_schemes + ''; + }; + }; + }; + }; + + }; + config = lib.mkIf cfg.enable { + assertions = [ + (lib.mkIf cfg.rpcAuth.enable { + assertion = lib.xor (cfg.rpcAuth.passwordFile == null) (cfg.rpcAuth.insecurePasswordText == null); + message = '' + RPC Auth is enabled for yt-dlp-web-ui! Exactly one RPC auth password source must be set! + + Tip: You should set 'services.yt-dlp-web-ui.rpcAuth.passwordfile'! + ''; + }) + ]; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; + + users.users = lib.mkIf (cfg.user == "yt-dlp-web-ui") { + yt-dlp-web-ui = { + inherit (cfg) group; + isSystemUser = true; + }; + }; + + users.groups = lib.mkIf (cfg.group == "yt-dlp-web-ui") { yt-dlp-web-ui = { }; }; + + systemd.services.yt-dlp-web-ui = { + description = "yt-dlp-web-ui system service"; + after = [ "network.target" ]; + path = [ cfg.package pkgs.tree ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = + rec { + ExecStart = + let + password = + if cfg.rpcAuth.passwordFile == null + then cfg.rpcAuth.insecurePasswordText + else "$(cat ${cfg.rpcAuth.passwordFile})"; + args = [ + "-host ${cfg.host}" + "-port ${builtins.toString cfg.port}" + ''-out "${cfg.downloadDir}"'' + "-qs ${builtins.toString cfg.queueSize}" + ] ++ (lib.optionals cfg.logging [ + "-fl" + ''-lf "/var/log/${LogsDirectory}/yt-dlp-web-ui.log"'' + ]) ++ (lib.optionals cfg.rpcAuth.enable [ + "-auth" + "-user ${cfg.rpcAuth.user}" + "-pass ${password}" + ]); + in + "${lib.getExe cfg.package} ${lib.concatStringsSep " " args}"; + User = cfg.user; + Group = cfg.group; + ProtectSystem = "strict"; + ProtectHome = "read-only"; + StateDirectory = "yt-dlp-web-ui"; + WorkingDirectory = "/var/lib/${StateDirectory}"; # equivalent to the dir above + LogsDirectory = "yt-dlp-web-ui"; + ReadWritePaths = [ + cfg.downloadDir + ]; + BindReadOnlyPaths = [ + builtins.storeDir + # required for youtube DNS lookup + "${config.environment.etc."ssl/certs/ca-certificates.crt".source}:/etc/ssl/certs/ca-certificates.crt" + ] ++ lib.optionals (cfg.rpcAuth.enable && cfg.rpcAuth.passwordFile != null) [ + cfg.rpcAuth.passwordFile + ]; + CapabilityBoundingSet = ""; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + RestrictRealtime = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + ProtectHostname = true; + }; + }; + }; +} diff --git a/nix/server.nix b/nix/server.nix new file mode 100644 index 0000000..5c7f99f --- /dev/null +++ b/nix/server.nix @@ -0,0 +1,52 @@ +{ yt-dlp-web-ui-frontend, buildGoModule, lib, makeWrapper, yt-dlp, ... }: +let + fs = lib.fileset; + common = import ./common.nix { inherit lib; }; +in +buildGoModule { + pname = "yt-dlp-web-ui"; + inherit (common) version; + src = fs.toSource rec { + root = ../.; + fileset = fs.difference root (fs.unions [ + ### LIST OF FILES TO IGNORE ### + # frontend (this is included by the frontend.nix drv instead) + ../frontend + # documentation + ../examples + # docker + ../Dockerfile + ../docker-compose.yml + # nix + ./devShell.nix + ../.envrc + ./tests + # make + ../Makefile # this derivation does not use the project Makefile + # repo commons + ../.github + ../README.md + ../LICENSE.md + ../.gitignore + ../.vscode + ]); + }; + + # https://github.com/golang/go/issues/44507 + preBuild = '' + cp -r ${yt-dlp-web-ui-frontend} frontend + ''; + + nativeBuildInputs = [ makeWrapper ]; + + postInstall = '' + wrapProgram $out/bin/yt-dlp-web-ui \ + --prefix PATH : ${lib.makeBinPath [ yt-dlp ]} + ''; + + vendorHash = "sha256-guM/U9DROJMx2ctPKBQis1YRhaf6fKvvwEWgswQKMG0="; + + meta = common.meta // { + mainProgram = "yt-dlp-web-ui"; + }; +} diff --git a/nix/tests/default.nix b/nix/tests/default.nix new file mode 100644 index 0000000..5761418 --- /dev/null +++ b/nix/tests/default.nix @@ -0,0 +1,20 @@ +{ self, pkgs }: { + testServiceStarts = pkgs.testers.runNixOSTest (_: { + name = "service-starts"; + nodes = { + machine = _: { + imports = [ + self.nixosModules.default + ]; + + services.yt-dlp-web-ui = { + enable = true; + downloadDir = "/var/lib/yt-dlp-web-ui"; + }; + }; + }; + testScript = '' + machine.wait_for_unit("yt-dlp-web-ui") + ''; + }); +}