Repository: streamlink/streamlink

Issue:
cli: add title to json
![image](https://user-images.githubusercontent.com/73726132/130978913-69572b80-9ebc-46ed-a43b-e8e81392f719.png)

![image](https://user-images.githubusercontent.com/73726132/130978558-821844dc-c820-48cd-840a-94fb355dea02.png)

used as mpv backend :
![image](https://user-images.githubusercontent.com/73726132/130914398-298d1678-b079-4d18-b041-ec532c0cacd4.png)



Discussion:
After thinking about it again (I only had the diff in mind in my previous comment, sorry), do we want the interpolated `--title` parameter value in the JSON output, or the individual stream metadata properties?

The `--title` parameter is just a utility for formatting the window title of the player launched by Streamlink, which is not useful for JSON data, where individual metadata properties make much more sense.
> After thinking about it again (I only had the diff in mind in my previous comment, sorry), do we want the interpolated `--title` parameter value in the JSON output, or the individual stream metadata properties?

both

> The `--title` parameter is just a utility for formatting the window title of the player launched by Streamlink, which is not useful for JSON data, where individual metadata properties make much more sense.

i have a script that use streamlink as mpv backend instead of youtube-dl for some specific sites like twitch... [$](https://gist.github.com/ChrisK2/8701184fe3ea7701c9cc) 
just by typing "mpv %url%"
it run 'streamlink -j url quality' then extract streamurl from the response and passe it to mpv
this is needed so they are no need to call streamlink two times to get the title
What I am saying is that the JSON data should not include the custom `--title` stuff but rather the individual values of the plugin metadata, so one who uses the JSON response can read metadata separately, which is impossible with a single string based on the user's `--title` value. In your case, these single metadata properties can be used and combined for formatting the player window title.

```json
{
  "type": "hls",
  "url": "https://foo",
  "master": "https://bar",
  "headers": {},
  "metadata": {
    "author": ...,
    "category": ...,
    "title": ...,
  }
}
```
i am not sure if this is worth adding right now ;
if user wanted only a specific metadata like the author name he can alwayse call `streamlink -j --title "{author}" %url% best`
if he wanted multiple metadata properties, he probably gonna combine them; so instead of just calling streamlink with the formate he wanted, he gonna need to combine them manually 

also this way respect the formate in streamlink config file if i called `streamlink -j %url% best`

Resolving patch:
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -430,6 +430,13 @@ def stream_weight_only(s):
     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
--- 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
--- a/tests/test_cli_main.py
+++ b/tests/test_cli_main.py
@@ -9,7 +9,6 @@
 
 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 @@
     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 @@ def test_handle_stream_with_json_and_stream_url(self, console, args):
     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()