Repository: streamlink/streamlink

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
```



Discussion:
It looks like they must have changed things a little bit.  Previously it wasn't possible to get a canonical URL anywhere except via the popout link for VoDs.  VoDs on profile pages now appear to link via a canonical URL though.

What is happening here is that the current URL logic is identifying the URL as live rather than VoD, so it's going via the method to handle a live stream instead of a VoD.  An update to the URL logic should fix it I think.

Edit: I'll put in a PR later today with fix and updated tests


Resolving patch:
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_vod(self, vod_id):
     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


Test patch:
diff --git a/tests/plugins/test_picarto.py b/tests/plugins/test_picarto.py
--- a/tests/plugins/test_picarto.py
+++ b/tests/plugins/test_picarto.py
@@ -8,7 +8,15 @@ class TestPluginCanHandleUrlPicarto(PluginCanHandleUrl):
     should_match = [
         'https://picarto.tv/example',
         'https://www.picarto.tv/example',
+        'https://www.picarto.tv/example/videos/123456',
         'https://www.picarto.tv/streampopout/example/public',
         'https://www.picarto.tv/videopopout/123456',
-        'https://www.picarto.tv/example?tab=videos&id=123456',
+    ]
+
+    should_not_match = [
+        'https://picarto.tv/',
+        'https://www.picarto.tv/example/',
+        'https://www.picarto.tv/example/videos/abc123',
+        'https://www.picarto.tv/streampopout/example/notpublic',
+        'https://www.picarto.tv/videopopout/abc123',
     ]