Repository: streamlink/streamlink

Issue:
RTS and TVC
- [ ] This is a bug report.
- [ ] This is a feature request.
- [x] This is a plugin (improvement) request.
- [ ] I have read the contribution guidelines.

I'm requesting a plugin for cloudfront.net

its streamed it serves a generated .m3u8 (for 30ish seconds) along with its 1ish second chunk every second or so

Sites that use this server to stream:
http://www.rts.com.ec/envivo
http://www.tvc.com.ec/envivo

XHR:
http://d36zvzlneynve1.cloudfront.net/liverts/smil:liverts.smil/playlist.m3u8
http://d36zvzlneynve1.cloudfront.net/liverts/smil:liverts.smil/media-u5wu5zzkr_b899152_3340.ts
http://d36zvzlneynve1.cloudfront.net/liverts/smil:liverts.smil/media-u5wu5zzkr_b899152_3341.ts
http://d36zvzlneynve1.cloudfront.net/liverts/smil:liverts.smil/media-u5wu5zzkr_b899152_3342.ts
http://d36zvzlneynve1.cloudfront.net/liverts/smil:liverts.smil/media-u5wu5zzkr_b899152_3343.ts

These links were captured with IDM:
http://d36zvzlneynve1.cloudfront.net/liverts/smil:liverts.smil/chunklist_b899152.m3u8 (360p)
http://d36zvzlneynve1.cloudfront.net/liverts/smil:liverts.smil/chunklist_b249152.m3u8 (160p)
http://d36zvzlneynve1.cloudfront.net/liverts/smil:liverts.smil/chunklist_b1073152.m3u8 (480p)

A look at the .m3u8:
```
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=899152,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=640x360
chunklist_b899152.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=249152,CODECS="avc1.66.21,mp4a.40.2",RESOLUTION=284x160
chunklist_b249152.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1073152,CODECS="avc1.77.32,mp4a.40.2",RESOLUTION=854x480
chunklist_b1073152.m3u8
```
When I tried to download the stream, usually Streamlink says "Error 403", but other times Streamlink says it does not have the plugin.


Discussion:
You want plugins for http://www.rts.com.ec/envivo and http://www.tvc.com.ec/envivo? Rather than cloudfront, cloudfront is just the CDN they use. 
I would like it to work for both sites. But what would happen if would there be more sites that also use cloudfront for their streams? For example these two web pages

http://www.elnueve.com.ar/envivo
http://www.repretel.com/envivo

There are other sites that are part of the same international network (Albavisión) and they use the same server, but have geoblocking.
So, you want a plugin for all the Albavisión sites? :) 
I'm interested in RTS and TVC :)

I just set the example other similar streams that are from other countries in case someone could be useful in the future.
I guess nobody has worked on the plugin yet
Streamlink is an open source software project. You and anyone else can work on this. This is a community effort. Simply requesting a plugin (and asking for it again) doesn't mean that it'll be added.

Resolving patch:
diff --git a/src/streamlink/plugins/albavision.py b/src/streamlink/plugins/albavision.py
new file mode 100644
--- /dev/null
+++ b/src/streamlink/plugins/albavision.py
@@ -0,0 +1,117 @@
+"""
+Support for the live streams on Albavision sites
+    - http://www.tvc.com.ec
+    - http://www.rts.com.ec
+    - https://www.elnueve.com.ar
+"""
+import logging
+import re
+import time
+
+from streamlink import PluginError
+from streamlink.compat import quote, urlencode
+from streamlink.plugin import Plugin
+from streamlink.plugin.api.utils import itertags
+from streamlink.stream import HLSStream
+from streamlink.utils import update_scheme
+
+log = logging.getLogger(__name__)
+
+
+class Albavision(Plugin):
+    _url_re = re.compile(r"https?://(?:www\.)?(tvc.com.ec|rts.com.ec|elnueve.com.ar)/en-?vivo")
+    _token_input_re = re.compile(r"Math.floor\(Date.now\(\) / 3600000\),'([a-f0-9OK]+)'")
+    _live_url_re = re.compile(r"LIVE_URL = '(.*?)';")
+    _playlist_re = re.compile(r"file:\s*'(http.*m3u8)'")
+    _token_url_re = re.compile(r"https://.*/token/.*?\?rsk=")
+
+    _channel_urls = {
+        'Quito': 'http://d3aacg6baj4jn0.cloudfront.net/reproductor_rts_o_quito.html?iut=',
+        'Guayaquil': 'http://d2a6tcnofawcbm.cloudfront.net/player_rts.html?iut=',
+        'Canal5': 'http://dxejh4fchgs18.cloudfront.net/player_televicentro.html?iut='
+    }
+
+    def __init__(self, url):
+        super(Albavision, self).__init__(url)
+        self._page = None
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    @property
+    def page(self):
+        if not self._page:
+            self._page = self.session.http.get(self.url)
+        return self._page
+
+    def _get_token_url(self):
+        token = self._get_live_url_token()
+        if token:
+            m = self._token_url_re.search(self.page.text)
+            token_url = m and m.group(0)
+            if token_url:
+                log.debug("token_url={0}{1}".format(token_url, token))
+                return token_url + token
+        else:
+            log.error("Could not find site token")
+
+    @staticmethod
+    def transform_token(token_in, date):
+        token_out = list(token_in)
+        offset = len(token_in)
+        for i in range(offset - 1, -1, -1):
+            p = (i * date) % offset
+            # swap chars at p and i
+            token_out[i], token_out[p] = token_out[p], token_out[i]
+        token_out = ''.join(token_out)
+        if token_out.endswith("OK"):
+            return token_out[:-2]  # return token without OK suffix
+        else:
+            log.error("Invalid site token: {0} => {1}".format(token_in, token_out))
+
+    def _get_live_url_token(self):
+        m = self._token_input_re.search(self.page.text)
+        if m:
+            date = int(time.time()//3600)
+            return self.transform_token(m.group(1), date) or self.transform_token(m.group(1), date - 1)
+
+    def _get_token(self):
+        token_url = self._get_token_url()
+        if token_url:
+            res = self.session.http.get(token_url)
+            data = self.session.http.json(res)
+            if data['success']:
+                return data['token']
+
+    def _get_streams(self):
+        m = self._live_url_re.search(self.page.text)
+        playlist_url = m and update_scheme(self.url, m.group(1))
+        player_url = self.url
+        token = self._get_token()
+
+        if playlist_url:
+            log.debug("Found playlist URL in the page")
+        else:
+            live_channel = None
+            for div in itertags(self.page.text, "div"):
+                if div.attributes.get("id") == "botonLive":
+                    live_channel = div.attributes.get("data-canal")
+
+            if live_channel:
+                log.debug("Live channel: {0}".format(live_channel))
+                player_url = self._channel_urls[live_channel]+quote(token)
+                page = self.session.http.get(player_url, raise_for_status=False)
+                if "block access from your country." in page.text:
+                    raise PluginError("Content is geo-locked")
+                m = self._playlist_re.search(page.text)
+                playlist_url = m and update_scheme(self.url, m.group(1))
+            else:
+                log.error("Could not find the live channel")
+
+        if playlist_url:
+            stream_url = "{0}?{1}".format(playlist_url, urlencode({"iut": token}))
+            return HLSStream.parse_variant_playlist(self.session, stream_url, headers={"referer": player_url})
+
+
+__plugin__ = Albavision


Test patch:
diff --git a/tests/plugins/test_albavision.py b/tests/plugins/test_albavision.py
new file mode 100644
--- /dev/null
+++ b/tests/plugins/test_albavision.py
@@ -0,0 +1,29 @@
+import unittest
+
+import pytest
+
+from streamlink.plugins.albavision import Albavision
+
+
+class TestPluginAlbavision:
+    valid_urls = [
+        ("https://www.elnueve.com.ar/en-vivo",),
+        ("http://www.rts.com.ec/envivo",),
+        ("https://www.tvc.com.ec/envivo",),
+    ]
+    invalid_urls = [
+        ("https://news.now.com/home/local",),
+        ("http://media.now.com.hk/",),
+        ("https://www.youtube.com",)
+    ]
+
+    @pytest.mark.parametrize(["url"], valid_urls)
+    def test_can_handle_url(self, url):
+        assert Albavision.can_handle_url(url), "url should be handled"
+
+    @pytest.mark.parametrize(["url"], invalid_urls)
+    def test_can_handle_url_negative(self, url):
+        assert not Albavision.can_handle_url(url), "url should not be handled"
+
+    def test_transform(self):
+        assert Albavision.transform_token(u'6b425761cc8a86569b1a05a9bf1870c95fca717dOK', 436171) == "6b425761cc8a86569b1a05a9bf1870c95fca717d"