Repository: streamlink/streamlink (python)

Issue:
plugin rerquest for mjunoon
https://www.mjunoon.tv/

streamlink https://www.mjunoon.tv/a-tv-live
[cli][info] streamlink is running as root! Be careful!
error: No plugin can handle URL: https://www.mjunoon.tv/a-tv-live


Pull request description:
plugins.mjunoon: support for live and vod streams on mjunoon.tv
Support for both live and VOD streams from mjunoon.tv. 

Would be nice to have someone test it out. 

Fixes #1834 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 74a24292..c3bbeba7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,83 +1,5 @@
 # Changelog
 
-## streamlink 0.14.0 (2018-06-26)
-
-Here are the changes to this months release!
-
-- Multiple plugin fixes
-- Bug fixes for DASH streams (https://github.com/streamlink/streamlink/pull/1846)
-- Updated API call for api.utils hours_minutes_seconds (https://github.com/streamlink/streamlink/pull/1804)
-- Updated documentation (https://github.com/streamlink/streamlink/pull/1826)
-- Dict structures fix (https://github.com/streamlink/streamlink/pull/1792)
-- Reformated help menu (https://github.com/streamlink/streamlink/pull/1754)
-- Logger fix (https://github.com/streamlink/streamlink/pull/1773)
-
-```text
-Alexis Murzeau <amubtdx@gmail.com> (3):
-      sdist: include tests resources (#1785)
-      tests: freezegun: use object instead of lambda (#1787)
-      rtlxl: use raw string to fix escape sequences (#1786)
-
-BZHDeveloper <39899575+BZHDeveloper@users.noreply.github.com> (1):
-      plugins.cnews : separate CNEWS data from CanalPlus plugin. (#1782)
-
-MasterofJOKers <joker@someserver.de> (1):
-      plugins.sportschau: Fix "No schema supplied" error
-
-Mohamed El Morabity <melmorabity@fedoraproject.com> (2):
-      plugins.pluzz: support for DASH streams
-      plugins.pluzz: fix HDS streams
-
-Mohamed El Morabity <melmorabity@users.noreply.github.com> (1):
-      [plugins.rtbf] Fix radio streams + DASH support (#1771)
-
-Sebastian Meyer <mail@bastimeyer.de> (1):
-      Move docs version selection to sidebar (#1802)
-
-Twilight0 <twilight@freemail.gr> (2):
-      Convert literal comprehensive dicts to dict contructs
-      Add arguments to __init__ - super to work on Python 2.7.X (#1796)
-
-back-to <backto@protonmail.ch> (9):
-      plugins: marked or removed broken plugins
-      plugins.earthcam: Fixed hls_url - No schema supplied.
-      plugins.rte: allow https
-      plugins.VinhLongTV: New plugin for livestreams of thvli.vn
-      docs.thirdparty: Added LiveProxy
-      plugins.tlctr: New Plugin for tlctv.com.tr/canli-izle
-      plugins.bigo: Fix for new channelnames and plugin cleanup (#1797)
-      docs: removed some notes, updated some urls
-      utils.times: hours_minutes_seconds update, twitch automate time offset
-
-beardypig <beardypig@protonmail.com> (15):
-      help: reformat all the help text so that it is <80 chars
-      plugins.bbciplayer: fix bug is DASH for VOD
-      sdist and wheel release fixes (#1758)
-      plugin.youtube: find video id in page, fall back to API (#1746)
-      logging: rename logger for main back to 'cli'
-      plugins.vaughnlive: support for the HTML flv player
-      plugins.yupptv: support for yupptv with login support
-      plugins.nos: update for new page layout for live and VOD
-      plugins.lrt: add support for Lithuanian National Television
-      plugins.delfi: support for delfi.lt web portal
-      plugins.vrtbe: update to new page layout/API
-      plugins.itvplayer: update to new HTML5 API
-      plugins.atresplayer: support new layout/API
-      stream.dash: use the ID and mime-type to identify a representation
-      plugins.mitele: sometimes ogn is null, html5 pdata endpoint works better
-
-beardypig <git@beardypig.com> (5):
-      plugins.crunchyroll: refactoring and updated API calls (#1820)
-      Suppress/fix deprecated warnings for Python 3 (#1833)
-      API for plugins to request input from the user (#1827)
-      Steam Broadcast Plugin (#1717)
-      USTV Now (#1840)
-
-fozzy <fozzy@fozzy.co> (1):
-      fix bug caused by indentation and add support for url pattern like 'xingxiu.panda.tv'
-```
-
-
 ## streamlink 0.13.0 (2018-06-06)
 
 Massive release this month!
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 28c0d06a..d0efcd25 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -123,6 +123,7 @@ mediaklikk              mediaklikk.hu        Yes   No    Streams may be geo-rest
 mips                    mips.tv              Yes   --    Requires rtmpdump with K-S-V patches.
 mitele                  mitele.es            Yes   No    Streams may be geo-restricted to Spain.
 mixer                   mixer.com            Yes   Yes
+mjunoon                 mjunoon.tv           Yes   Yes
 mlgtv                   mlg.tv               Yes   --
 nbc                     nbc.com              No    Yes   Streams are geo-restricted to USA. Authentication is not supported.
 nbcsports               nbcsports.com        No    Yes   Streams maybe be geo-restricted to USA. Authentication is not supported.
diff --git a/src/streamlink/plugins/atresplayer.py b/src/streamlink/plugins/atresplayer.py
index bc3348ce..b0c4f15d 100644
--- a/src/streamlink/plugins/atresplayer.py
+++ b/src/streamlink/plugins/atresplayer.py
@@ -2,18 +2,17 @@ from __future__ import print_function
 
 import logging
 import re
-from functools import partial
 
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import http, validate, StreamMapper
-from streamlink.stream import HLSStream, DASHStream
-from streamlink.utils import parse_json, update_scheme, search_dict
+from streamlink.plugin.api import http, validate
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_json, update_scheme
 
 log = logging.getLogger(__name__)
 
 
 class AtresPlayer(Plugin):
-    url_re = re.compile(r"https?://(?:www.)?atresplayer.com/")
+    url_re = re.compile(r"https?://(?:www.)?atresplayer.com/directos/([\w-]+)/?")
     state_re = re.compile(r"""window.__PRELOADED_STATE__\s*=\s*({.*?});""", re.DOTALL)
     channel_id_schema = validate.Schema(
         validate.transform(state_re.search),
@@ -22,17 +21,19 @@ class AtresPlayer(Plugin):
             validate.all(
                 validate.get(1),
                 validate.transform(parse_json),
-                validate.transform(partial(search_dict, key="urlVideo"))
-            )
-        )
-    )
+                {
+                    "programming": {
+                        validate.text: validate.all({"urlVideo": validate.text},
+                                                    validate.get("urlVideo"))
+                    }
+                },
+                validate.get("programming")
+            )))
     stream_schema = validate.Schema(
         validate.transform(parse_json),
         {"sources": [
-            validate.all({
-                "src": validate.url(),
-                validate.optional("type"): validate.text
-            })
+            validate.all({"src": validate.url()},
+                         validate.get("src"))
         ]}, validate.get("sources"))
 
 
@@ -45,18 +46,11 @@ class AtresPlayer(Plugin):
         super(AtresPlayer, self).__init__(update_scheme("https://", url))
 
     def _get_streams(self):
-        api_urls = http.get(self.url, schema=self.channel_id_schema)
-        for api_url in api_urls:
-            log.debug("API URL: {0}".format(api_url))
-            for source in http.get(api_url, schema=self.stream_schema):
-                log.debug("Stream source: {0} ({1})".format(source['src'], source.get("type", "n/a")))
-
-                if "type" not in source or source["type"] == "application/vnd.apple.mpegurl":
-                    for s in HLSStream.parse_variant_playlist(self.session, source["src"]).items():
-                        yield s
-                elif source["type"] == "application/dash+xml":
-                    for s in DASHStream.parse_manifest(self.session, source["src"]).items():
-                        yield s
+        programming = http.get(self.url, schema=self.channel_id_schema)
+        for api_url in programming.values():
+            for src in http.get(api_url, schema=self.stream_schema):
+                for s in HLSStream.parse_variant_playlist(self.session, src).items():
+                    yield s
 
 
 __plugin__ = AtresPlayer
diff --git a/src/streamlink/plugins/mjunoon.py b/src/streamlink/plugins/mjunoon.py
new file mode 100644
index 00000000..ebf874fc
--- /dev/null
+++ b/src/streamlink/plugins/mjunoon.py
@@ -0,0 +1,34 @@
+import logging
+import re
+
+from streamlink.compat import urlparse, parse_qsl
+from streamlink.plugin import Plugin
+from streamlink.plugin.api.utils import itertags
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class Mjunoon(Plugin):
+    url_re = re.compile(r"""https?://(?:www.)?mjunoon.tv/""", re.VERBOSE)
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def _get_streams(self):
+        res = self.session.http.get(self.url)
+        for script in itertags(res.text, 'script'):
+            if script.attributes.get("id") == "playerScript":
+                log.debug("Found the playerScript script tag")
+                urlparts = urlparse(script.attributes.get("src"))
+                i = 0
+
+                for key, url in parse_qsl(urlparts.query):
+                    if key == "streamUrl":
+                        i += 1
+                        for s in HLSStream.parse_variant_playlist(self.session, url, params=dict(id=i), verify=False).items():
+                            yield s
+
+
+__plugin__ = Mjunoon
diff --git a/src/streamlink/plugins/tv3cat.py b/src/streamlink/plugins/tv3cat.py
index 52f31ba3..167b917c 100644
--- a/src/streamlink/plugins/tv3cat.py
+++ b/src/streamlink/plugins/tv3cat.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-import logging
 import re
 
 from streamlink.plugin import Plugin
@@ -7,40 +6,46 @@ from streamlink.plugin.api import http
 from streamlink.stream import HLSStream
 from streamlink.plugin.api import validate
 
-log = logging.getLogger(__name__)
+STREAM_INFO_URL = "http://dinamics.ccma.cat/pvideo/media.jsp?media=video&version=0s&idint={ident}&profile=pc&desplacament=0"
+_url_re = re.compile(r"http://(?:www.)?ccma.cat/tv3/directe/(.+?)/")
+_media_schema = validate.Schema({
+    "geo": validate.text,
+    "url": validate.url(scheme=validate.any("http"))
+})
+_channel_schema = validate.Schema({
+    "media": validate.any([_media_schema], _media_schema)
+})
 
 
 class TV3Cat(Plugin):
-    _url_re = re.compile(r"http://(?:www.)?ccma.cat/tv3/directe/(.+?)/")
-    _stream_info_url = "http://dinamics.ccma.cat/pvideo/media.jsp?media=video&version=0s&idint={ident}&profile=pc&desplacament=0"
-    _media_schema = validate.Schema({
-        "geo": validate.text,
-        "url": validate.url(scheme=validate.any("http", "https"))
-    })
-    _channel_schema = validate.Schema({
-        "media": validate.any([_media_schema], _media_schema)},
-        validate.get("media"),
-        # If there is only one item, it's not a list ... silly
-        validate.transform(lambda x: x if isinstance(x, list) else [x])
-    )
-
     @classmethod
-    def can_handle_url(cls, url):
-        return cls._url_re.match(url) is not None
+    def can_handle_url(self, url):
+        match = _url_re.match(url)
+        return match
 
     def _get_streams(self):
-        match = self._url_re.match(self.url)
+
+        match = _url_re.match(self.url)
         if match:
             ident = match.group(1)
-            data_url = self._stream_info_url.format(ident=ident)
-            stream_infos = http.json(http.get(data_url), schema=self._channel_schema)
+            data_url = STREAM_INFO_URL.format(ident=ident)
+
+            # find the region, default to TOTS (international)
+            res = http.get(self.url)
+            geo_data = re.search(r'data-geo="([A-Z]+?)"', res.text)
+            geo = geo_data and geo_data.group(1) or "TOTS"
+
+            stream_data = http.json(http.get(data_url), schema=_channel_schema)
+
+            # If there is only one item, it's not a list ... silly
+            if isinstance(stream_data['media'], list):
+                stream_infos = stream_data['media']
+            else:
+                stream_infos = [stream_data['media']]
 
             for stream in stream_infos:
-                try:
-                    return HLSStream.parse_variant_playlist(self.session, stream['url'], name_fmt="{pixels}_{bitrate}")
-                except:
-                    log.debug("Failed to get streams for: {0}".format(stream['geo']))
-                    pass
+                if stream['geo'] == geo:
+                    return HLSStream.parse_variant_playlist(self.session, stream['url'])
 
 
 __plugin__ = TV3Cat
diff --git a/src/streamlink/stream/dash.py b/src/streamlink/stream/dash.py
index e202e53b..aa0a72fa 100644
--- a/src/streamlink/stream/dash.py
+++ b/src/streamlink/stream/dash.py
@@ -28,7 +28,6 @@ class DASHStreamWriter(SegmentedStreamWriter):
             return
 
         try:
-            headers = {}
             now = datetime.datetime.now(tz=utc)
             if segment.available_at > now:
                 time_to_wait = (segment.available_at - now).total_seconds()
@@ -36,18 +35,9 @@ class DASHStreamWriter(SegmentedStreamWriter):
                 log.debug("Waiting for segment: {fname} ({wait:.01f}s)".format(fname=fname, wait=time_to_wait))
                 sleep_until(segment.available_at)
 
-            if segment.range:
-                start, length = segment.range
-                if length:
-                    end = start + length - 1
-                else:
-                    end = ""
-                headers["Range"] = "bytes={0}-{1}".format(start, end)
-
             return self.session.http.get(segment.url,
                                          timeout=self.timeout,
-                                         exception=StreamError,
-                                         headers=headers)
+                                         exception=StreamError)
         except StreamError as err:
             log.error("Failed to open segment {0}: {1}", segment.url, err)
             return self.fetch(segment, retries - 1)
diff --git a/src/streamlink/stream/dash_manifest.py b/src/streamlink/stream/dash_manifest.py
index e7389088..5477347e 100644
--- a/src/streamlink/stream/dash_manifest.py
+++ b/src/streamlink/stream/dash_manifest.py
@@ -32,13 +32,12 @@ epoch_start = datetime.datetime(1970, 1, 1, tzinfo=utc)
 
 
 class Segment(object):
-    def __init__(self, url, duration, init=False, content=True, available_at=epoch_start, range=None):
+    def __init__(self, url, duration, init=False, content=True, available_at=epoch_start):
         self.url = url
         self.duration = duration
         self.init = init
         self.content = content
         self.available_at = available_at
-        self.range = range
 
 
 def datetime_to_seconds(dt):
@@ -120,15 +119,6 @@ class MPDParsers(object):
 
         return _timedelta
 
-    @staticmethod
-    def range(range_spec):
-        r = range_spec.split("-")
-        if len(r) != 2:
-            raise MPDParsingError("invalid byte-range-spec")
-
-        start, end = int(r[0]), r[1] and int(r[1]) or None
-        return start, end and ((end-start) + 1)
-
 
 class MPDParsingError(Exception):
     pass
@@ -332,53 +322,9 @@ class EventStream(MPDNode):
     __tag__ = "EventStream"
 
 
-class Initialization(MPDNode):
-    __tag__ = "Initialization"
-
-    def __init__(self, node, root=None, parent=None, *args, **kwargs):
-        super(Initialization, self).__init__(node, root, parent, *args, **kwargs)
-        self.source_url = self.attr("sourceURL")
-
-
-class SegmentURL(MPDNode):
-    __tag__ = "SegmentURL"
-
-    def __init__(self, node, root=None, parent=None, *args, **kwargs):
-        super(SegmentURL, self).__init__(node, root, parent, *args, **kwargs)
-        self.media = self.attr("media")
-        self.media_range = self.attr("mediaRange", parser=MPDParsers.range)
-
-
 class SegmentList(MPDNode):
     __tag__ = "SegmentList"
 
-    def __init__(self, node, root=None, parent=None, *args, **kwargs):
-        super(SegmentList, self).__init__(node, root, parent, *args, **kwargs)
-
-        self.presentation_time_offset = self.attr("presentationTimeOffset")
-        self.timescale = self.attr("timescale", parser=int)
-        self.duration = self.attr("duration", parser=int)
-        self.start_number = self.attr("startNumber", parser=int, default=1)
-
-        if self.duration:
-            self.duration_seconds = self.duration / float(self.timescale)
-        else:
-            self.duration_seconds = None
-
-
-        self.initialization = self.only_child(Initialization)
-        self.segment_urls = self.children(SegmentURL, minimum=1)
-
-    @property
-    def segments(self):
-        if self.initialization:
-            yield Segment(self.make_url(self.initialization.source_url), 0, init=True, content=False)
-        for n, segment_url in enumerate(self.segment_urls, self.start_number):
-            yield Segment(self.make_url(segment_url.media), self.duration_seconds, range=segment_url.media_range)
-
-    def make_url(self, url):
-        return BaseURL.join(self.base_url, url)
-
 
 class AdaptationSet(MPDNode):
     __tag__ = u"AdaptationSet"
@@ -577,7 +523,7 @@ class Representation(MPDNode):
         self.baseURLs = self.children(BaseURL)
         self.subRepresentation = self.children(SubRepresentation)
         self.segmentBase = self.only_child(SegmentBase)
-        self.segmentList = self.children(SegmentList)
+        self.segmentList = self.only_child(SegmentList)
         self.segmentTemplate = self.only_child(SegmentTemplate)
 
     def segments(self, **kwargs):
@@ -592,7 +538,7 @@ class Representation(MPDNode):
         """
 
         segmentBase = self.segmentBase or self.walk_back_get_attr("segmentBase")
-        segmentLists = self.segmentList or self.walk_back_get_attr("segmentList")
+        segmentList = self.segmentList or self.walk_back_get_attr("segmentList")
         segmentTemplate = self.segmentTemplate or self.walk_back_get_attr("segmentTemplate")
 
         if segmentTemplate:
@@ -603,10 +549,6 @@ class Representation(MPDNode):
                     yield segment
                 else:
                     yield segment
-        elif segmentLists:
-            for segmentList in segmentLists:
-                for segment in segmentList.segments:
-                    yield segment
         else:
             yield Segment(self.base_url, 0, True, True)
 
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 24da2fe1..dbe08d36 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -588,7 +588,7 @@ def authenticate_twitch_oauth():
 
     client_id = TWITCH_CLIENT_ID
     redirect_uri = "https://streamlink.github.io/twitch_oauth.html"
-    url = ("https://api.twitch.tv/kraken/oauth2/authorize"
+    url = ("https://api.twitch.tv/kraken/oauth2/authorize/"
            "?response_type=token&client_id={0}&redirect_uri="
            "{1}&scope=user_read+user_subscriptions").format(client_id, redirect_uri)
 


Test patch:
diff --git a/tests/resources/dash/test_7.mpd b/tests/resources/dash/test_7.mpd
deleted file mode 100644
index 17e9bec0..00000000
--- a/tests/resources/dash/test_7.mpd
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-     xmlns="urn:mpeg:dash:schema:mpd:2011"
-     xmlns:xlink="http://www.w3.org/1999/xlink"
-     xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
-     profiles="urn:mpeg:dash:profile:isoff-main:2011"
-     type="static"
-     publishTime="2018-06-25T18:01:58Z"
-     mediaPresentationDuration="PT53M18.059S"
-     minBufferTime="PT1.5S">
-<ProgramInformation>
-    <Title>test/dash.smil</Title>
-</ProgramInformation>
-<Period id="0" start="PT0.0S">
-    <AdaptationSet id="0" group="1" mimeType="video/mp4" maxWidth="1920" maxHeight="1080" par="16:9" frameRate="25" segmentAlignment="true" startWithSAP="1" subsegmentAlignment="true" subsegmentStartsWithSAP="1">
-        <Representation id="p0va0br4332748" codecs="avc1.640028" width="1920" height="1080" sar="1:1" bandwidth="4332748">
-            <SegmentList presentationTimeOffset="0" timescale="90000" duration="900000" startNumber="1">
-                <Initialization sourceURL="chunk_ctvideo_ridp0va0br4332748_cinit_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn1_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn2_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn3_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn4_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn5_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn6_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn7_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn8_mpd.m4s"/>
-                <SegmentURL media="chunk_ctvideo_ridp0va0br4332748_cn9_mpd.m4s"/>
-            </SegmentList>
-        </Representation>
-    </AdaptationSet>
-</Period>
-</MPD>
diff --git a/tests/streams/test_dash_parser.py b/tests/streams/test_dash_parser.py
index 2eba726c..09600859 100644
--- a/tests/streams/test_dash_parser.py
+++ b/tests/streams/test_dash_parser.py
@@ -61,11 +61,6 @@ class TestMPDParsers(unittest.TestCase):
         self.assertEqual(MPDParsers.timedelta(10)(100),
                          datetime.timedelta(0, 10.0))
 
-    def test_range(self):
-        self.assertEqual(MPDParsers.range("100-"), (100, None))
-        self.assertEqual(MPDParsers.range("100-199"), (100, 100))
-        self.assertRaises(MPDParsingError, MPDParsers.range, "100")
-
 
 class TestMPDParser(unittest.TestCase):
     maxDiff = None
@@ -154,21 +149,6 @@ class TestMPDParser(unittest.TestCase):
                                       'http://test.se/dash/150633-video_eng=194000-4000.dash',
                                       ])
 
-    def test_segments_list(self):
-        with xml("dash/test_7.mpd") as mpd_xml:
-            mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd")
-
-            segments = mpd.periods[0].adaptationSets[0].representations[0].segments()
-            init_segment = next(segments)
-            self.assertEqual(init_segment.url, "http://test.se/chunk_ctvideo_ridp0va0br4332748_cinit_mpd.m4s")
-
-            video_segments = [x.url for x in itertools.islice(segments, 3)]
-            self.assertSequenceEqual(video_segments,
-                                     ['http://test.se/chunk_ctvideo_ridp0va0br4332748_cn1_mpd.m4s',
-                                      'http://test.se/chunk_ctvideo_ridp0va0br4332748_cn2_mpd.m4s',
-                                      'http://test.se/chunk_ctvideo_ridp0va0br4332748_cn3_mpd.m4s',
-                                      ])
-
     def test_segments_dynamic_timeline_continue(self):
         with xml("dash/test_6_p1.mpd") as mpd_xml_p1:
             with xml("dash/test_6_p2.mpd") as mpd_xml_p2:
diff --git a/tests/test_plugin_mjunoon.py b/tests/test_plugin_mjunoon.py
new file mode 100644
index 00000000..1aa08b1b
--- /dev/null
+++ b/tests/test_plugin_mjunoon.py
@@ -0,0 +1,36 @@
+import unittest
+import requests_mock
+
+from streamlink import Streamlink
+from tests.mock import patch, Mock, ANY, call
+from streamlink.plugins.mjunoon import Mjunoon
+
+
+class TestPluginMixer(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Mjunoon.can_handle_url('https://mjunoon.tv/news-live'))
+        self.assertTrue(Mjunoon.can_handle_url('http://mjunoon.tv/watch/some-long-vod-name23456'))
+        self.assertTrue(Mjunoon.can_handle_url('https://www.mjunoon.tv/other-live'))
+        self.assertTrue(Mjunoon.can_handle_url('https://www.mjunoon.tv/watch/something-else-2321'))
+
+    def test_can_handle_url_negative(self):
+        # shouldn't match
+        self.assertFalse(Mjunoon.can_handle_url('https://mjunoon.com'))
+
+    @patch('streamlink.plugins.mjunoon.HLSStream.parse_variant_playlist')
+    def test_get_streams(self, parse_variant_playlist):
+        session = Streamlink()
+        Mjunoon.bind(session, "test")
+        script_text = """
+        <script id="playerScript"  src="playerAssets/js/player.js?v=2.2&streamUrl=https://vod.mjunoon.tv:8181/live/41/41.m3u8&streamUrl=https://vod.mjunoon.tv:8181/live/17/17.m3u8"></script>
+        """
+        parse_variant_playlist.items.return_value = [("test", Mock())]
+        with requests_mock.Mocker() as rmock:
+            rmock.get("https://mjunoon.tv/news-live", text=script_text)
+            plugin = Mjunoon("https://mjunoon.tv/news-live")
+            _ = list(plugin.streams())
+            self.assertSequenceEqual(
+                [call(ANY, "https://vod.mjunoon.tv:8181/live/41/41.m3u8", params=dict(id=1), verify=False),
+                 call(ANY, "https://vod.mjunoon.tv:8181/live/17/17.m3u8", params=dict(id=2), verify=False)],
+                [parse_variant_playlist.mock_calls[0], parse_variant_playlist.mock_calls[3]])