diff options
Diffstat (limited to 'plugin.video.viaplay/resources')
-rw-r--r-- | plugin.video.viaplay/resources/language/Swedish/strings.po | 158 | ||||
-rw-r--r-- | plugin.video.viaplay/resources/language/resource.language.en_gb/strings.po (renamed from plugin.video.viaplay/resources/language/English/strings.po) | 72 | ||||
-rw-r--r-- | plugin.video.viaplay/resources/lib/kodihelper.py | 228 | ||||
-rw-r--r-- | plugin.video.viaplay/resources/lib/vialib.py | 371 | ||||
-rw-r--r-- | plugin.video.viaplay/resources/lib/viaplay.py | 347 | ||||
-rw-r--r-- | plugin.video.viaplay/resources/settings.xml | 6 |
6 files changed, 638 insertions, 544 deletions
diff --git a/plugin.video.viaplay/resources/language/Swedish/strings.po b/plugin.video.viaplay/resources/language/Swedish/strings.po deleted file mode 100644 index f586117..0000000 --- a/plugin.video.viaplay/resources/language/Swedish/strings.po +++ /dev/null @@ -1,158 +0,0 @@ -# Kodi Viaplay language file -msgid "" -msgstr "" -"Project-Id-Version: Kodi-Viaplay\n" -"Report-Msgid-Bugs-To: https://github.com/emilsvennesson/kodi-viaplay\n" -"POT-Creation-Date: 2016-07-05 14:30+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: Swedish\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: sv\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -msgctxt "#30001" -msgid "Email" -msgstr "" - -msgctxt "#30002" -msgid "Password" -msgstr "Lösenord" - -msgctxt "#30003" -msgid "General" -msgstr "Allmänt" - -msgctxt "#30004" -msgid "Advanced" -msgstr "Avancerat" - -msgctxt "#30005" -msgid "Error" -msgstr "" - -msgctxt "#30006" -msgid "Login failed. Please make sure that your account information is correct." -msgstr "Inloggning misslyckades. Kontrollera att dina användaruppgfiter är korrekta." - -msgctxt "#30007" -msgid "Country" -msgstr "Land" - -msgctxt "#30008" -msgid "Sweden" -msgstr "Sverige" - -msgctxt "#30009" -msgid "Denmark" -msgstr "Danmark" - -msgctxt "#30010" -msgid "Norway" -msgstr "Norge" - -msgctxt "#30011" -msgid "Finland" -msgstr "Finland" - -msgctxt "#30012" -msgid "Download available subtitles" -msgstr "Ladda ner tillgängliga undertexter" - -msgctxt "#30013" -msgid "List all in alphabetical order" -msgstr "Visa alla i alfabetisk ordning" - -msgctxt "#30014" -msgid "Season" -msgstr "Säsong" - -msgctxt "#30015" -msgid "Search" -msgstr "Sök" - -msgctxt "#30016" -msgid "This event starts" -msgstr "Detta event startar" - -msgctxt "#30017" -msgid "Information" -msgstr "" - -msgctxt "#30018" -msgid "Next Page" -msgstr "Nästa Sida" - -msgctxt "#30020" -msgid "Your account is not authorized to watch this content. You find information on how to upgrade your package on the Viaplay website." -msgstr "Ditt konto har inte tillgång till det här innehållet. Du hittar information om hur du uppgraderar ditt paket på Viaplays hemsida." - -msgctxt "#30021" -msgid "You need to rent/purchase this movie on the Viaplay website before playing it." -msgstr "Du måste hyra/köpa filmen på Viaplays hemsida innan du spelar upp den." - -msgctxt "#30022" -msgid "You're not allowed to watch content from this region." -msgstr "Det går inte att spela upp innehåll från regionen du befinner dig i." - -msgctxt "#30023" -msgid "Preferred stream quality (movies & TV)" -msgstr "Föredragen strömkvalitet (filmer & TV)" - -msgctxt "#30024" -msgid "Always use highest bitrate" -msgstr "Använd alltid högsta bitrate" - -msgctxt "#30025" -msgid "Ask" -msgstr "Fråga" - -msgctxt "#30026" -msgid "Select stream quality" -msgstr "Välj strömkvalitet" - -msgctxt "#30027" -msgid "Today" -msgstr "Idag" - -msgctxt "#30028" -msgid "Upcoming days" -msgstr "Kommande dagar" - -msgctxt "#30029" -msgid "Previous days" -msgstr "Föregående dagar" - -msgctxt "#30031" -msgid "Archived events" -msgstr "Arkiverade evenemang" - -msgctxt "#30032" -msgid "Enter your PIN code" -msgstr "Ange din PIN-kod" - -msgctxt "#30033" -msgid "Parental control" -msgstr "Barnlås" - -msgctxt "#30034" -msgid "The PIN code you have entered is incorrect." -msgstr "PIN-koden du angav är felaktig." - -msgctxt "#30035" -msgid "Limit bitrate" -msgstr "Begränsa bitrate" - -msgctxt "#30036" -msgid "Max bitrate allowed (Kbps)" -msgstr "Högsta tillåtna bitrate (Kbps)" - -msgctxt "#30037" -msgid "Live & upcoming events" -msgstr "Live & kommande evenemang" - -msgctxt "#30038" -msgid "No valid stream URL was found." -msgstr "Ingen giltig stream URL hittades." diff --git a/plugin.video.viaplay/resources/language/English/strings.po b/plugin.video.viaplay/resources/language/resource.language.en_gb/strings.po index 24d1666..1d7140d 100644 --- a/plugin.video.viaplay/resources/language/English/strings.po +++ b/plugin.video.viaplay/resources/language/resource.language.en_gb/strings.po @@ -38,27 +38,27 @@ msgid "Login failed. Please make sure that your account information is correct." msgstr "" msgctxt "#30007" -msgid "Country" +msgid "Site" msgstr "" msgctxt "#30008" -msgid "Sweden" +msgid "viaplay.se" msgstr "" msgctxt "#30009" -msgid "Denmark" +msgid "viaplay.dk" msgstr "" msgctxt "#30010" -msgid "Norway" +msgid "viaplay.no" msgstr "" msgctxt "#30011" -msgid "Finland" +msgid "viaplay.fi" msgstr "" msgctxt "#30012" -msgid "Download available subtitles" +msgid "Subtitles" msgstr "" msgctxt "#30013" @@ -66,7 +66,7 @@ msgid "List all in alphabetical order" msgstr "" msgctxt "#30014" -msgid "Season" +msgid "Season {0}" msgstr "" msgctxt "#30015" @@ -74,7 +74,7 @@ msgid "Search" msgstr "" msgctxt "#30016" -msgid "This event starts" +msgid "This event starts [B]{0}[/B]." msgstr "" msgctxt "#30017" @@ -82,19 +82,19 @@ msgid "Information" msgstr "" msgctxt "#30018" -msgid "Next Page" +msgid "[B]Next page[/B]" msgstr "" msgctxt "#30020" -msgid "Your account is not authorized to watch this content. You find information on how to upgrade your package on the Viaplay website." +msgid "This content is not included in your current subscription." msgstr "" msgctxt "#30021" -msgid "You need to rent/purchase this movie on the Viaplay website before playing it." +msgid "You need to rent or purchase this movie on the Viaplay website." msgstr "" msgctxt "#30022" -msgid "You're not allowed to watch content from this region." +msgid "This content is not available in your current region." msgstr "" msgctxt "#30023" @@ -156,3 +156,51 @@ msgstr "" msgctxt "#30038" msgid "No valid stream URL was found." msgstr "" + +msgctxt "#30039" +msgid "Go to [B]{0}[/B] and activate your device using the following registration code: [B]{1}[/B]" +msgstr "" + +msgctxt "#30040" +msgid "Activate your device" +msgstr "" + +msgctxt "#30041" +msgid "Categories" +msgstr "" + +msgctxt "#30042" +msgid "Log out" +msgstr "" + +msgctxt "#30043" +msgid "Are you sure you want to log out?" +msgstr "" + +msgctxt "#30044" +msgid "Subtitle language" +msgstr "" + +msgctxt "#30045" +msgid "Swedish" +msgstr "" + +msgctxt "#30046" +msgid "Danish" +msgstr "" + +msgctxt "#30047" +msgid "Norwegian" +msgstr "" + +msgctxt "#30048" +msgid "Finnish" +msgstr "" + +msgctxt "#30049" +msgid "No broadcast" +msgstr "" + +msgctxt "#30050" +msgid "You can watch two videos at the same time using a Viaplay account. Your account is currently being used to watch two other videos." +msgstr "" diff --git a/plugin.video.viaplay/resources/lib/kodihelper.py b/plugin.video.viaplay/resources/lib/kodihelper.py new file mode 100644 index 0000000..8b1ea38 --- /dev/null +++ b/plugin.video.viaplay/resources/lib/kodihelper.py @@ -0,0 +1,228 @@ +import urllib + +from viaplay import Viaplay + +import xbmc +import xbmcvfs +import xbmcgui +import xbmcplugin +import inputstreamhelper +from xbmcaddon import Addon + + +class KodiHelper(object): + def __init__(self, base_url=None, handle=None): + addon = self.get_addon() + self.base_url = base_url + self.handle = handle + self.addon_path = xbmc.translatePath(addon.getAddonInfo('path')) + self.addon_profile = xbmc.translatePath(addon.getAddonInfo('profile')) + self.addon_name = addon.getAddonInfo('id') + self.addon_version = addon.getAddonInfo('version') + self.language = addon.getLocalizedString + self.logging_prefix = '[%s-%s]' % (self.addon_name, self.addon_version) + if not xbmcvfs.exists(self.addon_profile): + xbmcvfs.mkdir(self.addon_profile) + if self.get_setting('first_run'): + self.get_addon().openSettings() + self.set_setting('first_run', 'false') + self.vp = Viaplay(self.addon_profile, self.get_country_code(), True) + + def get_addon(self): + """Returns a fresh addon instance.""" + return Addon() + + def get_setting(self, setting_id): + addon = self.get_addon() + setting = addon.getSetting(setting_id) + if setting == 'true': + return True + elif setting == 'false': + return False + else: + return setting + + def set_setting(self, key, value): + return self.get_addon().setSetting(key, value) + + def log(self, string): + msg = '%s: %s' % (self.logging_prefix, string) + xbmc.log(msg=msg, level=xbmc.LOGDEBUG) + + def get_country_code(self): + country_id = self.get_setting('site') + if country_id == '0': + country_code = 'se' + elif country_id == '1': + country_code = 'dk' + elif country_id == '2': + country_code = 'no' + else: + country_code = 'fi' + + return country_code + + def get_sub_lang(self): + sub_lang_id = self.get_setting('sub_lang') + if sub_lang_id == '0': + sub_lang = 'sv' + elif sub_lang_id == '1': + sub_lang = 'da' + elif sub_lang_id == '2': + sub_lang = 'no' + else: + sub_lang = 'fi' + + return sub_lang + + def dialog(self, dialog_type, heading, message=None, options=None, nolabel=None, yeslabel=None): + dialog = xbmcgui.Dialog() + if dialog_type == 'ok': + dialog.ok(heading, message) + elif dialog_type == 'yesno': + return dialog.yesno(heading, message, nolabel=nolabel, yeslabel=yeslabel) + elif dialog_type == 'select': + ret = dialog.select(heading, options) + if ret > -1: + return ret + else: + return None + + def authorize(self): + try: + self.vp.validate_session() + return True + except self.vp.ViaplayError as error: + if not error.value == 'PersistentLoginError' or error.value == 'MissingSessionCookieError': + raise + else: + return self.device_registration() + + def log_out(self): + confirm = self.dialog('yesno', self.language(30042), self.language(30043)) + if confirm: + self.vp.log_out() + # send Kodi back to home screen + xbmc.executebuiltin('XBMC.Container.Update(path, replace)') + xbmc.executebuiltin('XBMC.ActivateWindow(Home)') + + def device_registration(self): + """Presents a dialog with information on how to activate the device. + Attempts to authorize the device using the interval returned by the activation data.""" + activation_data = self.vp.get_activation_data() + message = self.language(30039).format(activation_data['verificationUrl'], activation_data['userCode']) + dialog = xbmcgui.DialogProgress() + xbmc.sleep(200) # small delay to prevent DialogProgress from hanging + dialog.create(self.language(30040), message) + secs = 0 + expires = activation_data['expires'] + + while not xbmc.Monitor().abortRequested() and secs < expires: + try: + self.vp.authorize_device(activation_data) + dialog.close() + return True + except self.vp.ViaplayError as error: + # raise all non-pending authorization errors + if not error.value == 'DeviceAuthorizationPendingError': + raise + secs += activation_data['interval'] + percent = int(100 * float(secs) / float(expires)) + dialog.update(percent, message) + xbmc.Monitor().waitForAbort(activation_data['interval']) + if dialog.iscanceled(): + dialog.close() + return False + + dialog.close() + return False + + def get_user_input(self, heading, hidden=False): + keyboard = xbmc.Keyboard('', heading, hidden) + keyboard.doModal() + if keyboard.isConfirmed(): + query = keyboard.getText() + self.log('User input string: %s' % query) + else: + query = None + + if query and len(query) > 0: + return query + else: + return None + + def get_numeric_input(self, heading): + dialog = xbmcgui.Dialog() + numeric_input = dialog.numeric(0, heading) + + if len(numeric_input) > 0: + return str(numeric_input) + else: + return None + + def add_item(self, title, params, items=False, folder=True, playable=False, info=None, art=None, content=False): + addon = self.get_addon() + listitem = xbmcgui.ListItem(label=title) + + if playable: + listitem.setProperty('IsPlayable', 'true') + folder = False + if art: + listitem.setArt(art) + else: + art = { + 'icon': addon.getAddonInfo('icon'), + 'fanart': addon.getAddonInfo('fanart') + } + listitem.setArt(art) + if info: + listitem.setInfo('video', info) + if content: + xbmcplugin.setContent(self.handle, content) + + recursive_url = self.base_url + '?' + urllib.urlencode(params) + + if items is False: + xbmcplugin.addDirectoryItem(self.handle, recursive_url, listitem, folder) + else: + items.append((recursive_url, listitem, folder)) + return items + + def eod(self): + """Tell Kodi that the end of the directory listing is reached.""" + xbmcplugin.endOfDirectory(self.handle) + + def play(self, guid=None, url=None, pincode=None): + if url: + guid = self.vp.get_products(url)['products'][0]['system']['guid'] + elif guid: + pass + else: + self.log('No guid or URL supplied.') + return False + + try: + stream = self.vp.get_stream(guid, pincode=pincode) + except self.vp.ViaplayError as error: + if not error.value == 'ParentalGuidancePinChallengeNeededError': + raise + if pincode: + self.dialog(dialog_type='ok', heading=self.language(30033), message=self.language(30034)) + else: + pincode = self.get_numeric_input(self.language(30032)) + if pincode: + self.play(guid, pincode=pincode) + return + + ia_helper = inputstreamhelper.Helper('mpd', drm='widevine') + if ia_helper.check_inputstream(): + playitem = xbmcgui.ListItem(path=stream['mpd_url']) + playitem.setContentLookup(False) + playitem.setMimeType('application/xml+dash') # prevents HEAD request that causes 404 error + playitem.setProperty('inputstreamaddon', 'inputstream.adaptive') + playitem.setProperty('inputstream.adaptive.manifest_type', 'mpd') + playitem.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha') + playitem.setProperty('inputstream.adaptive.license_key', stream['license_url'].replace('{widevineChallenge}', 'B{SSM}') + '|||JBlicense') + if self.get_setting('subtitles') and 'subtitles' in stream: + playitem.setSubtitles(self.vp.download_subtitles(stream['subtitles'], language_to_download=self.get_sub_lang())) + xbmcplugin.setResolvedUrl(self.handle, True, listitem=playitem) diff --git a/plugin.video.viaplay/resources/lib/vialib.py b/plugin.video.viaplay/resources/lib/vialib.py deleted file mode 100644 index 562ead3..0000000 --- a/plugin.video.viaplay/resources/lib/vialib.py +++ /dev/null @@ -1,371 +0,0 @@ -# -*- 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 diff --git a/plugin.video.viaplay/resources/lib/viaplay.py b/plugin.video.viaplay/resources/lib/viaplay.py new file mode 100644 index 0000000..bf0a338 --- /dev/null +++ b/plugin.video.viaplay/resources/lib/viaplay.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- +""" +A Kodi-agnostic library for Viaplay +""" +import codecs +import os +import cookielib +import calendar +import re +import json +import uuid +import HTMLParser +from collections import OrderedDict +from datetime import datetime, timedelta + +import iso8601 +import requests + + +class Viaplay(object): + def __init__(self, settings_folder, country, debug=False): + self.debug = debug + 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.device_key = 'xdk-%s' % self.country + self.base_url = 'https://content.viaplay.{0}/{1}'.format(self.country, self.device_key) + self.login_api = 'https://login.viaplay.%s/api' % self.country + try: + self.cookie_jar.load(ignore_discard=True, ignore_expires=True) + except IOError: + pass + self.http_session.cookies = self.cookie_jar + + class ViaplayError(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + def log(self, string): + if self.debug: + try: + print('[Viaplay]: %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('[Viaplay]: %s' % string.replace(bom, '')) + except: + pass + + def parse_url(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 = r'\{.+?\}' + result = re.search(template, url) + if result: + self.log('Unparsed URL: {0}'.format(url)) + url = re.sub(template, '', url) + + return url + + def make_request(self, url, method, params=None, payload=None, headers=None): + """Make an HTTP request. Return the response.""" + url = self.parse_url(url) + self.log('Request URL: %s' % url) + self.log('Method: %s' % method) + if params: + self.log('Params: %s' % params) + if payload: + self.log('Payload: %s' % payload) + if headers: + self.log('Headers: %s' % headers) + + if method == 'get': + req = self.http_session.get(url, params=params, headers=headers) + elif method == 'put': + req = self.http_session.put(url, params=params, data=payload, headers=headers) + else: # post + req = self.http_session.post(url, params=params, data=payload, headers=headers) + 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 self.parse_response(req.content) + + def parse_response(self, response): + """Try to load JSON data into dict and raise potential errors.""" + try: + response = json.loads(response, object_pairs_hook=OrderedDict) # keep the key order + if 'success' in response and not response['success']: # raise ViaplayError when 'success' is False + raise self.ViaplayError(response['name'].encode('utf-8')) + except ValueError: # if response is not json + pass + + return response + + def get_activation_data(self): + """Get activation data (reg code etc) needed to authorize the device.""" + url = self.login_api + '/device/code' + params = { + 'deviceKey': self.device_key, + 'deviceId': self.get_deviceid() + } + + return self.make_request(url=url, method='get', params=params) + + def authorize_device(self, activation_data): + """Try to register the device. This will set the session cookies.""" + url = self.login_api + '/device/authorized' + params = { + 'deviceId': self.get_deviceid(), + 'deviceToken': activation_data['deviceToken'], + 'userCode': activation_data['userCode'] + } + + self.make_request(url=url, method='get', params=params) + self.validate_session() # we need this to validate the new cookies + return True + + def validate_session(self): + """Check if the session is valid.""" + url = self.login_api + '/persistentLogin/v1' + params = { + 'deviceKey': self.device_key + } + self.make_request(url=url, method='get', params=params) + return True + + def log_out(self): + """Log out from Viaplay.""" + url = self.login_api + '/logout/v1' + params = { + 'deviceKey': self.device_key + } + self.make_request(url=url, method='get', params=params) + return True + + def get_stream(self, guid, pincode=None): + """Return a dict with the stream URL:s and available subtitle URL:s.""" + stream = {} + url = 'https://play.viaplay.%s/api/stream/byguid' % self.country + params = { + 'deviceId': self.get_deviceid(), + 'deviceName': 'web', + 'deviceType': 'pc', + 'userAgent': 'Kodi', + 'deviceKey': 'pcdash-%s' % self.country, + 'guid': guid + } + if pincode: + params['pgPin'] = pincode + + data = self.make_request(url=url, method='get', params=params) + if 'viaplay:media' in data['_links']: + mpd_url = data['_links']['viaplay:media']['href'] + elif 'viaplay:fallbackMedia' in data['_links']: + mpd_url = data['_links']['viaplay:fallbackMedia'][0]['href'] + elif 'viaplay:playlist' in data['_links']: + mpd_url = data['_links']['viaplay:playlist']['href'] + elif 'viaplay:encryptedPlaylist' in data['_links']: + mpd_url = data['_links']['viaplay:encryptedPlaylist']['href'] + else: + self.log('Failed to retrieve stream URL.') + return False + + stream['mpd_url'] = mpd_url + stream['license_url'] = data['_links']['viaplay:license']['href'] + stream['release_pid'] = data['_links']['viaplay:license']['releasePid'] + if 'viaplay:sami' in data['_links']: + stream['subtitles'] = [x['href'] for x in data['_links']['viaplay:sami']] + + return stream + + def get_root_page(self): + """Dynamically builds the root page from the returned _links. + Uses the named dict as 'name' when no 'name' exists in the dict.""" + pages = [] + blacklist = ['byGuid'] + data = self.make_request(url=self.base_url, method='get') + if 'user' not in data: + raise self.ViaplayError('MissingSessionCookieError') # raise error if user is not logged in + + for link in data['_links']: + if isinstance(data['_links'][link], dict): + # sort out _links that doesn't contain a title + if 'title' in data['_links'][link]: + title = data['_links'][link]['title'] + data['_links'][link]['name'] = link # add name key to dict + if not title.islower() and title not in blacklist: + pages.append(data['_links'][link]) + else: # list (viaplay:sections for example) + for i in data['_links'][link]: + if 'title' in i and not i['title'].islower(): + pages.append(i) + + return pages + + def get_collections(self, url): + """Return all available collections.""" + data = self.make_request(url=url, method='get') + # return all blocks (collections) with 'list' in type + return [x for x in data['_embedded']['viaplay:blocks'] if 'list' in x['type'].lower()] + + def get_products(self, url, filter_event=False, search_query=None): + """Return a dict containing the products and next page if available.""" + if search_query: + params = {'query': search_query} + else: + params = None + data = self.make_request(url, method='get', params=params) + + if 'list' in data['type'].lower(): + products = data['_embedded']['viaplay:products'] + elif data['type'] == 'tvChannel': + # sort out 'nobroadcast' items + products = [x for x in data['_embedded']['viaplay:products'] if 'nobroadcast' not in x['system']['flags']] + elif data['type'] == 'product': + # explicity put into list when only one product is returned + products = [data['_embedded']['viaplay:product']] + else: + # try to collect all products found in viaplay:blocks + products = [p for x in data['_embedded']['viaplay:blocks'] if 'viaplay:products' in x['_embedded'] for p in x['_embedded']['viaplay:products']] + + if filter_event: + # filter out and only return products with event_status in filter_event + products = [x for x in products if x['event_status'] in filter_event] + + products_dict = { + 'products': products, + 'next_page': self.get_next_page(data) + } + + return products_dict + + def get_channels(self, url): + data = self.make_request(url, method='get') + channels_block = data['_embedded']['viaplay:blocks'][0]['_embedded']['viaplay:blocks'] + channels = [x['viaplay:channel'] for x in channels_block] + channels_dict = { + 'channels': channels, + 'next_page': self.get_next_page(data) + } + + return channels_dict + + def get_seasons(self, url): + """Return all available series seasons.""" + data = self.make_request(url=url, method='get') + return [x for x in data['_embedded']['viaplay:blocks'] if x['type'] == 'season-list'] + + def download_subtitles(self, suburls, language_to_download=None): + """Download the SAMI subtitles, decode the HTML entities and save to temp directory. + Return a list of the path to the downloaded subtitles.""" + paths = [] + for url in suburls: + lang_pattern = re.search(r'[_]([a-z]+)', url) + if lang_pattern: + sub_lang = lang_pattern.group(1) + else: + sub_lang = 'unknown' + self.log('Failed to identify subtitle language.') + + if language_to_download and sub_lang not in language_to_download: + continue + else: + sami = self.make_request(url=url, method='get').decode('utf-8', 'ignore').strip() + htmlparser = HTMLParser.HTMLParser() + subtitle = htmlparser.unescape(sami).encode('utf-8') + path = os.path.join(self.tempdir, '{0}.sami'.format(sub_lang)) + with open(path, 'w') as subfile: + subfile.write(subtitle) + paths.append(path) + + return 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/program is live/upcoming/archive.""" + now = datetime.utcnow() + if 'startTime' in data['epg']: + start_time = data['epg']['startTime'] + end_time = data['epg']['endTime'] + else: + start_time = data['epg']['start'] + end_time = data['epg']['end'] + start_time_obj = self.parse_datetime(start_time).replace(tzinfo=None) + end_time_obj = self.parse_datetime(end_time).replace(tzinfo=None) + + if 'isLive' in data['system']['flags']: + status = 'live' + elif now >= start_time_obj and now < end_time_obj: + status = 'live' + elif start_time_obj >= now: + status = 'upcoming' + else: + status = 'archive' + + return status + + def get_next_page(self, data): + """Return the URL to the next page. Returns False when there is no next page.""" + if data['type'] == 'page': # multiple blocks in _embedded, find the right one + for block in data['_embedded']['viaplay:blocks']: + if 'list' in block['type'].lower() or 'grid' in block['type'].lower(): + data = block + break + elif data['type'] == 'product': + data = data['_embedded']['viaplay:product'] + if 'next' in data['_links']: + next_page_url = data['_links']['next']['href'] + return next_page_url + + return False + + 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 + + @staticmethod + def utc_to_local(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) diff --git a/plugin.video.viaplay/resources/settings.xml b/plugin.video.viaplay/resources/settings.xml index 57cd9a0..0aad478 100644 --- a/plugin.video.viaplay/resources/settings.xml +++ b/plugin.video.viaplay/resources/settings.xml @@ -1,8 +1,8 @@ <settings> <category label="30003"> - <setting id="country" type="enum" label="30007" lvalues="30008|30009|30010|30011" default="0"/> - <setting id="email" type="text" label="30001" default=""/> - <setting id="password" type="text" label="30002" option="hidden" enable="!eq(-1,)" default=""/> + <setting id="site" type="enum" label="30007" lvalues="30008|30009|30010|30011" default="0"/> <setting id="subtitles" type="bool" label="30012" default="true"/> + <setting id="sub_lang" type="enum" label="30044" lvalues="30045|30046|30047|30048" default="0" visible="!eq(-1,false)" subsetting="true"/> + <setting id="first_run" type="bool" default="true" visible="false"/> </category> </settings> |