We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
plugins.picarto: Plugin fails to open VODS but works just fine on popout player links
### Checklist

- [X] This is a plugin issue 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 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 build from the master branch

### Description

Running lateast dev build, The picarto plugin does not seem to want to open Vods.
Attempting to open https://picarto.tv/Grimsby/videos/197524 results in [plugins.picarto][error] User is not online.
But opening https://picarto.tv/videopopout/197524 works just fine.

### Debug log

```text
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.9.7
[cli][debug] Streamlink: 2.4.0+63.gbf269e2
[cli][debug] Requests(2.26.0), Socks(1.7.1), Websocket(1.2.1)
[cli][debug] Arguments:
[cli][debug]  url=https://picarto.tv/Grimsby/videos/197524
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player="C:\mpv\mpv.exe"
[cli][debug]  --output=asdd.mkv
[cli][debug]  --rtmp-rtmpdump=rtmpdump.exe
[cli][debug]  --rtmpdump=C:\Program Files (x86)\Streamlink\rtmpdump\rtmpdump.exe
[cli][debug]  --ffmpeg-ffmpeg=C:\Program Files (x86)\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin picarto for URL https://picarto.tv/Grimsby/videos/197524
[plugins.picarto][debug] Type=Live
[plugins.picarto][error] User is not online
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/streamlink/plugins/picarto.py`
Content:
```
1 import logging
2 import re
3 from urllib.parse import urlparse
4 
5 from streamlink.plugin import Plugin, pluginmatcher
6 from streamlink.plugin.api import validate
7 from streamlink.stream.hls import HLSStream
8 
9 log = logging.getLogger(__name__)
10 
11 
12 @pluginmatcher(re.compile(r"""
13     https?://(?:www\.)?picarto\.tv/
14     (?:(?P<po>streampopout|videopopout)/)?
15     (?P<user>[^&?/]+)
16     (?:\?tab=videos&id=(?P<vod_id>\d+))?
17 """, re.VERBOSE))
18 class Picarto(Plugin):
19     API_URL_LIVE = "https://ptvintern.picarto.tv/api/channel/detail/{username}"
20     API_URL_VOD = "https://ptvintern.picarto.tv/ptvapi"
21     HLS_URL = "https://{netloc}/stream/hls/{file_name}/index.m3u8"
22 
23     def get_live(self, username):
24         netloc = self.session.http.get(self.url, schema=validate.Schema(
25             validate.parse_html(),
26             validate.xml_xpath_string(".//script[contains(@src,'/stream/player.js')][1]/@src"),
27             validate.any(None, validate.transform(lambda src: urlparse(src).netloc))
28         ))
29         if not netloc:
30             log.error("Could not find server netloc")
31             return
32 
33         channel, multistreams = self.session.http.get(self.API_URL_LIVE.format(username=username), schema=validate.Schema(
34             validate.parse_json(),
35             {
36                 "channel": validate.any(None, {
37                     "stream_name": str,
38                     "title": str,
39                     "online": bool,
40                     "private": bool,
41                     "categories": [{"label": str}],
42                 }),
43                 "getMultiStreams": validate.any(None, {
44                     "multistream": bool,
45                     "streams": [{
46                         "name": str,
47                         "online": bool,
48                     }],
49                 }),
50             },
51             validate.union_get("channel", "getMultiStreams")
52         ))
53         if not channel or not multistreams:
54             log.debug("Missing channel or streaming data")
55             return
56 
57         log.trace(f"netloc={netloc!r}")
58         log.trace(f"channel={channel!r}")
59         log.trace(f"multistreams={multistreams!r}")
60 
61         if not channel["online"]:
62             log.error("User is not online")
63             return
64 
65         if channel["private"]:
66             log.info("This is a private stream")
67             return
68 
69         self.author = username
70         self.category = channel["categories"][0]["label"]
71         self.title = channel["title"]
72 
73         hls_url = self.HLS_URL.format(
74             netloc=netloc,
75             file_name=channel["stream_name"]
76         )
77 
78         return HLSStream.parse_variant_playlist(self.session, hls_url)
79 
80     def get_vod(self, vod_id):
81         data = {
82             'query': (
83                 'query ($videoId: ID!) {\n'
84                 '  video(id: $videoId) {\n'
85                 '    id\n'
86                 '    title\n'
87                 '    file_name\n'
88                 '    video_recording_image_url\n'
89                 '    channel {\n'
90                 '      name\n'
91                 '      }'
92                 '  }\n'
93                 '}\n'
94             ),
95             'variables': {'videoId': vod_id},
96         }
97         vod_data = self.session.http.post(self.API_URL_VOD, json=data, schema=validate.Schema(
98             validate.parse_json(),
99             {"data": {
100                 "video": validate.any(None, {
101                     "id": str,
102                     "title": str,
103                     "file_name": str,
104                     "video_recording_image_url": str,
105                     "channel": {"name": str},
106                 }),
107             }},
108             validate.get(("data", "video"))
109         ))
110 
111         if not vod_data:
112             log.debug("Missing video data")
113             return
114 
115         log.trace(f"vod_data={vod_data!r}")
116 
117         self.author = vod_data["channel"]["name"]
118         self.category = "VOD"
119         self.title = vod_data["title"]
120 
121         netloc = urlparse(vod_data["video_recording_image_url"]).netloc
122         hls_url = self.HLS_URL.format(
123             netloc=netloc,
124             file_name=vod_data["file_name"]
125         )
126 
127         return HLSStream.parse_variant_playlist(self.session, hls_url)
128 
129     def _get_streams(self):
130         m = self.match.groupdict()
131 
132         if (m['po'] == 'streampopout' or not m['po']) and m['user'] and not m['vod_id']:
133             log.debug('Type=Live')
134             return self.get_live(m['user'])
135         elif m['po'] == 'videopopout' or (m['user'] and m['vod_id']):
136             log.debug('Type=VOD')
137             vod_id = m['vod_id'] if m['vod_id'] else m['user']
138             return self.get_vod(vod_id)
139 
140 
141 __plugin__ = Picarto
142
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/streamlink/plugins/picarto.py b/src/streamlink/plugins/picarto.py
--- a/src/streamlink/plugins/picarto.py
+++ b/src/streamlink/plugins/picarto.py
@@ -11,9 +11,15 @@
 
 @pluginmatcher(re.compile(r"""
     https?://(?:www\.)?picarto\.tv/
-    (?:(?P<po>streampopout|videopopout)/)?
-    (?P<user>[^&?/]+)
-    (?:\?tab=videos&id=(?P<vod_id>\d+))?
+    (?:
+        streampopout/(?P<po_user>[^/]+)/public
+    |
+        videopopout/(?P<po_vod_id>\d+)
+    |
+        [^/]+/videos/(?P<vod_id>\d+)
+    |
+        (?P<user>[^/?&]+)
+    )$
 """, re.VERBOSE))
 class Picarto(Plugin):
     API_URL_LIVE = "https://ptvintern.picarto.tv/api/channel/detail/{username}"
@@ -129,13 +135,12 @@
     def _get_streams(self):
         m = self.match.groupdict()
 
-        if (m['po'] == 'streampopout' or not m['po']) and m['user'] and not m['vod_id']:
-            log.debug('Type=Live')
-            return self.get_live(m['user'])
-        elif m['po'] == 'videopopout' or (m['user'] and m['vod_id']):
+        if m['po_vod_id'] or m['vod_id']:
             log.debug('Type=VOD')
-            vod_id = m['vod_id'] if m['vod_id'] else m['user']
-            return self.get_vod(vod_id)
+            return self.get_vod(m['po_vod_id'] or m['vod_id'])
+        elif m['po_user'] or m['user']:
+            log.debug('Type=Live')
+            return self.get_live(m['po_user'] or m['user'])
 
 
 __plugin__ = Picarto

{"golden_diff": "diff --git a/src/streamlink/plugins/picarto.py b/src/streamlink/plugins/picarto.py\n--- a/src/streamlink/plugins/picarto.py\n+++ b/src/streamlink/plugins/picarto.py\n@@ -11,9 +11,15 @@\n \n @pluginmatcher(re.compile(r\"\"\"\n     https?://(?:www\\.)?picarto\\.tv/\n-    (?:(?P<po>streampopout|videopopout)/)?\n-    (?P<user>[^&?/]+)\n-    (?:\\?tab=videos&id=(?P<vod_id>\\d+))?\n+    (?:\n+        streampopout/(?P<po_user>[^/]+)/public\n+    |\n+        videopopout/(?P<po_vod_id>\\d+)\n+    |\n+        [^/]+/videos/(?P<vod_id>\\d+)\n+    |\n+        (?P<user>[^/?&]+)\n+    )$\n \"\"\", re.VERBOSE))\n class Picarto(Plugin):\n     API_URL_LIVE = \"https://ptvintern.picarto.tv/api/channel/detail/{username}\"\n@@ -129,13 +135,12 @@\n     def _get_streams(self):\n         m = self.match.groupdict()\n \n-        if (m['po'] == 'streampopout' or not m['po']) and m['user'] and not m['vod_id']:\n-            log.debug('Type=Live')\n-            return self.get_live(m['user'])\n-        elif m['po'] == 'videopopout' or (m['user'] and m['vod_id']):\n+        if m['po_vod_id'] or m['vod_id']:\n             log.debug('Type=VOD')\n-            vod_id = m['vod_id'] if m['vod_id'] else m['user']\n-            return self.get_vod(vod_id)\n+            return self.get_vod(m['po_vod_id'] or m['vod_id'])\n+        elif m['po_user'] or m['user']:\n+            log.debug('Type=Live')\n+            return self.get_live(m['po_user'] or m['user'])\n \n \n __plugin__ = Picarto\n", "issue": "plugins.picarto: Plugin fails to open VODS but works just fine on popout player links\n### Checklist\r\n\r\n- [X] This is a plugin issue and not a different kind of issue\r\n- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)\r\n- [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)\r\n- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)\r\n\r\n### Streamlink version\r\n\r\nLatest build from the master branch\r\n\r\n### Description\r\n\r\nRunning lateast dev build, The picarto plugin does not seem to want to open Vods.\r\nAttempting to open https://picarto.tv/Grimsby/videos/197524 results in [plugins.picarto][error] User is not online.\r\nBut opening https://picarto.tv/videopopout/197524 works just fine.\r\n\r\n### Debug log\r\n\r\n```text\r\n[cli][debug] OS:         Windows 10\r\n[cli][debug] Python:     3.9.7\r\n[cli][debug] Streamlink: 2.4.0+63.gbf269e2\r\n[cli][debug] Requests(2.26.0), Socks(1.7.1), Websocket(1.2.1)\r\n[cli][debug] Arguments:\r\n[cli][debug]  url=https://picarto.tv/Grimsby/videos/197524\r\n[cli][debug]  stream=['best']\r\n[cli][debug]  --loglevel=debug\r\n[cli][debug]  --player=\"C:\\mpv\\mpv.exe\"\r\n[cli][debug]  --output=asdd.mkv\r\n[cli][debug]  --rtmp-rtmpdump=rtmpdump.exe\r\n[cli][debug]  --rtmpdump=C:\\Program Files (x86)\\Streamlink\\rtmpdump\\rtmpdump.exe\r\n[cli][debug]  --ffmpeg-ffmpeg=C:\\Program Files (x86)\\Streamlink\\ffmpeg\\ffmpeg.exe\r\n[cli][info] Found matching plugin picarto for URL https://picarto.tv/Grimsby/videos/197524\r\n[plugins.picarto][debug] Type=Live\r\n[plugins.picarto][error] User is not online\r\n```\r\n\n", "before_files": [{"content": "import logging\nimport re\nfrom urllib.parse import urlparse\n\nfrom streamlink.plugin import Plugin, pluginmatcher\nfrom streamlink.plugin.api import validate\nfrom streamlink.stream.hls import HLSStream\n\nlog = logging.getLogger(__name__)\n\n\n@pluginmatcher(re.compile(r\"\"\"\n    https?://(?:www\\.)?picarto\\.tv/\n    (?:(?P<po>streampopout|videopopout)/)?\n    (?P<user>[^&?/]+)\n    (?:\\?tab=videos&id=(?P<vod_id>\\d+))?\n\"\"\", re.VERBOSE))\nclass Picarto(Plugin):\n    API_URL_LIVE = \"https://ptvintern.picarto.tv/api/channel/detail/{username}\"\n    API_URL_VOD = \"https://ptvintern.picarto.tv/ptvapi\"\n    HLS_URL = \"https://{netloc}/stream/hls/{file_name}/index.m3u8\"\n\n    def get_live(self, username):\n        netloc = self.session.http.get(self.url, schema=validate.Schema(\n            validate.parse_html(),\n            validate.xml_xpath_string(\".//script[contains(@src,'/stream/player.js')][1]/@src\"),\n            validate.any(None, validate.transform(lambda src: urlparse(src).netloc))\n        ))\n        if not netloc:\n            log.error(\"Could not find server netloc\")\n            return\n\n        channel, multistreams = self.session.http.get(self.API_URL_LIVE.format(username=username), schema=validate.Schema(\n            validate.parse_json(),\n            {\n                \"channel\": validate.any(None, {\n                    \"stream_name\": str,\n                    \"title\": str,\n                    \"online\": bool,\n                    \"private\": bool,\n                    \"categories\": [{\"label\": str}],\n                }),\n                \"getMultiStreams\": validate.any(None, {\n                    \"multistream\": bool,\n                    \"streams\": [{\n                        \"name\": str,\n                        \"online\": bool,\n                    }],\n                }),\n            },\n            validate.union_get(\"channel\", \"getMultiStreams\")\n        ))\n        if not channel or not multistreams:\n            log.debug(\"Missing channel or streaming data\")\n            return\n\n        log.trace(f\"netloc={netloc!r}\")\n        log.trace(f\"channel={channel!r}\")\n        log.trace(f\"multistreams={multistreams!r}\")\n\n        if not channel[\"online\"]:\n            log.error(\"User is not online\")\n            return\n\n        if channel[\"private\"]:\n            log.info(\"This is a private stream\")\n            return\n\n        self.author = username\n        self.category = channel[\"categories\"][0][\"label\"]\n        self.title = channel[\"title\"]\n\n        hls_url = self.HLS_URL.format(\n            netloc=netloc,\n            file_name=channel[\"stream_name\"]\n        )\n\n        return HLSStream.parse_variant_playlist(self.session, hls_url)\n\n    def get_vod(self, vod_id):\n        data = {\n            'query': (\n                'query ($videoId: ID!) {\\n'\n                '  video(id: $videoId) {\\n'\n                '    id\\n'\n                '    title\\n'\n                '    file_name\\n'\n                '    video_recording_image_url\\n'\n                '    channel {\\n'\n                '      name\\n'\n                '      }'\n                '  }\\n'\n                '}\\n'\n            ),\n            'variables': {'videoId': vod_id},\n        }\n        vod_data = self.session.http.post(self.API_URL_VOD, json=data, schema=validate.Schema(\n            validate.parse_json(),\n            {\"data\": {\n                \"video\": validate.any(None, {\n                    \"id\": str,\n                    \"title\": str,\n                    \"file_name\": str,\n                    \"video_recording_image_url\": str,\n                    \"channel\": {\"name\": str},\n                }),\n            }},\n            validate.get((\"data\", \"video\"))\n        ))\n\n        if not vod_data:\n            log.debug(\"Missing video data\")\n            return\n\n        log.trace(f\"vod_data={vod_data!r}\")\n\n        self.author = vod_data[\"channel\"][\"name\"]\n        self.category = \"VOD\"\n        self.title = vod_data[\"title\"]\n\n        netloc = urlparse(vod_data[\"video_recording_image_url\"]).netloc\n        hls_url = self.HLS_URL.format(\n            netloc=netloc,\n            file_name=vod_data[\"file_name\"]\n        )\n\n        return HLSStream.parse_variant_playlist(self.session, hls_url)\n\n    def _get_streams(self):\n        m = self.match.groupdict()\n\n        if (m['po'] == 'streampopout' or not m['po']) and m['user'] and not m['vod_id']:\n            log.debug('Type=Live')\n            return self.get_live(m['user'])\n        elif m['po'] == 'videopopout' or (m['user'] and m['vod_id']):\n            log.debug('Type=VOD')\n            vod_id = m['vod_id'] if m['vod_id'] else m['user']\n            return self.get_vod(vod_id)\n\n\n__plugin__ = Picarto\n", "path": "src/streamlink/plugins/picarto.py"}], "after_files": [{"content": "import logging\nimport re\nfrom urllib.parse import urlparse\n\nfrom streamlink.plugin import Plugin, pluginmatcher\nfrom streamlink.plugin.api import validate\nfrom streamlink.stream.hls import HLSStream\n\nlog = logging.getLogger(__name__)\n\n\n@pluginmatcher(re.compile(r\"\"\"\n    https?://(?:www\\.)?picarto\\.tv/\n    (?:\n        streampopout/(?P<po_user>[^/]+)/public\n    |\n        videopopout/(?P<po_vod_id>\\d+)\n    |\n        [^/]+/videos/(?P<vod_id>\\d+)\n    |\n        (?P<user>[^/?&]+)\n    )$\n\"\"\", re.VERBOSE))\nclass Picarto(Plugin):\n    API_URL_LIVE = \"https://ptvintern.picarto.tv/api/channel/detail/{username}\"\n    API_URL_VOD = \"https://ptvintern.picarto.tv/ptvapi\"\n    HLS_URL = \"https://{netloc}/stream/hls/{file_name}/index.m3u8\"\n\n    def get_live(self, username):\n        netloc = self.session.http.get(self.url, schema=validate.Schema(\n            validate.parse_html(),\n            validate.xml_xpath_string(\".//script[contains(@src,'/stream/player.js')][1]/@src\"),\n            validate.any(None, validate.transform(lambda src: urlparse(src).netloc))\n        ))\n        if not netloc:\n            log.error(\"Could not find server netloc\")\n            return\n\n        channel, multistreams = self.session.http.get(self.API_URL_LIVE.format(username=username), schema=validate.Schema(\n            validate.parse_json(),\n            {\n                \"channel\": validate.any(None, {\n                    \"stream_name\": str,\n                    \"title\": str,\n                    \"online\": bool,\n                    \"private\": bool,\n                    \"categories\": [{\"label\": str}],\n                }),\n                \"getMultiStreams\": validate.any(None, {\n                    \"multistream\": bool,\n                    \"streams\": [{\n                        \"name\": str,\n                        \"online\": bool,\n                    }],\n                }),\n            },\n            validate.union_get(\"channel\", \"getMultiStreams\")\n        ))\n        if not channel or not multistreams:\n            log.debug(\"Missing channel or streaming data\")\n            return\n\n        log.trace(f\"netloc={netloc!r}\")\n        log.trace(f\"channel={channel!r}\")\n        log.trace(f\"multistreams={multistreams!r}\")\n\n        if not channel[\"online\"]:\n            log.error(\"User is not online\")\n            return\n\n        if channel[\"private\"]:\n            log.info(\"This is a private stream\")\n            return\n\n        self.author = username\n        self.category = channel[\"categories\"][0][\"label\"]\n        self.title = channel[\"title\"]\n\n        hls_url = self.HLS_URL.format(\n            netloc=netloc,\n            file_name=channel[\"stream_name\"]\n        )\n\n        return HLSStream.parse_variant_playlist(self.session, hls_url)\n\n    def get_vod(self, vod_id):\n        data = {\n            'query': (\n                'query ($videoId: ID!) {\\n'\n                '  video(id: $videoId) {\\n'\n                '    id\\n'\n                '    title\\n'\n                '    file_name\\n'\n                '    video_recording_image_url\\n'\n                '    channel {\\n'\n                '      name\\n'\n                '      }'\n                '  }\\n'\n                '}\\n'\n            ),\n            'variables': {'videoId': vod_id},\n        }\n        vod_data = self.session.http.post(self.API_URL_VOD, json=data, schema=validate.Schema(\n            validate.parse_json(),\n            {\"data\": {\n                \"video\": validate.any(None, {\n                    \"id\": str,\n                    \"title\": str,\n                    \"file_name\": str,\n                    \"video_recording_image_url\": str,\n                    \"channel\": {\"name\": str},\n                }),\n            }},\n            validate.get((\"data\", \"video\"))\n        ))\n\n        if not vod_data:\n            log.debug(\"Missing video data\")\n            return\n\n        log.trace(f\"vod_data={vod_data!r}\")\n\n        self.author = vod_data[\"channel\"][\"name\"]\n        self.category = \"VOD\"\n        self.title = vod_data[\"title\"]\n\n        netloc = urlparse(vod_data[\"video_recording_image_url\"]).netloc\n        hls_url = self.HLS_URL.format(\n            netloc=netloc,\n            file_name=vod_data[\"file_name\"]\n        )\n\n        return HLSStream.parse_variant_playlist(self.session, hls_url)\n\n    def _get_streams(self):\n        m = self.match.groupdict()\n\n        if m['po_vod_id'] or m['vod_id']:\n            log.debug('Type=VOD')\n            return self.get_vod(m['po_vod_id'] or m['vod_id'])\n        elif m['po_user'] or m['user']:\n            log.debug('Type=Live')\n            return self.get_live(m['po_user'] or m['user'])\n\n\n__plugin__ = Picarto\n", "path": "src/streamlink/plugins/picarto.py"}]}