summaryrefslogtreecommitdiff
path: root/plugin.video.viaplay/resources
diff options
context:
space:
mode:
Diffstat (limited to 'plugin.video.viaplay/resources')
-rw-r--r--plugin.video.viaplay/resources/language/Swedish/strings.po158
-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.py228
-rw-r--r--plugin.video.viaplay/resources/lib/vialib.py371
-rw-r--r--plugin.video.viaplay/resources/lib/viaplay.py347
-rw-r--r--plugin.video.viaplay/resources/settings.xml6
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>