Repository: streamlink/streamlink

Issue:
Support for wasd.tv
There is 2 type of links. For example:
https://wasd.tv/channel/86692/videos/269793
https://wasd.tv/channel/86692
Both of them can be live stream or replay of past stream.

What I will say next is not confirmed by any documentation or anything its just how I think this website working.

For example user call streamlink for URL https://wasd.tv/channel/86692
Then we request
https://wasd.tv/api/media-containers?media_container_status=RUNNING&limit=1&offset=0&channel_id=86692&media_container_type=SINGLE,COOP
Response is in JSON format.
*I replaced `media_container_status=STOPPED,RUNNING` to just `RUNNING`. I think if user calls streamlink without /videos/ part in URL he wants live stream. Not past record.
If there is no live stream then response will be `{"result":[]}`
From response we need `json["result"][0]["user_id"]`. For example this channel's `user_id=179599`
Then we look into array `json["result"][0]["media_container_streams"]` and search for element where `json["result"][0]["media_container_streams"][i]["user_id"]` is equals to channel's `user_id=179599`
Then inside that element of array we get another element `json["result"][0]["media_container_streams"][i]["stream_media"]` which has different streams with different quality. For example:
`json["result"][0]["media_container_streams"][0]["stream_media"][0]["media_type"]="HLS"`
`json["result"][0]["media_container_streams"][0]["stream_media"][0]["media_status"]="RUNNING"`
`json["result"][0]["media_container_streams"][0]["stream_media"][0]["media_meta"]["media_current_resolution"]="1280x720"`
`json["result"][0]["media_container_streams"][0]["stream_media"][0]["media_meta"]["media_url"]="https://cdn.wasd.tv/live/179599/index.m3u8"`

Now if user call streamlink for URL https://wasd.tv/channel/86692/videos/269793
We request https://wasd.tv/api/media-containers/269793
And its basically same response and we need to do same job here. 
If this is live stream then in `json["result"][0]["media_container_streams"][0]["stream_media"]` array will be element with `media_status="RUNNING"`
If this is replay of past stream then in `json["result"][0]["media_container_streams"][0]["stream_media"]` array will be element with `media_status="STOPPED"`
And instead of `media_url` we need to get `media_archive_url` which will be `.m3u8` file.

I have checked other plugins code and its too hard for me to make it myself. 
I hope somebody will be able to do it with results of my "investigation".


Discussion:
I attempted to do it myself and it was easier than I expected until I got to HLS part.
This is my firs experience with streamlink and third time using python. And I have no idea how HLS works.
WASD has array of resolutions in its JSON files but I can't find stream with more that one resolution to select. Looks like they not implemented that yet.
So I just give streamlink first HLS URL that I find (there is only one at the moment) and it works. And even detects resolution using some HLS voodoo magic.
So it works, but I don't know if I did everything correctly.
Both live streams and records works.

Resolving patch:
diff --git a/src/streamlink/plugins/wasd.py b/src/streamlink/plugins/wasd.py
new file mode 100644
--- /dev/null
+++ b/src/streamlink/plugins/wasd.py
@@ -0,0 +1,87 @@
+import logging
+import re
+
+from streamlink.exceptions import PluginError
+from streamlink.plugin import Plugin
+from streamlink.stream import HLSStream
+from streamlink.plugin.api import validate
+
+log = logging.getLogger(__name__)
+
+
+class WASD(Plugin):
+    _url_re = re.compile(r'https?://(?:www\.)?wasd\.tv/channel/(?P<channel_id>\d+)(?:/videos/(?P<video_id>\d+))?')
+    _media_schema = validate.Schema({
+        'user_id': int,
+        'media_container_streams': [{
+            'stream_status': validate.text,
+            'stream_online_status': validate.text,
+            'user_id': int,
+            'stream_media': [{
+                'media_id': int,
+                'media_meta': {
+                    'media_url': validate.any(validate.text, None),
+                    'media_archive_url': validate.any(validate.text, None),
+                },
+                'media_status': validate.any('STOPPED', 'RUNNING'),
+                'media_type': 'HLS',
+                'user_id': int,
+            }]
+        }],
+    })
+    _api_schema = validate.Schema({
+        'result':
+            validate.any(
+                _media_schema,
+                validate.all(list,
+                             validate.get(0),
+                             _media_schema),
+                [],
+            ),
+    }, validate.get('result'))
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        m = self._url_re.match(self.url)
+        video_id = m.group('video_id')
+        channel_id = m.group('channel_id')
+
+        self.session.http.post('https://wasd.tv/api/auth/anon-token')
+
+        if video_id:
+            res = self.session.http.get('https://wasd.tv/api/media-containers/{0}'.format(video_id))
+        else:
+            res = self.session.http.get(
+                'https://wasd.tv/api/media-containers',
+                params={
+                    'media_container_status': 'RUNNING',
+                    'limit': '1',
+                    'offset': '0',
+                    'channel_id': channel_id,
+                    'media_container_type': 'SINGLE,COOP',
+                }
+            )
+
+        json_res = self.session.http.json(res, schema=self._api_schema)
+        log.trace('{0!r}'.format(json_res))
+        if not json_res:
+            raise PluginError('No data returned from URL={0}'.format(res.url))
+
+        for stream in json_res['media_container_streams']:
+            if stream['user_id'] == json_res['user_id']:
+                log.debug('stream_status: {0}, stream_online_status: {1}'.format(
+                    stream['stream_status'], stream['stream_online_status']))
+                for stream in stream['stream_media']:
+                    if stream['media_status'] == 'STOPPED':
+                        hls_url = stream['media_meta']['media_archive_url']
+                    elif stream['media_status'] == 'RUNNING':
+                        hls_url = stream['media_meta']['media_url']
+
+                    for s in HLSStream.parse_variant_playlist(self.session, hls_url).items():
+                        yield s
+
+
+__plugin__ = WASD


Test patch:
diff --git a/tests/plugins/test_wasd.py b/tests/plugins/test_wasd.py
new file mode 100644
--- /dev/null
+++ b/tests/plugins/test_wasd.py
@@ -0,0 +1,20 @@
+import unittest
+
+from streamlink.plugins.wasd import WASD
+
+
+class TestPluginWasd(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'https://wasd.tv/channel/12345',
+            'https://wasd.tv/channel/12345/videos/67890'
+        ]
+        for url in should_match:
+            self.assertTrue(WASD.can_handle_url(url), url)
+
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            'https://example.com/index.html',
+        ]
+        for url in should_not_match:
+            self.assertFalse(WASD.can_handle_url(url), url)