Repository: streamlink/streamlink

Issue:
RUV Iceland : plugin partially outdated
### Checklist

- [x] This is a bug report.
- [ ] This is a plugin request.
- [ ] This is a feature request.
- [ ] I used the search function to find already opened/closed issues or pull requests.

### Description

RUV is Icelandic broadcasting corporation consisting of 3 radio and TV channels : 

1. Radio channels
------------------

- RAS 1 : `http://ruv.is/nolayout/popup/ras1`
- RAS 2 : `http://ruv.is/nolayout/popup/ras2`
- Rondo : `http://ruv.is/nolayout/popup/rondo`

2. TV
-----

- RUV : `http://ruv.is/ruv`
- RUV 2 : `http://ruv.is/ruv-2`
- Krakkaruv : `http://krakkaruv.is/hlusta/spila`

### Expected / Actual behavior

Channels use HLS for broacasting. In the past they used rtmp too, not sure if it's still the case. 

### Reproduction steps / Stream URLs to test

Radio
------

`streamlink -l debug "http://ruv.is/nolayout/popup/ras1"
error: No plugin can handle URL: http://ruv.is/nolayout/popup/ras1`

TV
---

```
streamlink -l debug --http-proxy "82.221.48.137:8080" "http://ruv.is/ruv" best
[cli][info] Found matching plugin ruv for URL http://ruv.is/ruv
[cli][info] Available streams: 720p_hls, 480p_hls, 240p_hls, 360p_hls, 240p (wor
st), 360p, 480p, 720p (best)
[cli][info] Opening stream: 720p (rtmp)
[stream.rtmp][debug] Spawning command: C:\Users\Ddr\Downloads\streamlink\\rtmpdu
mp\rtmpdump.exe --flv - --live --pageUrl http://ruv.is/ruv --rtmp rtmp://ruvruvl
ivefs.fplive.net/ruvruvlive-live/stream1
[cli][error] Could not open stream: Error while executing subprocess
```

### Environment details (operating system, python version, etc.)

W7 PRO/streamlink portable

### Comments, logs, screenshots, etc.

TV channels are geolocked unlike radio. 


Comments:

Patch:
diff --git a/src/streamlink/plugins/ruv.py b/src/streamlink/plugins/ruv.py
index 09db4860..5873d330 100644
--- a/src/streamlink/plugins/ruv.py
+++ b/src/streamlink/plugins/ruv.py
@@ -3,40 +3,34 @@
 import re
 
 from streamlink.plugin import Plugin
-from streamlink.stream import RTMPStream, HLSStream
+from streamlink.stream import HLSStream
 
 from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
 
-RTMP_LIVE_URL = "rtmp://ruv{0}livefs.fplive.net/ruv{0}live-live/stream{1}"
-RTMP_SARPURINN_URL = "rtmp://sipvodfs.fplive.net/sipvod/{0}/{1}{2}.{3}"
-
-HLS_RUV_LIVE_URL = "http://ruvruv-live.hls.adaptive.level3.net/ruv/ruv/index/stream{0}.m3u8"
-HLS_RADIO_LIVE_URL = "http://sip-live.hds.adaptive.level3.net/hls-live/ruv-{0}/_definst_/live/stream1.m3u8"
-HLS_SARPURINN_URL = "http://sip-ruv-vod.dcp.adaptive.level3.net/{0}/{1}{2}.{3}.m3u8"
+# URL to the RUV LIVE API
+RUV_LIVE_API = """http://www.ruv.is/sites/all/themes/at_ruv/scripts/\
+ruv-stream.php?channel={0}&format=json"""
 
 
 _live_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/
-                                (?P<channel_path>
-                                    ruv|
-                                    ras1|
-                                    ras-1|
-                                    ras2|
-                                    ras-2|
-                                    rondo
+                                (?P<stream_id>
+                                    ruv/?$|
+                                    ruv2/?$|
+                                    ruv-2/?$|
+                                    ras1/?$|
+                                    ras2/?$|
+                                    rondo/?$
                                 )
                                 /?
                                 """, re.VERBOSE)
 
-_sarpurinn_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/sarpurinn/
-                                    (?:
+_sarpurinn_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/spila/
+                                    (?P<stream_id>
                                         ruv|
                                         ruv2|
                                         ruv-2|
                                         ruv-aukaras|
-                                        ras1|
-                                        ras-1|
-                                        ras2|
-                                        ras-2
                                     )
                                     /
                                     [a-zA-Z0-9_-]+
@@ -45,37 +39,26 @@ _sarpurinn_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/sarpurinn/
                                     /?
                                     """, re.VERBOSE)
 
-_rtmp_url_re = re.compile(r"""rtmp://sipvodfs\.fplive.net/sipvod/
-                                (?P<status>
-                                    lokad|
-                                    opid
-                                )
-                                /
-                                (?P<date>[0-9]+/[0-9][0-9]/[0-9][0-9]/)?
-                                (?P<id>[A-Z0-9\$_]+)
-                                \.
-                                (?P<ext>
-                                    mp4|
-                                    mp3
-                                )""", re.VERBOSE)
-
-_id_map = {
-    "ruv": "ruv",
-    "ras1": "ras1",
-    "ras-1": "ras1",
-    "ras2": "ras2",
-    "ras-2": "ras2",
-    "rondo": "ras3"
-}
+_single_re = re.compile(r"""(?P<url>http://[0-9a-zA-Z\-\.]*/
+                            (lokad|opid)
+                            /
+                            ([0-9]+/[0-9][0-9]/[0-9][0-9]/)?
+                            ([A-Z0-9\$_]+\.mp4\.m3u8)
+                            )
+                         """, re.VERBOSE)
+
+_multi_re = re.compile(r"""(?P<base_url>http://[0-9a-zA-Z\-\.]*/
+                            (lokad|opid)
+                            /)
+                            manifest.m3u8\?tlm=hls&streams=
+                            (?P<streams>[0-9a-zA-Z\/\.\,:]+)
+                         """, re.VERBOSE)
 
 
 class Ruv(Plugin):
     @classmethod
     def can_handle_url(cls, url):
-        if _live_url_re.match(url):
-            return _live_url_re.match(url)
-        else:
-            return _sarpurinn_url_re.match(url)
+        return _live_url_re.match(url) or _sarpurinn_url_re.match(url)
 
     def __init__(self, url):
         Plugin.__init__(self, url)
@@ -83,75 +66,77 @@ class Ruv(Plugin):
 
         if live_match:
             self.live = True
-            self.channel_path = live_match.group("channel_path")
+            self.stream_id = live_match.group("stream_id")
+
+            # Remove slashes
+            self.stream_id.replace("/", "")
+
+            # Remove dashes
+            self.stream_id.replace("-", "")
+
+            # Rondo is identified as ras3
+            if self.stream_id == "rondo":
+                self.stream_id = "ras3"
         else:
             self.live = False
 
     def _get_live_streams(self):
-        stream_id = _id_map[self.channel_path]
+        # Get JSON API
+        res = http.get(RUV_LIVE_API.format(self.stream_id))
 
-        if stream_id == "ruv":
-            qualities_rtmp = ["720p", "480p", "360p", "240p"]
-
-            for i, quality in enumerate(qualities_rtmp):
-                yield quality, RTMPStream(
-                    self.session,
-                    {
-                        "rtmp": RTMP_LIVE_URL.format(stream_id, i + 1),
-                        "pageUrl": self.url,
-                        "live": True
-                    }
-                )
+        # Parse the JSON API
+        json_res = http.json(res)
 
-            qualities_hls = ["240p", "360p", "480p", "720p"]
-            for i, quality_hls in enumerate(qualities_hls):
-                yield quality_hls, HLSStream(
-                    self.session,
-                    HLS_RUV_LIVE_URL.format(i + 1)
-                )
+        for url in json_res["result"]:
+            if url.startswith("rtmp:"):
+                continue
 
-        else:
-            yield "audio", RTMPStream(self.session, {
-                "rtmp": RTMP_LIVE_URL.format(stream_id, 1),
-                "pageUrl": self.url,
-                "live": True
-            })
+            # Get available streams
+            streams = HLSStream.parse_variant_playlist(self.session, url)
 
-            yield "audio", HLSStream(
-                self.session,
-                HLS_RADIO_LIVE_URL.format(stream_id)
-            )
+            for quality, hls in streams.items():
+                yield quality, hls
 
     def _get_sarpurinn_streams(self):
-        res = http.get(self.url)
-        match = _rtmp_url_re.search(res.text)
-
-        if not match:
-            yield
-
-        token = match.group("id")
-        status = match.group("status")
-        extension = match.group("ext")
-        date = match.group("date")
-        if not date:
-            date = ""
+        # Get HTML page
+        res = http.get(self.url).text
+        lines = "\n".join([l for l in res.split("\n") if "video.src" in l])
+        multi_stream_match = _multi_re.search(lines)
+
+        if multi_stream_match and multi_stream_match.group("streams"):
+            base_url = multi_stream_match.group("base_url")
+            streams = multi_stream_match.group("streams").split(",")
+
+            for stream in streams:
+                if stream.count(":") != 1:
+                    continue
+
+                [token, quality] = stream.split(":")
+                quality = int(quality)
+                key = ""
+
+                if quality <= 500:
+                    key = "240p"
+                elif quality <= 800:
+                    key = "360p"
+                elif quality <= 1200:
+                    key = "480p"
+                elif quality <= 2400:
+                    key = "720p"
+                else:
+                    key = "1080p"
+
+                yield key, HLSStream(
+                    self.session,
+                    base_url + token
+                )
 
-        if extension == "mp3":
-            key = "audio"
         else:
-            key = "576p"
-
-            # HLS on Sarpurinn is currently only available on videos
-            yield key, HLSStream(
-                self.session,
-                HLS_SARPURINN_URL.format(status, date, token, extension)
-            )
-
-        yield key, RTMPStream(self.session, {
-            "rtmp": RTMP_SARPURINN_URL.format(status, date, token, extension),
-            "pageUrl": self.url,
-            "live": True
-        })
+            single_stream_match = _single_re.search(lines)
+
+            if single_stream_match:
+                url = single_stream_match.group("url")
+                yield "576p", HLSStream(self.session, url)
 
     def _get_streams(self):
         if self.live:


Test patch:
diff --git a/tests/test_plugin_ruv.py b/tests/test_plugin_ruv.py
new file mode 100644
index 00000000..fc7d5194
--- /dev/null
+++ b/tests/test_plugin_ruv.py
@@ -0,0 +1,28 @@
+import unittest
+
+from streamlink.plugins.ruv import Ruv
+
+
+class TestPluginRuv(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Ruv.can_handle_url("ruv.is/ruv"))
+        self.assertTrue(Ruv.can_handle_url("http://ruv.is/ruv"))
+        self.assertTrue(Ruv.can_handle_url("http://ruv.is/ruv/"))
+        self.assertTrue(Ruv.can_handle_url("https://ruv.is/ruv/"))
+        self.assertTrue(Ruv.can_handle_url("http://www.ruv.is/ruv"))
+        self.assertTrue(Ruv.can_handle_url("http://www.ruv.is/ruv/"))
+        self.assertTrue(Ruv.can_handle_url("ruv.is/ruv2"))
+        self.assertTrue(Ruv.can_handle_url("ruv.is/ras1"))
+        self.assertTrue(Ruv.can_handle_url("ruv.is/ras2"))
+        self.assertTrue(Ruv.can_handle_url("ruv.is/rondo"))
+        self.assertTrue(Ruv.can_handle_url("http://www.ruv.is/spila/ruv/ol-2018-ishokki-karla/20180217"))
+        self.assertTrue(Ruv.can_handle_url("http://www.ruv.is/spila/ruv/frettir/20180217"))
+
+        # shouldn't match
+        self.assertFalse(Ruv.can_handle_url("rruv.is/ruv"))
+        self.assertFalse(Ruv.can_handle_url("ruv.is/ruvnew"))
+        self.assertFalse(Ruv.can_handle_url("https://www.bloomberg.com/live/"))
+        self.assertFalse(Ruv.can_handle_url("https://www.bloomberg.com/politics/articles/2017-04-17/french-race-up-for-grabs-days-before-voters-cast-first-ballots"))
+        self.assertFalse(Ruv.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(Ruv.can_handle_url("http://www.youtube.com/"))