Repository: streamlink/streamlink

Issue:
RTVE.es plugin broken
Spanish public TV RTVE recently changed broadcasting address and servers which made plugin broken. 

`livestreamer -p "C:\Users\Ddr\Downloads\VLCPortable\App\vlc\vlc" "http://www.rtve.es/noticias/mas-24/" best
error: No plugin can handle URL: http://www.rtve.es/noticias/mas-24/`

Below are direct addresses to channels : 

- La 1 : http://www.rtve.es/noticias/directo-la-1/
- La 2 : http://www.rtve.es/television/la-2-directo/
- TDP : http://www.rtve.es/deportes/directo/teledeporte/
- 24H : http://www.rtve.es/noticias/mas-24/

24H is not geoblocked.

Comments:

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index c771574b..0a900a08 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -37,6 +37,9 @@ bnt                 - tv.bnt.bg          Yes   No    Streams may be geo-restrict
                     - nova.bg
 bongacams           bongacams.com        Yes   No    Only RTMP streams are available.
 btv                 btv.bg               Yes   No    Requires login, and geo-restricted to Bulgaria.
+canaplus            - canalplus.fr       Yes   Yes   Streams may be geo-restricted to France.
+                    - c8.fr
+                    - cstar.fr
 chaturbate          chaturbate.com       Yes   No
 cinergroup          - showtv.com.tr      Yes   No
                     - haberturk.com
diff --git a/src/streamlink/plugins/canalplus.py b/src/streamlink/plugins/canalplus.py
new file mode 100644
index 00000000..ee06516d
--- /dev/null
+++ b/src/streamlink/plugins/canalplus.py
@@ -0,0 +1,103 @@
+import re
+
+from streamlink.exceptions import PluginError
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http, useragents, validate
+from streamlink.stream import HDSStream, HLSStream, HTTPStream
+
+
+class CanalPlus(Plugin):
+    API_URL = 'http://service.canal-plus.com/video/rest/getVideos/{0}/{1}?format=json'
+    CHANNEL_MAP = {'canalplus': 'cplus', 'c8': 'd8', 'cstar': 'd17'}
+    HDCORE_VERSION = '3.1.0'
+    # Secret parameter needed to download HTTP videos on canalplus.fr
+    SECRET = 'pqzerjlsmdkjfoiuerhsdlfknaes'
+
+    _url_re = re.compile(r'''
+        http://
+        (
+            www\.(?P<channel>canalplus|c8|cstar)\.fr/.*pid.+?\.html(\?(?P<video_id>[0-9]+))? |
+            replay\.(?P<replay_channel>c8|cstar)\.fr/video/(?P<replay_video_id>[0-9]+)
+        )
+''', re.VERBOSE)
+    _video_id_re = re.compile(r'\bdata-video="(?P<video_id>[0-9]+)"')
+    _mp4_bitrate_re = re.compile(r'.*_(?P<bitrate>[0-9]+k)\.mp4')
+    _api_schema = validate.Schema({
+        'MEDIA': validate.Schema({
+            'VIDEOS': validate.Schema({
+                validate.text: validate.any(
+                    validate.url(),
+                    ''
+                )
+            })
+        })
+    })
+    _user_agent = useragents.CHROME
+
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return CanalPlus._url_re.match(url)
+
+
+    def _get_streams(self):
+        # Get video ID and channel from URL
+        match = self._url_re.match(self.url)
+        channel = match.group('channel')
+        if channel is None:
+            # Replay website
+            channel = match.group('replay_channel')
+            video_id = match.group('replay_video_id')
+        else:
+            video_id = match.group('video_id')
+            if video_id is None:
+                # Retrieve URL page and search for video ID
+                res = http.get(self.url)
+                match = self._video_id_re.search(res.text)
+                if match is None:
+                    return
+                video_id = match.group('video_id')
+
+        res = http.get(self.API_URL.format(self.CHANNEL_MAP[channel], video_id))
+        videos = http.json(res, schema=self._api_schema)
+        parsed = []
+        headers = {'User-Agent': self._user_agent}
+        for quality, video_url in list(videos['MEDIA']['VIDEOS'].items()):
+            # Ignore empty URLs
+            if video_url == '':
+                continue
+
+            # Ignore duplicate video URLs
+            if video_url in parsed:
+                continue
+            parsed.append(video_url)
+
+            try:
+                if '.f4m' in video_url:
+                    for stream in HDSStream.parse_manifest(self.session,
+                                                           video_url,
+                                                           params={'hdcore': self.HDCORE_VERSION},
+                                                           headers=headers).items():
+                        yield stream
+                elif '.m3u8' in video_url:
+                    for stream in HLSStream.parse_variant_playlist(self.session,
+                                                                   video_url,
+                                                                   headers=headers).items():
+                        yield stream
+                elif '.mp4' in video_url:
+                    # Get bitrate from video filename
+                    match = self._mp4_bitrate_re.match(video_url)
+                    if match is not None:
+                        bitrate = match.group('bitrate')
+                    else:
+                        bitrate = quality
+                    yield bitrate, HTTPStream(self.session,
+                                              video_url,
+                                              params={'secret': self.SECRET},
+                                              headers=headers)
+            except PluginError:
+                self.logger.error('Failed to access stream, may be due to geo-restriction')
+                raise
+
+
+__plugin__ = CanalPlus
diff --git a/src/streamlink/plugins/rtve.py b/src/streamlink/plugins/rtve.py
index e0c31b05..0a3a6151 100644
--- a/src/streamlink/plugins/rtve.py
+++ b/src/streamlink/plugins/rtve.py
@@ -1,59 +1,86 @@
+import base64
 import re
 
-from streamlink.plugin import Plugin, PluginError
+from Crypto.Cipher import Blowfish
+from streamlink.compat import bytes, is_py3
+from streamlink.plugin import Plugin
 from streamlink.plugin.api import http
+from streamlink.plugin.api import useragents
+from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
+from streamlink.utils import parse_xml
 
-# The last four channel_paths repsond with 301 and provide
-# a redirect location that corresponds to a channel_path above.
-_url_re = re.compile(r"""
-    https?://www\.rtve\.es/
-    (?P<channel_path>
-        directo/la-1|
-        directo/la-2|
-        directo/teledeporte|
-        directo/canal-24h|
-
-        noticias/directo-la-1|
-        television/la-2-directo|
-        deportes/directo/teledeporte|
-        noticias/directo/canal-24h
-    )
-    /?
-""", re.VERBOSE)
 
-_id_map = {
-    "directo/la-1": "LA1",
-    "directo/la-2": "LA2",
-    "directo/teledeporte": "TDP",
-    "directo/canal-24h": "24H",
-    "noticias/directo-la-1": "LA1",
-    "television/la-2-directo": "LA2",
-    "deportes/directo/teledeporte": "TDP",
-    "noticias/directo/canal-24h": "24H",
-}
+class ZTNRClient(object):
+    base_url = "http://ztnr.rtve.es/ztnr/res/"
+    block_size = 16
+
+    def __init__(self, key):
+        self.cipher = Blowfish.new(key, Blowfish.MODE_ECB)
+
+    def pad(self, data):
+        n = self.block_size - len(data) % self.block_size
+        return data + bytes(chr(self.block_size - len(data) % self.block_size), "utf8") * n
+
+    def unpad(self, data):
+        if is_py3:
+            return data[0:-data[-1]]
+        else:
+            return data[0:-ord(data[-1])]
+
+    def encrypt(self, data):
+        return base64.b64encode(self.cipher.encrypt(self.pad(bytes(data, "utf-8"))), altchars=b"-_").decode("ascii")
+
+    def decrypt(self, data):
+        return self.unpad(self.cipher.decrypt(base64.b64decode(data, altchars=b"-_")))
+
+    def request(self, data, *args, **kwargs):
+        res = http.get(self.base_url+self.encrypt(data), *args, **kwargs)
+        return self.decrypt(res.content)
+
+    def get_cdn_list(self, vid, manager="apedemak", vtype="video", lang="es", schema=None):
+        data = self.request("{id}_{manager}_{type}_{lang}".format(id=vid, manager=manager, type=vtype, lang=lang))
+        if schema:
+            return schema.validate(data)
+        else:
+            return data
 
 
 class Rtve(Plugin):
+    secret_key = base64.b64decode("eWVMJmRhRDM=")
+    channel_id_re = re.compile(r'<span.*?id="iniIDA">(\d+)</span>')
+    url_re = re.compile(r"""
+        https?://(?:www\.)?rtve\.es/(?:noticias|television|deportes)/.*?/?
+    """, re.VERBOSE)
+    cdn_schema = validate.Schema(
+        validate.transform(parse_xml),
+        validate.xml_findtext(".//url")
+    )
+
     @classmethod
     def can_handle_url(cls, url):
-        return _url_re.match(url)
+        return cls.url_re.match(url) is not None
 
     def __init__(self, url):
         Plugin.__init__(self, url)
-        match = _url_re.match(url).groupdict()
-        self.channel_path = match["channel_path"]
+        self.zclient = ZTNRClient(self.secret_key)
+        http.headers = {"User-Agent": useragents.SAFARI_8}
+
+    def _get_channel_id(self):
+        res = http.get(self.url)
+        m = self.channel_id_re.search(res.text)
+        return m and int(m.group(1))
 
     def _get_streams(self):
-        stream_id = _id_map[self.channel_path]
-        hls_url = "http://iphonelive.rtve.es/{0}_LV3_IPH/{0}_LV3_IPH.m3u8".format(stream_id)
+        channel_id = self._get_channel_id()
 
-        # Check if the stream is available
-        res = http.head(hls_url, raise_for_status=False)
-        if res.status_code == 404:
-            raise PluginError("The program is not available due to rights restrictions")
+        if channel_id:
+            self.logger.debug("Found channel with id: {0}", channel_id)
+            hls_url = self.zclient.get_cdn_list(channel_id, schema=self.cdn_schema)
+            self.logger.debug("Got stream URL: {0}", hls_url)
+            return HLSStream.parse_variant_playlist(self.session, hls_url)
 
-        return HLSStream.parse_variant_playlist(self.session, hls_url)
+        return
 
 
 __plugin__ = Rtve


Test patch:
diff --git a/tests/test_plugin_canalplus.py b/tests/test_plugin_canalplus.py
new file mode 100644
index 00000000..7b841e26
--- /dev/null
+++ b/tests/test_plugin_canalplus.py
@@ -0,0 +1,28 @@
+import unittest
+
+from streamlink.plugins.canalplus import CanalPlus
+
+
+class TestPluginCanalPlus(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(CanalPlus.can_handle_url("http://www.canalplus.fr/pid3580-live-tv-clair.html"))
+        self.assertTrue(CanalPlus.can_handle_url("http://www.canalplus.fr/emissions/pid8596-the-tonight-show.html"))
+        self.assertTrue(CanalPlus.can_handle_url("http://www.canalplus.fr/c-divertissement/pid1787-c-groland.html?vid=1430239"))
+        self.assertTrue(CanalPlus.can_handle_url("http://www.c8.fr/pid5323-c8-live.html"))
+        self.assertTrue(CanalPlus.can_handle_url("http://www.c8.fr/c8-divertissement/pid8758-c8-la-folle-histoire-de-sophie-marceau.html"))
+        self.assertTrue(CanalPlus.can_handle_url("http://www.c8.fr/c8-sport/pid5224-c8-direct-auto.html?vid=1430292"))
+        self.assertTrue(CanalPlus.can_handle_url("http://replay.c8.fr/video/1431076"))
+        self.assertTrue(CanalPlus.can_handle_url("http://www.cstar.fr/pid5322-cstar-live.html"))
+        self.assertTrue(CanalPlus.can_handle_url("http://www.cstar.fr/emissions/pid8754-wild-transport.html"))
+        self.assertTrue(CanalPlus.can_handle_url("http://www.cstar.fr/musique/pid6282-les-tops.html?vid=1430143"))
+        self.assertTrue(CanalPlus.can_handle_url("http://replay.cstar.fr/video/1430245"))
+
+        # shouldn't match
+        self.assertFalse(CanalPlus.can_handle_url("http://www.canalplus.fr/"))
+        self.assertFalse(CanalPlus.can_handle_url("http://www.c8.fr/"))
+        self.assertFalse(CanalPlus.can_handle_url("http://replay.c8.fr/"))
+        self.assertFalse(CanalPlus.can_handle_url("http://www.cstar.fr/"))
+        self.assertFalse(CanalPlus.can_handle_url("http://replay.cstar.fr/"))
+        self.assertFalse(CanalPlus.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(CanalPlus.can_handle_url("http://www.youtube.com/"))