diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 51c35976a..b49a8cfc2 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -2180,6 +2180,16 @@ in { scrobbler (e.g. last.fm) ''; } + + { + time = "2025-03-29T04:16:57+00:00"; + message = '' + A new module is available: 'programs.streamlink'. + + Streamlink is a CLI utility which pipes video streams from various + services into a video player. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index b48ef31af..4a9ba5f1a 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -245,6 +245,7 @@ let ./programs/sqls.nix ./programs/ssh.nix ./programs/starship.nix + ./programs/streamlink.nix ./programs/swayimg.nix ./programs/swaylock.nix ./programs/swayr.nix diff --git a/modules/programs/streamlink.nix b/modules/programs/streamlink.nix new file mode 100644 index 000000000..b6dbd75b7 --- /dev/null +++ b/modules/programs/streamlink.nix @@ -0,0 +1,189 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + + cfg = config.programs.streamlink; + + renderSettings = + settings: + lib.concatLines ( + lib.remove "" ( + lib.mapAttrsToList ( + name: value: + if (builtins.isBool value) then + if value then name else "" + else if (builtins.isList value) then + lib.concatStringsSep "\n" (builtins.map (item: "${name}=${builtins.toString item}") value) + else + "${name}=${builtins.toString value}" + ) settings + ) + ); + + pluginType = lib.types.submodule { + options = { + src = lib.mkOption { + type = with lib.types; nullOr (either path lines); + default = null; + description = '' + Source of the custom plugin. The value should be a path to the + plugin file, or the text of the plugin code. Will be linked to + {file}`$XDG_DATA_HOME/streamlink/plugins/.py` (linux) or + {file}`Library/Application Support/streamlink/plugins/.py` (darwin). + ''; + example = lib.literalExpression "./custom_plugin.py"; + }; + + settings = lib.mkOption { + type = + with lib.types; + attrsOf (oneOf [ + bool + int + float + str + (listOf (oneOf [ + int + float + str + ])) + ]); + default = { }; + example = lib.literalExpression '' + { + quiet = true; + } + ''; + description = '' + Configuration for the specific plugin, written to + {file}`$XDG_CONFIG_HOME/streamlink/config.` (linux) or + {file}`Library/Application Support/streamlink/config.` (darwin). + ''; + }; + }; + }; + +in +{ + meta.maintainers = [ lib.hm.maintainers.folliehiyuki ]; + + options.programs.streamlink = { + enable = lib.mkEnableOption "streamlink"; + + package = lib.mkPackageOption pkgs "streamlink" { nullable = true; }; + + settings = lib.mkOption { + type = + with lib.types; + attrsOf (oneOf [ + bool + int + float + str + (listOf (oneOf [ + int + float + str + ])) + ]); + default = { }; + example = lib.literalExpression '' + { + player = "''${pkgs.mpv}/bin/mpv"; + player-args = "--cache 2048"; + player-no-close = true; + } + ''; + description = '' + Global configuration options for streamlink. It will be written to + {file}`$XDG_CONFIG_HOME/streamlink/config` (linux) or + {file}`Library/Application Support/streamlink/config` (darwin). + ''; + }; + + plugins = lib.mkOption { + description = '' + Streamlink plugins. + + If a source is set, the custom plugin will be linked to the data directory. + + Additional configuration specific to the plugin, if defined, will be + written to the config directory, and override global settings. + ''; + type = lib.types.attrsOf pluginType; + default = { }; + example = lib.literalExpression '' + { + custom_plugin = { + src = ./custom_plugin.py; + settings = { + quiet = true; + }; + }; + } + ''; + }; + }; + + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; } + + (lib.mkIf pkgs.stdenv.hostPlatform.isLinux { + xdg.configFile = + { + "streamlink/config" = lib.mkIf (cfg.settings != { }) { + text = renderSettings cfg.settings; + }; + } + // (lib.mapAttrs' ( + name: value: + lib.nameValuePair "streamlink/config.${name}" ( + lib.mkIf (value.settings != { }) { + text = renderSettings value.settings; + } + ) + ) cfg.plugins); + + xdg.dataFile = lib.mapAttrs' ( + name: value: + lib.nameValuePair "streamlink/plugins/${name}.py" ( + lib.mkIf (value.src != null) ( + if (builtins.isPath value.src) then { source = value.src; } else { text = value.src; } + ) + ) + ) cfg.plugins; + }) + + (lib.mkIf pkgs.stdenv.hostPlatform.isDarwin { + home.file = + { + "Library/Application Support/streamlink/config" = lib.mkIf (cfg.settings != { }) { + text = renderSettings cfg.settings; + }; + } + // (lib.mapAttrs' ( + name: value: + lib.nameValuePair "Library/Application Support/streamlink/config.${name}" ( + lib.mkIf (value.settings != { }) { + text = renderSettings value.settings; + } + ) + ) cfg.plugins) + // (lib.mapAttrs' ( + name: value: + lib.nameValuePair "Library/Application Support/streamlink/plugins/${name}.py" ( + lib.mkIf (value.src != null) ( + if (builtins.isPath value.src) then { source = value.src; } else { text = value.src; } + ) + ) + ) cfg.plugins); + }) + ] + ); +} diff --git a/tests/default.nix b/tests/default.nix index 04d7b1c52..ac0fdfc00 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -392,6 +392,7 @@ in import nmtSrc { ./modules/programs/spotify-player ./modules/programs/ssh ./modules/programs/starship + ./modules/programs/streamlink ./modules/programs/taskwarrior ./modules/programs/tealdeer ./modules/programs/tex-fmt diff --git a/tests/modules/programs/streamlink/config b/tests/modules/programs/streamlink/config new file mode 100644 index 000000000..bfeececec --- /dev/null +++ b/tests/modules/programs/streamlink/config @@ -0,0 +1,5 @@ +http-header=User-Agent=Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0 +http-header=Accept-Language=en-US +player=mpv +player-args=--cache 2048 +player-no-close diff --git a/tests/modules/programs/streamlink/default.nix b/tests/modules/programs/streamlink/default.nix new file mode 100644 index 000000000..4ce9bd645 --- /dev/null +++ b/tests/modules/programs/streamlink/default.nix @@ -0,0 +1,4 @@ +{ + streamlink-settings = ./streamlink-settings.nix; + streamlink-custom-plugins = ./streamlink-custom-plugins.nix; +} diff --git a/tests/modules/programs/streamlink/dummy.py b/tests/modules/programs/streamlink/dummy.py new file mode 100644 index 000000000..45149dd47 --- /dev/null +++ b/tests/modules/programs/streamlink/dummy.py @@ -0,0 +1,9 @@ +""" +$description Dummy plugin for testing +""" + +from streamlink.plugin import Plugin + +class DummyTV(Plugin): + +__plugin__ = DummyTV diff --git a/tests/modules/programs/streamlink/streamlink-custom-plugins.nix b/tests/modules/programs/streamlink/streamlink-custom-plugins.nix new file mode 100644 index 000000000..49eb225cc --- /dev/null +++ b/tests/modules/programs/streamlink/streamlink-custom-plugins.nix @@ -0,0 +1,45 @@ +{ pkgs, ... }: + +{ + programs.streamlink = { + enable = true; + plugins = { + dummy.src = ./dummy.py; + + dummy2.src = builtins.readFile ./dummy.py; + + twitch.settings = { + player = "haruna"; + quiet = true; + }; + }; + }; + + test.stubs.streamlink = { }; + + nmt.script = let + configDir = if pkgs.stdenv.isDarwin then + "Library/Application Support/streamlink" + else + ".config/streamlink"; + + pluginDir = if pkgs.stdenv.isDarwin then + "Library/Application Support/streamlink/plugins" + else + ".local/share/streamlink/plugins"; + in '' + assertFileExists home-files/${configDir}/config.twitch + assertFileContent home-files/${configDir}/config.twitch ${ + pkgs.writeText "expected" '' + player=haruna + quiet + '' + } + + assertFileExists home-files/${pluginDir}/dummy.py + assertFileContent home-files/${pluginDir}/dummy.py ${./dummy.py} + + assertFileExists home-files/${pluginDir}/dummy2.py + assertFileContent home-files/${pluginDir}/dummy2.py ${./dummy.py} + ''; +} diff --git a/tests/modules/programs/streamlink/streamlink-settings.nix b/tests/modules/programs/streamlink/streamlink-settings.nix new file mode 100644 index 000000000..df8a5e34c --- /dev/null +++ b/tests/modules/programs/streamlink/streamlink-settings.nix @@ -0,0 +1,28 @@ +{ pkgs, ... }: + +{ + programs.streamlink = { + enable = true; + settings = { + player = "mpv"; + player-args = "--cache 2048"; + player-no-close = true; + http-header = [ + "User-Agent=Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0" + "Accept-Language=en-US" + ]; + }; + }; + + test.stubs.streamlink = { }; + + nmt.script = let + streamlinkConfig = if pkgs.stdenv.hostPlatform.isDarwin then + "Library/Application Support/streamlink/config" + else + ".config/streamlink/config"; + in '' + assertFileExists home-files/${streamlinkConfig} + assertFileContent home-files/${streamlinkConfig} ${./config} + ''; +}