Repository: streamlink/streamlink

Issue:
Add {plugin_name} metadata variable
### Checklist

- [X] This is a feature request and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin requests](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22feature+request%22)

### Description

`yt-dlp` has [`webpage_url_domain`](https://github.com/yt-dlp/yt-dlp#user-content-output-template) as an output template field that I use to organize my downloads by site (`youtube.com`, `twitch.tv`, `vimeo.com` etc.). It'd be nice if a `url_domain` variable could be added to have parody with that of `yt-dlp` so both tools could coexist nicely and use the same organization directory structure.


Discussion:
I can only see this being useful for (custom) config files, if the output path is defined there and the user's input URL set on the CLI.

When not using a config file with a predefined output path, the input URL already implies which path you should use in the `--output` or `--record` argument value, so adding a new metadata var is not really necessary and the path can easily be set manually.

Plugins also support different domains, so adding a `{plugin}` or `{pluginname}` variable would make more sense IMO.
> I can only see this being useful for (custom) config files, if the output path is defined there and the user's input URL set on the CLI.

This is indeed how I intend to use this if added.

In my `youtube-dl` config I currently have:
```
# ~/.config/yt-dlp/config
--output "~/vids/%(webpage_url_domain)s/%(uploader_id)s/%(upload_date>%Y.%m.%d)s_%(id)s_%(title)s.%(ext)s"
```
In my `streamlink` config I _wish_ to have:
```
output=~/vids/{url_domain}/{author}/{time:%Y.%m.%d}_{id}_{title}.ts
```
This would allow the two tools to both use the same directory naming scheme.

> Plugins also support different domains, so adding a `{plugin}` or `{pluginname}` variable would make more sense IMO.

~~This does make more sense and I also would prefer this but unfortunately `yt-dlp` & friends don't have any such counterpart I could use currently, I'll open an issue on `yt-dlp` to address this though.~~ I'm an idiot, `yt-dlp` already has this working nicely, let's do that instead of what I originally proposed :+1: 
For Bigo.tv is worked bot?

Resolving patch:
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -55,6 +55,7 @@ def get_formatter(plugin: Plugin):
     return Formatter(
         {
             "url": lambda: args.url,
+            "plugin": lambda: plugin.module,
             "id": lambda: plugin.get_id(),
             "author": lambda: plugin.get_author(),
             "category": lambda: plugin.get_category(),


Test patch:
diff --git a/tests/test_cli_main.py b/tests/test_cli_main.py
--- a/tests/test_cli_main.py
+++ b/tests/test_cli_main.py
@@ -404,24 +404,17 @@ class TestCLIMainHandleStream(unittest.TestCase):
     @patch("streamlink_cli.main.output_stream")
     @patch("streamlink_cli.main.args")
     def test_handle_stream_output_stream(self, args: Mock, mock_output_stream: Mock):
-        """
-        Test that the formatter does define the correct variables
-        """
         args.json = False
         args.subprocess_cmdline = False
         args.stream_url = False
         args.output = False
         args.stdout = False
-        args.url = "URL"
         args.player_passthrough = []
         args.player_external_http = False
         args.player_continuous_http = False
         mock_output_stream.return_value = True
 
-        plugin = _TestPlugin("")
-        plugin.author = "AUTHOR"
-        plugin.category = "CATEGORY"
-        plugin.title = "TITLE"
+        plugin = FakePlugin("")
         stream = Stream(session=Mock())
         streams = {"best": stream}
 
@@ -430,10 +423,6 @@ def test_handle_stream_output_stream(self, args: Mock, mock_output_stream: Mock)
         paramStream, paramFormatter = mock_output_stream.call_args[0]
         self.assertIs(paramStream, stream)
         self.assertIsInstance(paramFormatter, Formatter)
-        self.assertEqual(
-            paramFormatter.title("{url} - {author} - {category}/{game} - {title}"),
-            "URL - AUTHOR - CATEGORY/CATEGORY - TITLE"
-        )
 
 
 class TestCLIMainOutputStream(unittest.TestCase):
diff --git a/tests/test_cli_main_formatter.py b/tests/test_cli_main_formatter.py
new file mode 100644
--- /dev/null
+++ b/tests/test_cli_main_formatter.py
@@ -0,0 +1,43 @@
+from unittest.mock import Mock, patch
+
+import pytest
+
+from streamlink.plugin import Plugin
+from streamlink_cli.main import get_formatter
+from streamlink_cli.utils import datetime
+
+
+@pytest.fixture(scope="module")
+def plugin():
+    class FakePlugin(Plugin):
+        def _get_streams(self):  # pragma: no cover
+            pass
+
+    plugin = FakePlugin("https://foo/bar")
+    plugin.id = "ID"
+    plugin.author = "AUTHOR"
+    plugin.category = "CATEGORY"
+    plugin.title = "TITLE"
+    plugin.bind(Mock(), "FAKE")
+
+    return plugin
+
+
+@pytest.mark.parametrize("formatterinput,expected", [
+    ("{url}", "https://foo/bar"),
+    ("{plugin}", "FAKE"),
+    ("{id}", "ID"),
+    ("{author}", "AUTHOR"),
+    ("{category}", "CATEGORY"),
+    ("{game}", "CATEGORY"),
+    ("{title}", "TITLE"),
+    ("{time}", "2000-01-01_00-00-00"),
+    ("{time:%Y}", "2000"),
+])
+# workaround for freezegun not being able to patch the subclassed datetime class in streamlink_cli.utils
+# which defines the default datetime->str conversion format (needed for path outputs)
+@patch("streamlink_cli.utils.datetime.now", Mock(return_value=datetime(2000, 1, 1, 0, 0, 0, 0)))
+@patch("streamlink_cli.main.args", Mock(url="https://foo/bar"))
+def test_get_formatter(plugin, formatterinput, expected):
+    formatter = get_formatter(plugin)
+    assert formatter.title(formatterinput) == expected