Repository: streamlink/streamlink

Issue:
plugins.nicolive: plugin does not support stream metadata
### Checklist

- [X] This is a [plugin issue](https://streamlink.github.io/plugins.html) and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [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 issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

latest

### Description

Current behaviour: Author folder is not created nor is the title grabbed properly. All streams have titles on the site so not sure whats going wrong here.

Current output: -20230825200547.ts (right in the recordings folder, no author folder, no title)

Expected behaviour/output: Create Author folder and filename consisting of title-timestuff.ts



### Debug log

```text
streamlink --output "~/recordings/{author}/{title}-{time:%Y%m%d%H%M%S}.ts" https://live.nicovideo.jp/watch/co1992508 best
```



Discussion:
https://streamlink.github.io/cli/metadata.html

> The availability of each variable depends on the used plugin and whether that plugin supports this kind of metadata. If a variable is unsupported or not available, then its substitution will either be a short placeholder text ([--title](https://streamlink.github.io/cli.html#cmdoption-title)) or an empty string ([--output](https://streamlink.github.io/cli.html#cmdoption-output), [--record](https://streamlink.github.io/cli.html#cmdoption-record), [--record-and-pipe](https://streamlink.github.io/cli.html#cmdoption-record-and-pipe)).

The plugin does not support any metadata:
https://github.com/streamlink/streamlink/blob/6.1.0/src/streamlink/plugins/nicolive.py#L173-L205

Resolving patch:
diff --git a/src/streamlink/plugins/nicolive.py b/src/streamlink/plugins/nicolive.py
--- a/src/streamlink/plugins/nicolive.py
+++ b/src/streamlink/plugins/nicolive.py
@@ -2,6 +2,9 @@
 $description Japanese live-streaming and video hosting social platform.
 $url live.nicovideo.jp
 $type live, vod
+$metadata id
+$metadata author
+$metadata title
 $account Required by some streams
 $notes Timeshift is supported
 """
@@ -11,7 +14,7 @@
 from threading import Event
 from urllib.parse import urljoin
 
-from streamlink.plugin import Plugin, PluginError, pluginargument, pluginmatcher
+from streamlink.plugin import Plugin, pluginargument, pluginmatcher
 from streamlink.plugin.api import useragents, validate
 from streamlink.plugin.api.websocket import WebsocketClient
 from streamlink.stream.hls import HLSStream, HLSStreamReader
@@ -181,7 +184,9 @@ def _get_streams(self):
 
         self.niconico_web_login()
 
-        wss_api_url = self.get_wss_api_url()
+        data = self.get_data()
+
+        wss_api_url = self.find_wss_api_url(data)
         if not wss_api_url:
             log.error(
                 "Failed to get wss_api_url. "
@@ -189,6 +194,8 @@ def _get_streams(self):
             )
             return
 
+        self.id, self.author, self.title = self.find_metadata(data)
+
         self.wsclient = NicoLiveWsClient(self.session, wss_api_url)
         self.wsclient.start()
 
@@ -213,26 +220,56 @@ def _get_hls_stream_url(self):
 
         return self.wsclient.hls_stream_url
 
-    def get_wss_api_url(self):
-        try:
-            data = self.session.http.get(self.url, schema=validate.Schema(
-                validate.parse_html(),
-                validate.xml_find(".//script[@id='embedded-data'][@data-props]"),
-                validate.get("data-props"),
-                validate.parse_json(),
-                {"site": {
+    def get_data(self):
+        return self.session.http.get(self.url, schema=validate.Schema(
+            validate.parse_html(),
+            validate.xml_find(".//script[@id='embedded-data'][@data-props]"),
+            validate.get("data-props"),
+            validate.parse_json(),
+        ))
+
+    @staticmethod
+    def find_metadata(data):
+        schema = validate.Schema(
+            {
+                "program": {
+                    "nicoliveProgramId": str,
+                    "supplier": {"name": str},
+                    "title": str,
+                },
+            },
+            validate.get("program"),
+            validate.union_get(
+                "nicoliveProgramId",
+                ("supplier", "name"),
+                "title",
+            ),
+        )
+
+        return schema.validate(data)
+
+    @staticmethod
+    def find_wss_api_url(data):
+        schema = validate.Schema(
+            {
+                "site": {
                     "relive": {
-                        "webSocketUrl": validate.url(scheme="wss"),
+                        "webSocketUrl": validate.any(
+                            validate.url(scheme="wss"),
+                            "",
+                        ),
                     },
                     validate.optional("frontendId"): int,
-                }},
-                validate.get("site"),
-                validate.union_get(("relive", "webSocketUrl"), "frontendId"),
-            ))
-        except PluginError:
+                },
+            },
+            validate.get("site"),
+            validate.union_get(("relive", "webSocketUrl"), "frontendId"),
+        )
+
+        wss_api_url, frontend_id = schema.validate(data)
+        if not wss_api_url:
             return
 
-        wss_api_url, frontend_id = data
         if frontend_id is not None:
             wss_api_url = update_qsd(wss_api_url, {"frontend_id": frontend_id})
 


Test patch: