Repository: streamlink/streamlink
Pull request #3239 (2020-10-09 10:13:09)

plugins.dlive: fix live playback

Billy2011: @bastimeyer why did you remove the vod support?
bastimeyer: @Billy2011 Apologies, I was under the impression that it was broken for now and removed it without checking it. Re-implemented it now.

Also hope it's okay that I've pushed onto your PR branch, but I didn't want to add review comments for a proper rewrite. Squashing this PR will preserve the authors of all commits.
Billy2011: > Also hope it's okay that I've pushed onto your PR branch, but I didn't want to add review comments for a proper rewrite.

That wasn’t so good for me. I wanted to make a git rebase, but since I didn’t notice your commit, my local branch is now broken due to several failed attempts.
bastimeyer: There is no "broken" in git. Just fetch your fork's remote, and then [force your local branch to the commit ID](https://git-scm.com/docs/git-branch#Documentation/git-branch.txt---force) (`84cd2f0`) of the updated branch on the fork repo to re-align them.
Billy2011: > Just fetch your fork's remote,

I’ve tried that a few times, but your changes don’t appear in my commit history.
bastimeyer: Now you've pushed a merge commit onto the PR branch for some reason and it includes unrelated commits that disappear in the overall diff as those are already on the master branch. That's not how it's supposed to be. I don't understand why you'd want to rebase, as the PR will get squashed on merge anyway.

Just keep it as it is now, please.
And btw, the coverage status failure is caused by something else, so don't worry about it.
Billy2011: @bastimeyer I think it’s better if you do another force push.
Robby-: I'm [side-loading](https://streamlink.github.io/cli.html#sideloading-plugins) this fixed version of the plugin and testing it with live streams, and it seems to work nicely.
Billy2011: > However, the test link specified in test_dlive.py doesn't work:

This only checks the validity of the link.

But this link runs: [https://dlive.tv/p/pdp+3mTzOl4WR](https://dlive.tv/p/pdp+3mTzOl4WR)

Patch:
diff --git a/src/streamlink/plugins/dlive.py b/src/streamlink/plugins/dlive.py
index dac758e6..b457f3da 100644
--- a/src/streamlink/plugins/dlive.py
+++ b/src/streamlink/plugins/dlive.py
@@ -1,8 +1,14 @@
+import json
+import logging
 import re
 
+from streamlink.compat import is_py3, unquote_plus
 from streamlink.plugin import Plugin, PluginError
+from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
-from streamlink.compat import unquote_plus, is_py3
+
+
+log = logging.getLogger(__name__)
 
 
 QUALITY_WEIGHTS = {
@@ -11,18 +17,43 @@ QUALITY_WEIGHTS = {
 
 
 class DLive(Plugin):
-    """
-    Plugin for dlive.tv
-    """
-
-    _url_re = re.compile(r"https?://(?:www\.)?dlive\.tv/")
-    _playback_re = re.compile(r"""(?<=playbackUrl":")(.+?)(?=")""")
-    _livestream_re = re.compile(r""""livestream":null""")
-    _username_re = re.compile(r"(?<=user:)(\w|-)+")
+    _re_url = re.compile(r"""
+        https?://(?:www\.)?dlive\.tv/
+        (?:
+            (?:p/(?P<video>[^/]+))
+            |
+            (?P<channel>[^/]+)
+        )
+    """, re.VERBOSE)
+    _re_videoPlaybackUrl = re.compile(r'"playbackUrl"\s*:\s*"([^"]+\.m3u8)"')
+
+    _schema_userByDisplayName = validate.Schema({
+        "data": {
+            "userByDisplayName": {
+                "livestream": validate.any(None, {
+                    "title": validate.text
+                }),
+                "username": validate.text
+            }
+        }},
+        validate.get("data"),
+        validate.get("userByDisplayName")
+    )
+    _schema_videoPlaybackUrl = validate.Schema(
+        validate.transform(_re_videoPlaybackUrl.search),
+        validate.any(None, validate.all(
+            validate.get(1),
+            validate.transform(unquote_plus),
+            validate.transform(lambda url: (
+                bytes(url, "utf-8").decode("unicode_escape") if is_py3 else url.decode("unicode_escape")
+            )),
+            validate.url()
+        ))
+    )
 
     @classmethod
     def can_handle_url(cls, url):
-        return cls._url_re.match(url)
+        return cls._re_url.match(url)
 
     @classmethod
     def stream_weight(cls, key):
@@ -32,31 +63,62 @@ class DLive(Plugin):
 
         return Plugin.stream_weight(key)
 
-    def _get_streams(self):
-        res = self.session.http.get(self.url)
+    def __init__(self, *args, **kwargs):
+        super(DLive, self).__init__(*args, **kwargs)
+        self.author = None
+        self.title = None
 
-        m = self._playback_re.search(res.text)
+        match = self._re_url.match(self.url)
+        self.video = match.group("video")
+        self.channel = match.group("channel")
 
-        if m:
-            hls_url = m.group(0)
+    def get_author(self):
+        return self.author
 
-            if is_py3:
-                hls_url = bytes(unquote_plus(hls_url), "utf-8").decode(
-                    "unicode_escape")
-            else:
-                hls_url = unquote_plus(hls_url).decode("unicode_escape")
-        else:
-            if self._livestream_re.search(res.text) is not None:
-                raise PluginError('Stream is offline')
+    def get_title(self):
+        return self.title
 
-            m = self._username_re.search(res.text)
-            if m:
-                hls_url = "https://live.prd.dlive.tv/hls/live/{}.m3u8".format(
-                    m.group(0))
-            else:
-                raise PluginError('Could not find username')
+    def _get_streams_video(self):
+        log.debug("Getting video HLS streams for {0}".format(self.video))
+        try:
+            hls_url = self.session.http.get(self.url, schema=self._schema_videoPlaybackUrl)
+            if hls_url is None:
+                return
+        except PluginError:
+            return
 
         return HLSStream.parse_variant_playlist(self.session, hls_url)
 
+    def _get_streams_live(self):
+        log.debug("Getting live HLS streams for {0}".format(self.channel))
+        try:
+            data = json.dumps({"query": """query {{
+                userByDisplayName(displayname:"{displayname}") {{
+                    livestream {{
+                        title
+                    }}
+                    username
+                }}
+            }}""".format(displayname=self.channel)})
+            res = self.session.http.post("https://graphigo.prd.dlive.tv/", data=data)
+            res = self.session.http.json(res, schema=self._schema_userByDisplayName)
+            if res["livestream"] is None:
+                return
+        except PluginError:
+            return
+
+        self.author = self.channel
+        self.title = res["livestream"]["title"]
+
+        hls_url = "https://live.prd.dlive.tv/hls/live/{0}.m3u8".format(res["username"])
+
+        return HLSStream.parse_variant_playlist(self.session, hls_url)
+
+    def _get_streams(self):
+        if self.video:
+            return self._get_streams_video()
+        elif self.channel:
+            return self._get_streams_live()
+
 
 __plugin__ = DLive


Test patch:
diff --git a/tests/plugins/test_dlive.py b/tests/plugins/test_dlive.py
index 5e81eb27..b3233a51 100644
--- a/tests/plugins/test_dlive.py
+++ b/tests/plugins/test_dlive.py
@@ -7,6 +7,7 @@ class TestPluginDLive(unittest.TestCase):
     def test_can_handle_url(self):
         # should match
         self.assertTrue(DLive.can_handle_url("https://dlive.tv/pewdiepie"))
+        self.assertTrue(DLive.can_handle_url("https://dlive.tv/p/pdp+K6DqqtYWR"))
 
         # shouldn't match
-        self.assertFalse(DLive.can_handle_url("https://twitch.tv/lirik"))
+        self.assertFalse(DLive.can_handle_url("https://dlive.tv/"))