Repository: streamlink/streamlink (python)

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.

Pull request description:
plugins.albavision: support for some albavision live streams
Supports
    - http://www.tvc.com.ec
    - http://www.rts.com.ec
    - https://www.elnueve.com.ar
with some paritially hardcoded streams. This list can be expanded by
adding more hardcoded stream urls, or by parsing the stream urls from
the javascript (a future enhancement).

Fixes #1825.

Patch:
diff --git a/README.md b/README.md
index f595ac45..5d6a67d8 100644
--- a/README.md
+++ b/README.md
@@ -16,12 +16,27 @@ services when they are available for the content you consume and enjoy.
 
 # [Installation][streamlink-installation]
 
-Please refer to our documentation for different ways to install Streamlink:
+#### Installation via Python pip
+
+```bash
+sudo pip install streamlink
+```
+
+#### Manual installation via Python
+
+```bash
+git clone https://github.com/streamlink/streamlink
+cd streamlink
+sudo python setup.py install
+```
+
+#### Windows, MacOS, Linux and BSD specific installation methods
 
 - [Windows][streamlink-installation-windows]
-- [macOS][streamlink-installation-macos]
-- [Linux and BSD][streamlink-installation-linux-and-bsd]
-- [Source code][streamlink-installation-source]
+- [Windows portable version][streamlink-installation-windows-portable]
+- [MacOS][streamlink-installation-others]
+- [Linux and BSD][streamlink-installation-linux]
+
 
 # Features
 
@@ -86,10 +101,10 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
   [streamlink-plugins]: https://streamlink.github.io/plugin_matrix.html
   [streamlink-documentation]: https://streamlink.github.io/cli.html
   [streamlink-installation]: https://streamlink.github.io/install.html
-  [streamlink-installation-windows]: https://streamlink.github.io/install.html#windows
-  [streamlink-installation-macos]: https://streamlink.github.io/install.html#macos
-  [streamlink-installation-linux-and-bsd]: https://streamlink.github.io/install.html#linux-and-bsd
-  [streamlink-installation-source]: https://streamlink.github.io/install.html#source-code
+  [streamlink-installation-windows]: https://streamlink.github.io/install.html#windows-binaries
+  [streamlink-installation-windows-portable]: https://streamlink.github.io/install.html#windows-portable-version
+  [streamlink-installation-linux]: https://streamlink.github.io/install.html#linux-and-bsd-packages
+  [streamlink-installation-others]: https://streamlink.github.io/install.html#other-platforms
   [livestreamer]: https://github.com/chrippa/livestreamer
   [contributing]: https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md
   [changelog]: https://github.com/streamlink/streamlink/blob/master/CHANGELOG.rst
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 7ffd8bd5..d354782d 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -16,6 +16,9 @@ abematv                 abema.tv             Yes   Yes   Streams are geo-restric
 abweb                   abweb.com            Yes   No    Requires a login and a subscription.
 adultswim               adultswim.com        Yes   Yes   Streams may be geo-restricted, some VOD streams are protected by DRM.
 afreeca                 play.afreecatv.com   Yes   No
+albavision              - tvc.com.ec         Yes   No    Some streams are geo-restricted.
+                        - rts.com.ec
+                        - elnueve.com.ar
 aljazeeraen             aljazeera.com        Yes   Yes   English version of the site.
 animelab                animelab.com         --    Yes   Requires a login. Streams may be geo-restricted to Australia and New Zealand.
 app17                   17app.co             Yes   --
diff --git a/src/streamlink/plugins/albavision.py b/src/streamlink/plugins/albavision.py
new file mode 100644
index 00000000..6d8d4625
--- /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
index 00000000..0fd72001
--- /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"