Repository: streamlink/streamlink

Issue:
pandalive.co.kr (not to be confused with panda.tv)
### Checklist

- [X] This is a plugin request 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 requests](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+request%22)

### Description

Pandalive is a Korean live streaming site similar to afreecatv. Previous tickets were closed with "more info required" but was never explained why ticket was rejected. 

It has the same name as the panda.tv which is a closed down china site, which in the old ticket others have confused with.

If this ticket gets closed, I hope there would at least be a reason/explanation on why it might be it (like in the previous closed ticket, what exactly is "does not meet requirements"? Seeing that thee list has many possible reasons the plugin might not be added but was never mentioned in the closed ticket.

Thanks

### Input URLs

https://www.pandalive.co.kr/


Discussion:
Hi, I managed to grab the stream url from the site (apparently the site uses WebSocket to get the stream url)

Demo: https://replit.com/@Trung0246/BowedImperfectZettabyte#main.py
Gist: https://gist.github.com/Trung0246/2676e0180c7d68c0713d96721a206a13

![image](https://user-images.githubusercontent.com/11626920/127076689-18febac8-a9cc-42e1-aed4-524ce9423d18.png)

Apparently there's password protected stream but no idea about that yet, beside that anything else I missed?
Also looks like the stream url is ip-dependent so that ip is from repl.it server ip
The reason past requests for this plugin have been closed is because they have not sufficiently met the requirements as set out in the first paragraph of the plugin request advice detailed at: https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#plugin-requests

Also, plugin requests will only be considered if a site meets the 11 requirements also set out there.

I'm unsure what the viewership numbers are for the live streams on the site.  There's a person icon with a number next to it in the range of ~10 to ~40, which could be the current number of viewers?  There's also a bit on the stream page itself which translates to `cumulative` in English with a number next to it in the hundreds of thousands.  I'm not sure what that is, it's probably not a great translation (total number of views for the channel?).

I can't tell you what viewership figures might justify a plugin addition to Streamlink, as it's up to the maintainers and there seems to be no quotable magic figure that I'm aware of.

> Apparently there's password protected stream

When you say `password protected`, do you mean that would require an account and to be signed in to the website?  Do these streams require any form of monetary subscription to view?  If so, they are unlikely to be supported because they are difficult to maintain.

> When you say `password protected`, do you mean that would require an account and to be signed in to the website? Do these streams require any form of monetary subscription to view? If so, they are unlikely to be supported because they are difficult to maintain.

I'm not really sure which type of password protected stream but by looking at the js code it's seems like you don't even needs to login and only needs the password that are associated with the stream. Needs more investigation of course.

Resolving patch:
diff --git a/src/streamlink/plugins/pandalive.py b/src/streamlink/plugins/pandalive.py
new file mode 100644
--- /dev/null
+++ b/src/streamlink/plugins/pandalive.py
@@ -0,0 +1,79 @@
+import logging
+import re
+
+from streamlink.plugin import Plugin, pluginmatcher
+from streamlink.plugin.api import validate
+from streamlink.stream.hls import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+@pluginmatcher(re.compile(
+    r"https?://(?:www\.)?pandalive\.co\.kr/"
+))
+class Pandalive(Plugin):
+    _room_id_re = re.compile(r"roomid\s*=\s*String\.fromCharCode\((.*)\)")
+
+    def _get_streams(self):
+        media_code = self.session.http.get(self.url, schema=validate.Schema(
+            validate.parse_html(),
+            validate.xml_xpath_string(".//script[contains(text(), 'roomid')]/text()"),
+            validate.any(None, validate.all(
+                validate.transform(self._room_id_re.search),
+                validate.any(None, validate.all(
+                    validate.get(1),
+                    validate.transform(lambda s: "".join(map(lambda c: chr(int(c)), s.split(",")))),
+                )),
+            )),
+        ))
+
+        if not media_code:
+            return
+
+        log.debug(f"Media code: {media_code}")
+
+        json = self.session.http.post(
+            "https://api.pandalive.co.kr/v1/live/play",
+            data={"action": "watch", "mediaCode": media_code},
+            schema=validate.Schema(
+                validate.parse_json(), {
+                    validate.optional("media"): {
+                        "title": str,
+                        "userId": str,
+                        "userNick": str,
+                        "isPw": bool,
+                        "isLive": bool,
+                        "liveType": str,
+                    },
+                    validate.optional("PlayList"): {
+                        "hls2": [{
+                            "url": validate.url(),
+                        }],
+                    },
+                    "result": bool,
+                    "message": str,
+                },
+            )
+        )
+
+        if not json["result"]:
+            log.error(json["message"])
+            return
+
+        if not json["media"]["isLive"]:
+            log.error("The broadcast has ended")
+            return
+
+        if json["media"]["isPw"]:
+            log.error("The broadcast is password protected")
+            return
+
+        log.info(f"Broadcast type: {json['media']['liveType']}")
+
+        self.author = f"{json['media']['userNick']} ({json['media']['userId']})"
+        self.title = f"{json['media']['title']}"
+
+        return HLSStream.parse_variant_playlist(self.session, json["PlayList"]["hls2"][0]["url"])
+
+
+__plugin__ = Pandalive


Test patch:
diff --git a/tests/plugins/test_pandalive.py b/tests/plugins/test_pandalive.py
new file mode 100644
--- /dev/null
+++ b/tests/plugins/test_pandalive.py
@@ -0,0 +1,17 @@
+from streamlink.plugins.pandalive import Pandalive
+from tests.plugins import PluginCanHandleUrl
+
+
+class TestPluginCanHandleUrlPandalive(PluginCanHandleUrl):
+    __plugin__ = Pandalive
+
+    should_match = [
+        "http://pandalive.co.kr/",
+        "http://pandalive.co.kr/any/path",
+        "http://www.pandalive.co.kr/",
+        "http://www.pandalive.co.kr/any/path",
+        "https://pandalive.co.kr/",
+        "https://pandalive.co.kr/any/path",
+        "https://www.pandalive.co.kr/",
+        "https://www.pandalive.co.kr/any/path",
+    ]