Repository: streamlink/streamlink
Pull request #1748 (2018-06-04 19:03:46)

dash: fix --json for dash streams and allow custom headers
Fixes the `--json` option when using `DASHStream`s. Enables custom headers to be added to the DASHStream request, eg. `User-Agent`, as per the other `Stream` classes. 

codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/1748?src=pr&el=h1) Report
> Merging [#1748](https://codecov.io/gh/streamlink/streamlink/pull/1748?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/62746e10ace39426cd930e35290853d71f8244ad?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `60%`.


```diff
@@            Coverage Diff             @@
##           master    #1748      +/-   ##
==========================================
+ Coverage   49.86%   49.86%   +<.01%     
==========================================
  Files         238      238              
  Lines       13977    13985       +8     
==========================================
+ Hits         6969     6973       +4     
- Misses       7008     7012       +4
```

beardypig: I should add some tests :)
gravyboat: Thanks for adding the tests @beardypig!

Patch:
diff --git a/src/streamlink/stream/dash.py b/src/streamlink/stream/dash.py
index ae214640..562190a1 100644
--- a/src/streamlink/stream/dash.py
+++ b/src/streamlink/stream/dash.py
@@ -3,8 +3,10 @@ import logging
 import datetime
 import os.path
 
+import requests
 from streamlink import StreamError, PluginError
 from streamlink.compat import urlparse, urlunparse
+from streamlink.stream.http import valid_args, normalize_key
 from streamlink.stream.stream import Stream
 from streamlink.stream.dash_manifest import MPD, sleeper, sleep_until, utc, freeze_timeline
 from streamlink.stream.ffmpegmux import FFMPEGMuxer
@@ -131,15 +133,24 @@ class DASHStream(Stream):
                  mpd,
                  video_representation=None,
                  audio_representation=None,
-                 period=0):
+                 period=0,
+                 **args):
         super(DASHStream, self).__init__(session)
         self.mpd = mpd
         self.video_representation = video_representation
         self.audio_representation = audio_representation
         self.period = period
+        self.args = args
+
+    def __json__(self):
+        req = requests.Request(method="GET", url=self.mpd.url, **valid_args(self.args))
+        req = req.prepare()
+
+        headers = dict(map(normalize_key, req.headers.items()))
+        return dict(type=type(self).shortname(), url=req.url, headers=headers)
 
     @classmethod
-    def parse_manifest(cls, session, url):
+    def parse_manifest(cls, session, url, **args):
         """
         Attempt to parse a DASH manifest file and return its streams
 
@@ -148,7 +159,7 @@ class DASHStream(Stream):
         :return: a dict of name -> DASHStream instances
         """
         ret = {}
-        res = session.http.get(url)
+        res = session.http.get(url, **args)
         url = res.url
 
         urlp = list(urlparse(url))
@@ -175,7 +186,7 @@ class DASHStream(Stream):
             audio = [None]
 
         for vid, aud in itertools.product(video, audio):
-            stream = DASHStream(session, mpd, vid, aud)
+            stream = DASHStream(session, mpd, vid, aud, **args)
             stream_name = []
 
             if vid:
diff --git a/src/streamlink/stream/hds.py b/src/streamlink/stream/hds.py
index a7ce39d2..ffece1a7 100644
--- a/src/streamlink/stream/hds.py
+++ b/src/streamlink/stream/hds.py
@@ -415,7 +415,9 @@ class HDSStream(Stream):
             metadata = self.metadata
 
         return dict(type=HDSStream.shortname(), baseurl=self.baseurl,
-                    url=self.url, bootstrap=bootstrap, metadata=metadata)
+                    url=self.url, bootstrap=bootstrap, metadata=metadata,
+                    params=self.request_params.get("params", {}),
+                    headers=self.request_params.get("headers", {}))
 
     def open(self):
         reader = HDSStreamReader(self)


Test patch:
diff --git a/tests/test_stream_json.py b/tests/test_stream_json.py
new file mode 100644
index 00000000..967672f2
--- /dev/null
+++ b/tests/test_stream_json.py
@@ -0,0 +1,93 @@
+import unittest
+
+from streamlink import Streamlink
+from streamlink.stream import AkamaiHDStream
+from streamlink.stream import HDSStream
+from streamlink.stream import HLSStream
+from streamlink.stream import HTTPStream
+from streamlink.stream import RTMPStream
+from streamlink.stream import Stream
+
+
+class TestStreamToJSON(unittest.TestCase):
+    def setUp(self):
+        self.session = Streamlink()
+
+    def test_base_stream(self):
+        stream = Stream(self.session)
+        self.assertEqual(
+            {"type": "stream"},
+            stream.__json__()
+        )
+
+    def test_http_stream(self):
+        url = "http://test.se/stream"
+        stream = HTTPStream(self.session, url, headers={"User-Agent": "Test"})
+        self.assertEqual(
+            {"type": "http",
+             "url": url,
+             "method": "GET",
+             "body": None,
+             "headers": {
+                 "User-Agent": "Test",
+                 "Accept": "*/*",
+                 "Accept-Encoding": "gzip, deflate",
+                 "Connection": "keep-alive",
+             }},
+            stream.__json__()
+        )
+
+    def test_hls_stream(self):
+        url = "http://test.se/stream.m3u8"
+        stream = HLSStream(self.session, url, headers={"User-Agent": "Test"})
+        self.assertEqual(
+            {"type": "hls",
+             "url": url,
+             "headers": {
+                 "User-Agent": "Test",
+                 "Accept": "*/*",
+                 "Accept-Encoding": "gzip, deflate",
+                 "Connection": "keep-alive",
+             }},
+            stream.__json__()
+        )
+
+    def test_hds_stream(self):
+        stream = HDSStream(self.session, "http://test.se/", "http://test.se/stream.f4m",
+                           "http://test.se/stream/1.bootstrap", headers={"User-Agent": "Test"})
+        self.assertEqual(
+            {"type": "hds",
+             "baseurl": "http://test.se/",
+             "bootstrap": "http://test.se/stream/1.bootstrap",
+             "url": "http://test.se/stream.f4m",
+             "metadata": None,
+             "headers": {"User-Agent": "Test"},
+             "params": {}},
+            stream.__json__()
+        )
+
+    def test_akamai_stream(self):
+        stream = AkamaiHDStream(self.session, "http://akamai.test.se/stream")
+        self.assertEqual(
+            {'swf': None,
+             'type': 'akamaihd',
+             'url': 'http://akamai.test.se/stream'},
+            stream.__json__()
+        )
+
+    def test_rtmp_stream(self):
+        stream = RTMPStream(self.session, {"rtmp": "rtmp://test.se/app/play_path",
+                                           "swfVfy": "http://test.se/player.swf",
+                                           "swfhash": "test",
+                                           "swfsize": 123456,
+                                           "playPath": "play_path"})
+        self.assertEqual(
+            {"type": "rtmp",
+             "args": [],
+             "params": {"rtmp": "rtmp://test.se/app/play_path",
+                        "swfVfy": "http://test.se/player.swf",
+                        "swfhash": "test",
+                        "swfsize": 123456,
+                        "playPath": "play_path"}},
+            stream.__json__()
+        )