summaryrefslogtreecommitdiff
path: root/plugin.video.viaplay/resources/lib/vialib.py
blob: 562ead3963d0ac61c934ac7d35d7f3b9febec596 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# -*- coding: utf-8 -*-
"""
A Kodi-agnostic library for Viaplay
"""
import codecs
import os
import cookielib
import calendar
import time
import re
import json
import uuid
import HTMLParser
from urllib import urlencode
from datetime import datetime, timedelta

import iso8601
import requests


class vialib(object):
    def __init__(self, username, password, settings_folder, country, debug=False):
        self.debug = debug
        self.username = username
        self.password = password
        self.country = country
        self.settings_folder = settings_folder
        self.cookie_jar = cookielib.LWPCookieJar(os.path.join(self.settings_folder, 'cookie_file'))
        self.tempdir = os.path.join(settings_folder, 'tmp')
        if not os.path.exists(self.tempdir):
            os.makedirs(self.tempdir)
        self.deviceid_file = os.path.join(settings_folder, 'deviceId')
        self.http_session = requests.Session()
        self.base_url = 'https://content.viaplay.%s/pc-%s' % (self.country, self.country)
        try:
            self.cookie_jar.load(ignore_discard=True, ignore_expires=True)
        except IOError:
            pass
        self.http_session.cookies = self.cookie_jar

    class LoginFailure(Exception):
        def __init__(self, value):
            self.value = value

        def __str__(self):
            return repr(self.value)

    class AuthFailure(Exception):
        def __init__(self, value):
            self.value = value

        def __str__(self):
            return repr(self.value)

    def log(self, string):
        if self.debug:
            try:
                print '[vialib]: %s' % string
            except UnicodeEncodeError:
                # we can't anticipate everything in unicode they might throw at
                # us, but we can handle a simple BOM
                bom = unicode(codecs.BOM_UTF8, 'utf8')
                print '[vialib]: %s' % string.replace(bom, '')
            except:
                pass

    def url_parser(self, url):
        """Sometimes, Viaplay adds some weird templated stuff to the URL
        we need to get rid of. Example: https://content.viaplay.se/androiddash-se/serier{?dtg}"""
        template = re.search(r'\{.+?\}', url)
        if template:
            url = url.replace(template.group(), '')

        return url

    def make_request(self, url, method, payload=None, headers=None):
        """Make an HTTP request. Return the JSON response in a dict."""
        self.log('URL: %s' % url)
        parsed_url = self.url_parser(url)
        if parsed_url != url:
            url = parsed_url
            self.log('Parsed URL: %s' % url)
        if method == 'get':
            req = self.http_session.get(url, params=payload, headers=headers, allow_redirects=False, verify=False)
        else:
            req = self.http_session.post(url, data=payload, headers=headers, allow_redirects=False, verify=False)
        self.log('Response code: %s' % req.status_code)
        self.log('Response: %s' % req.content)
        self.cookie_jar.save(ignore_discard=True, ignore_expires=False)

        return json.loads(req.content)

    def login(self, username, password):
        """Login to Viaplay. Return True/False based on the result."""
        url = 'https://login.viaplay.%s/api/login/v1' % self.country
        payload = {
            'deviceKey': 'pc-%s' % self.country,
            'username': username,
            'password': password,
            'persistent': 'true'
        }
        data = self.make_request(url=url, method='get', payload=payload)

        return data['success']

    def validate_session(self):
        """Check if our session cookies are still valid."""
        url = 'https://login.viaplay.%s/api/persistentLogin/v1' % self.country
        payload = {
            'deviceKey': 'pc-%s' % self.country
        }
        data = self.make_request(url=url, method='get', payload=payload)

        return data['success']

    def verify_login_status(self, data):
        """Check if we're logged in. If we're not, try to.
        Raise errors as LoginFailure."""
        if 'MissingSessionCookieError' in data.values():
            if not self.validate_session():
                if not self.login(self.username, self.password):
                    raise self.LoginFailure('login failed')

    def get_video_urls(self, guid, pincode=None):
        """Return a dict with the stream URL:s and available subtitle URL:s."""
        video_urls = {}
        url = 'https://play.viaplay.%s/api/stream/byguid' % self.country
        payload = {
            'deviceId': self.get_deviceid(),
            'deviceName': 'web',
            'deviceType': 'pc',
            'userAgent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0',
            'deviceKey': 'atv-%s' % self.country,
            'guid': guid,
            'pgPin': pincode
        }

        data = self.make_request(url=url, method='get', payload=payload)
        self.verify_login_status(data)
        # we might have to request the stream again after logging in
        if 'MissingSessionCookieError' in data.values():
            data = self.make_request(url=url, method='get', payload=payload)
        self.check_for_subscription(data)

        for x in xrange(3):  # retry if we get an encrypted playlist
            if not 'viaplay:encryptedPlaylist' in data['_links'].keys():
                break
            data = self.make_request(url=url, method='get', payload=payload)
        if 'viaplay:media' in data['_links'].keys():
            manifest_url = data['_links']['viaplay:media']['href']
        elif 'viaplay:fallbackMedia' in data['_links'].keys():
            manifest_url = data['_links']['viaplay:fallbackMedia'][0]['href']
        elif 'viaplay:playlist' in data['_links'].keys():
            manifest_url = data['_links']['viaplay:playlist']['href']
        else:
            self.log('Unable to retrieve stream URL.')
            return False

        video_urls['manifest_url'] = manifest_url
        video_urls['subtitle_urls'] = self.get_subtitle_urls(data)

        return video_urls

    def check_for_subscription(self, data):
        """Check if the user is authorized to watch the requested stream.
        Raise errors as AuthFailure."""
        try:
            if data['success'] is False:
                subscription_error = data['name']
                raise self.AuthFailure(subscription_error)
        except KeyError:
            # 'success' won't be in response if it's successful
            pass

    def get_categories(self, input, method=None):
        if method == 'data':
            data = input
        else:
            data = self.make_request(url=input, method='get')

        if data['pageType'] == 'root':
            categories = data['_links']['viaplay:sections']
        elif data['pageType'] == 'section':
            categories = data['_links']['viaplay:categoryFilters']

        return categories

    def get_sortings(self, url):
        data = self.make_request(url=url, method='get')
        try:
            sorttypes = data['_links']['viaplay:sortings']
        except KeyError:
            self.log('No sortings available for this category.')
            return None

        return sorttypes

    def get_letters(self, url):
        """Return a list of available letters for sorting in alphabetical order."""
        letters = []
        products = self.get_products(input=url, method='url')
        for item in products:
            letter = item['group'].encode('utf-8')
            if letter not in letters:
                letters.append(letter)

        return letters

    def get_products(self, input, method=None, filter_event=False):
        """Return a list of all available products."""
        if method == 'data':
            data = input
        else:
            data = self.make_request(url=input, method='get')

        if 'list' in data['type']:
            products = data['_embedded']['viaplay:products']
        elif data['type'] == 'product':
            products = data['_embedded']['viaplay:product']
        else:
            products = self.get_products_block(data)['_embedded']['viaplay:products']

        try:
            # try adding additional info to sports dict
            aproducts = []
            for product in products:
                if product['type'] == 'sport':
                    product['event_date'] = self.parse_datetime(product['epg']['start'], localize=True)
                    product['event_status'] = self.get_event_status(product)
                aproducts.append(product)
            products = aproducts
        except TypeError:
            pass

        if filter_event:
            fproducts = []
            for product in products:
                for event in filter_event:
                    if event == product['event_status']:
                        fproducts.append(product)
            products = fproducts

        return products

    def get_seasons(self, url):
        """Return all available series seasons as a list."""
        seasons = []
        data = self.make_request(url=url, method='get')

        items = data['_embedded']['viaplay:blocks']
        for item in items:
            if item['type'] == 'season-list':
                seasons.append(item)

        return seasons

    def get_subtitle_urls(self, data):
        """Return all subtitle SAMI URL:s in a list."""
        subtitle_urls = []
        try:
            for subtitle in data['_links']['viaplay:sami']:
                subtitle_urls.append(subtitle['href'])
        except KeyError:
            self.log('No subtitles found for guid: %s' % data['socket2']['productGuid'])

        return subtitle_urls

    def download_subtitles(self, suburls):
        """Download the SAMI subtitles, decode the HTML entities and save to temp directory.
        Return a list of the path to the downloaded subtitles."""
        subtitle_paths = []
        for suburl in suburls:
            req = requests.get(suburl)
            sami = req.content.decode('utf-8', 'ignore').strip()
            htmlparser = HTMLParser.HTMLParser()
            subtitle = htmlparser.unescape(sami).encode('utf-8')
            subtitle = subtitle.replace('  ', ' ')  # replace two spaces with one

            subpattern = re.search(r'[_]([a-z]+)', suburl)
            if subpattern:
                sublang = subpattern.group(1)
            else:
                sublang = 'unknown'
                self.log('Unable to identify subtitle language.')

            path = os.path.join(self.tempdir, '%s.sami') % sublang
            with open(path, 'w') as subfile:
                subfile.write(subtitle)
            subtitle_paths.append(path)

        return subtitle_paths

    def get_deviceid(self):
        """"Read/write deviceId (generated UUID4) from/to file and return it."""
        try:
            with open(self.deviceid_file, 'r') as deviceid:
                return deviceid.read()
        except IOError:
            deviceid = str(uuid.uuid4())
            with open(self.deviceid_file, 'w') as idfile:
                idfile.write(deviceid)
            return deviceid

    def get_event_status(self, data):
        """Return whether the event is live/upcoming/archive."""
        now = datetime.utcnow()
        producttime_start = self.parse_datetime(data['epg']['start'])
        producttime_start = producttime_start.replace(tzinfo=None)
        if 'isLive' in data['system']['flags']:
            status = 'live'
        elif producttime_start >= now:
            status = 'upcoming'
        else:
            status = 'archive'

        return status

    def get_sports_dates(self, url, event_date=None):
        """Return the available sports dates.
        Filter upcoming/previous dates with the event_date parameter."""
        dates = []
        data = self.make_request(url=url, method='get')
        dates_data = data['_links']['viaplay:days']
        now = datetime.now()

        for date in dates_data:
            date_obj = datetime(*(time.strptime(date['date'], '%Y-%m-%d')[0:6]))  # http://forum.kodi.tv/showthread.php?tid=112916
            if event_date == 'upcoming':
                if date_obj.date() > now.date():
                    dates.append(date)
            elif event_date == 'archive':
                if date_obj.date() < now.date():
                    dates.append(date)
            else:
                dates.append(date)

        return dates

    def get_next_page(self, data):
        """Return the URL to the next page if the current page count is less than the total page count."""
        # first page is always (?) from viaplay:blocks
        if data['type'] == 'page':
            data = self.get_products_block(data)
        if int(data['pageCount']) > int(data['currentPage']):
            next_page_url = data['_links']['next']['href']
            return next_page_url

    def get_products_block(self, data):
        """Get the viaplay:blocks containing all product information."""
        blocks = []
        blocks_data = data['_embedded']['viaplay:blocks']
        for block in blocks_data:
            # example: https://content.viaplay.se/pc-se/sport
            if 'viaplay:products' in block['_embedded'].keys():
                blocks.append(block)
        return blocks[-1]  # the last block is always (?) the right one

    def utc_to_local(self, utc_dt):
        # get integer timestamp to avoid precision lost
        timestamp = calendar.timegm(utc_dt.timetuple())
        local_dt = datetime.fromtimestamp(timestamp)
        assert utc_dt.resolution >= timedelta(microseconds=1)
        return local_dt.replace(microsecond=utc_dt.microsecond)

    def parse_datetime(self, iso8601_string, localize=False):
        """Parse ISO8601 string to datetime object."""
        datetime_obj = iso8601.parse_date(iso8601_string)
        if localize:
            return self.utc_to_local(datetime_obj)
        else:
            return datetime_obj