1import re
2
3from io import BytesIO
4from time import sleep
5
6from livestreamer.exceptions import PluginError
7from livestreamer.packages.flashmedia import AMFPacket, AMFMessage
8from livestreamer.packages.flashmedia.types import AMF3ObjectBase
9from livestreamer.plugin import Plugin
10from livestreamer.plugin.api import http, validate
11from livestreamer.stream import AkamaiHDStream
12
13AMF_GATEWAY = "http://c.brightcove.com/services/messagebroker/amf"
14AMF_MESSAGE_PREFIX = "af6b88c640c8d7b4cc75d22f7082ad95603bc627"
15STREAM_NAMES = ["360p", "480p", "720p", "source"]
16HTTP_HEADERS = {
17    "User-Agent": ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
18                   "(KHTML, like Gecko) Chrome/36.0.1944.9 Safari/537.36")
19}
20
21_url_re = re.compile("http(s)?://(\w+\.)?azubu.tv/[^/]+")
22
23_viewerexp_schema = validate.Schema(
24    validate.attr({
25        "programmedContent": {
26            "videoPlayer": validate.attr({
27                "mediaDTO": validate.attr({
28                    "renditions": {
29                        int: validate.attr({
30                            "encodingRate": int,
31                            "defaultURL": validate.text
32                        })
33                    }
34                })
35            })
36        }
37    })
38)
39
40
41@AMF3ObjectBase.register("com.brightcove.experience.ViewerExperienceRequest")
42class ViewerExperienceRequest(AMF3ObjectBase):
43    __members__ = ["contentOverrides",
44                   "experienceId",
45                   "URL",
46                   "playerKey",
47                   "deliveryType",
48                   "TTLToken"]
49
50    def __init__(self, URL, contentOverrides, experienceId, playerKey, TTLToken=""):
51        self.URL = URL
52        self.deliveryType = float("nan")
53        self.contentOverrides = contentOverrides
54        self.experienceId = experienceId
55        self.playerKey = playerKey
56        self.TTLToken = TTLToken
57
58
59@AMF3ObjectBase.register("com.brightcove.experience.ContentOverride")
60class ContentOverride(AMF3ObjectBase):
61    __members__ = ["featuredRefId",
62                   "contentRefIds",
63                   "contentId",
64                   "contentType",
65                   "contentIds",
66                   "featuredId",
67                   "contentRefId",
68                   "target"]
69
70    def __init__(self, contentId=float("nan"), contentRefId=None, contentType=0,
71                 target="videoPlayer"):
72        self.contentType = contentType
73        self.contentId = contentId
74        self.target = target
75        self.contentIds = None
76        self.contentRefId = contentRefId
77        self.contentRefIds = None
78        self.contentType = 0
79        self.featuredId = float("nan")
80        self.featuredRefId = None
81
82
83class AzubuTV(Plugin):
84    @classmethod
85    def can_handle_url(cls, url):
86        return _url_re.match(url)
87
88    @classmethod
89    def stream_weight(cls, stream):
90        if stream == "source":
91            weight = 1080
92        else:
93            weight, group = Plugin.stream_weight(stream)
94
95        return weight, "azubutv"
96
97    def _create_amf_request(self, key, video_player, player_id):
98        if video_player.startswith("ref:"):
99            content_override = ContentOverride(contentRefId=video_player[4:])
100        else:
101            content_override = ContentOverride(contentId=int(video_player))
102        viewer_exp_req = ViewerExperienceRequest(self.url,
103                                                [content_override],
104                                                int(player_id), key)
105
106        req = AMFPacket(version=3)
107        req.messages.append(AMFMessage(
108            "com.brightcove.experience.ExperienceRuntimeFacade.getDataForExperience",
109            "/1",
110            [AMF_MESSAGE_PREFIX, viewer_exp_req]
111        ))
112
113        return req
114
115    def _send_amf_request(self, req, key):
116        headers = {
117            "content-type": "application/x-amf"
118        }
119        res = http.post(AMF_GATEWAY, data=bytes(req.serialize()),
120                        headers=headers, params=dict(playerKey=key))
121
122        return AMFPacket.deserialize(BytesIO(res.content))
123
124    def _get_player_params(self, retries=5):
125        try:
126            res = http.get(self.url, headers=HTTP_HEADERS)
127        except PluginError as err:
128            # The server sometimes gives us 404 for no reason
129            if "404" in str(err) and retries:
130                sleep(1)
131                return self._get_player_params(retries - 1)
132            else:
133                raise
134
135        match = re.search("<param name=\"playerKey\" value=\"(.+)\" />", res.text)
136        if not match:
137            # The HTML returned sometimes doesn't contain the parameters
138            if not retries:
139                raise PluginError("Missing key 'playerKey' in player params")
140            else:
141                sleep(1)
142                return self._get_player_params(retries - 1)
143
144        key = match.group(1)
145        match = re.search("AZUBU.setVar\(\"firstVideoRefId\", \"(.+)\"\);", res.text)
146        if not match:
147            # The HTML returned sometimes doesn't contain the parameters
148            if not retries:
149                raise PluginError("Unable to find video reference")
150            else:
151                sleep(1)
152                return self._get_player_params(retries - 1)
153
154        video_player = "ref:" + match.group(1)
155        match = re.search("<param name=\"playerID\" value=\"(\d+)\" />", res.text)
156        if not match:
157            # The HTML returned sometimes doesn't contain the parameters
158            if not retries:
159                raise PluginError("Missing key 'playerID' in player params")
160            else:
161                sleep(1)
162                return self._get_player_params(retries - 1)
163
164        player_id = match.group(1)
165        match = re.search("<!-- live on -->", res.text)
166        if not match:
167            match = re.search("<div id=\"channel_live\">", res.text)
168        is_live = not not match
169
170        return key, video_player, player_id, is_live
171
172    def _parse_result(self, res):
173        res = _viewerexp_schema.validate(res)
174        player = res.programmedContent["videoPlayer"]
175        renditions = sorted(player.mediaDTO.renditions.values(),
176                            key=lambda r: r.encodingRate or 100000000)
177
178        streams = {}
179        for stream_name, rendition in zip(STREAM_NAMES, renditions):
180            stream = AkamaiHDStream(self.session, rendition.defaultURL)
181            streams[stream_name] = stream
182
183        return streams
184
185    def _get_streams(self):
186        key, video_player, player_id, is_live = self._get_player_params()
187
188        if not is_live:
189            return
190
191        req = self._create_amf_request(key, video_player, player_id)
192        res = self._send_amf_request(req, key)
193
194        streams = {}
195        for message in res.messages:
196            if message.target_uri == "/1/onResult":
197                streams = self._parse_result(message.value)
198
199        return streams
200
201__plugin__ = AzubuTV
202