Repository: streamlink/streamlink
Pull request #3987 (2021-09-03 12:06:44)

cli: include plugin metadata in --json output
- Add `Plugin.get_metadata()` which returns a dict of all metadata.
- Add `metadata` property to list of all resolved streams, but merge
  all metadata properties with the specific stream output in order to
  not introduce a breaking change in the JSON output format.

----

Resolves #3941 


Patch:
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
index c4869508..de0ff492 100644
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -430,6 +430,13 @@ class Plugin:
     def _get_streams(self):
         raise NotImplementedError
 
+    def get_metadata(self) -> Dict[str, Optional[str]]:
+        return dict(
+            author=self.get_author(),
+            category=self.get_category(),
+            title=self.get_title()
+        )
+
     def get_title(self) -> Optional[str]:
         return self.title
 
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index bfe2117f..f6bb845b 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -408,7 +408,10 @@ def handle_stream(plugin, streams, stream_name):
 
     # Print JSON representation of the stream
     elif args.json:
-        console.msg_json(stream)
+        console.msg_json(
+            stream,
+            metadata=plugin.get_metadata()
+        )
 
     elif args.stream_url:
         try:
@@ -583,11 +586,20 @@ def handle_url():
 
         err = f"The specified stream(s) '{', '.join(args.stream)}' could not be found"
         if args.json:
-            console.msg_json(plugin=plugin.module, streams=streams, error=err)
+            console.msg_json(
+                plugin=plugin.module,
+                metadata=plugin.get_metadata(),
+                streams=streams,
+                error=err
+            )
         else:
             console.exit(f"{err}.\n       Available streams: {validstreams}")
     elif args.json:
-        console.msg_json(plugin=plugin.module, streams=streams)
+        console.msg_json(
+            plugin=plugin.module,
+            metadata=plugin.get_metadata(),
+            streams=streams
+        )
     elif args.stream_url:
         try:
             console.msg(streams[list(streams)[-1]].to_manifest_url())


Test patch:
diff --git a/tests/test_cli_main.py b/tests/test_cli_main.py
index 10374468..35047bdd 100644
--- a/tests/test_cli_main.py
+++ b/tests/test_cli_main.py
@@ -9,7 +9,6 @@ import freezegun
 
 import streamlink_cli.main
 import tests.resources
-from streamlink.plugin.plugin import Plugin
 from streamlink.session import Streamlink
 from streamlink.stream.stream import Stream
 from streamlink_cli.compat import DeprecatedPath, is_win32, stdout
@@ -26,11 +25,7 @@ from streamlink_cli.main import (
     setup_config_args
 )
 from streamlink_cli.output import FileOutput, PlayerOutput
-
-
-class FakePlugin(Plugin):
-    def _get_streams(self):
-        pass  # pragma: no cover
+from tests.plugin.testplugin import TestPlugin as FakePlugin
 
 
 class TestCLIMain(unittest.TestCase):
@@ -107,11 +102,21 @@ class TestCLIMainJsonAndStreamUrl(unittest.TestCase):
     def test_handle_stream_with_json_and_stream_url(self, console, args):
         stream = Mock()
         streams = dict(best=stream)
-        plugin = Mock(FakePlugin(""), module="fake", arguments=[], streams=Mock(return_value=streams))
+        plugin = FakePlugin("")
+        plugin.module = "fake"
+        plugin.arguments = []
+        plugin.streams = Mock(return_value=streams)
 
         handle_stream(plugin, streams, "best")
         self.assertEqual(console.msg.mock_calls, [])
-        self.assertEqual(console.msg_json.mock_calls, [call(stream)])
+        self.assertEqual(console.msg_json.mock_calls, [call(
+            stream,
+            metadata=dict(
+                author="Tѥst Āuƭhǿr",
+                category=None,
+                title="Test Title"
+            )
+        )])
         self.assertEqual(console.error.mock_calls, [])
         console.msg_json.mock_calls.clear()
 
@@ -133,12 +138,23 @@ class TestCLIMainJsonAndStreamUrl(unittest.TestCase):
     def test_handle_url_with_json_and_stream_url(self, console, args):
         stream = Mock()
         streams = dict(worst=Mock(), best=stream)
-        plugin = Mock(FakePlugin(""), module="fake", arguments=[], streams=Mock(return_value=streams))
+        plugin = FakePlugin("")
+        plugin.module = "fake"
+        plugin.arguments = []
+        plugin.streams = Mock(return_value=streams)
 
         with patch("streamlink_cli.main.streamlink", resolve_url=Mock(return_value=plugin)):
             handle_url()
             self.assertEqual(console.msg.mock_calls, [])
-            self.assertEqual(console.msg_json.mock_calls, [call(plugin="fake", streams=streams)])
+            self.assertEqual(console.msg_json.mock_calls, [call(
+                plugin="fake",
+                metadata=dict(
+                    author="Tѥst Āuƭhǿr",
+                    category=None,
+                    title="Test Title"
+                ),
+                streams=streams
+            )])
             self.assertEqual(console.error.mock_calls, [])
             console.msg_json.mock_calls.clear()