summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoremilsvennesson <emilsvennesson@users.noreply.github.com>2017-12-27 17:33:54 +0100
committerenen92 <enen92@users.noreply.github.com>2017-12-27 16:33:54 +0000
commitef2b6daac59ba9971220cbedf04ec1160d455e30 (patch)
treeaa95328eb10434cead54d0cdeb3e88a172c17534
parentdf656f1563f88101fbc8fb62c69b715262e79c8f (diff)
[plugin.video.viaplay] 2.0.1 (#1572)
-rw-r--r--plugin.video.viaplay/LICENSE.txt126
-rw-r--r--plugin.video.viaplay/README.md50
-rw-r--r--plugin.video.viaplay/addon.py943
-rw-r--r--plugin.video.viaplay/addon.xml19
-rw-r--r--plugin.video.viaplay/changelog.txt16
-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
11 files changed, 1069 insertions, 1267 deletions
diff --git a/plugin.video.viaplay/LICENSE.txt b/plugin.video.viaplay/LICENSE.txt
index 10d3823..8da6560 100644
--- a/plugin.video.viaplay/LICENSE.txt
+++ b/plugin.video.viaplay/LICENSE.txt
@@ -1,8 +1,4 @@
-Files: *
-Copyright: 2016 (see git for respective authors)
-License: GPLv3
-
- GNU GENERAL PUBLIC LICENSE
+ GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
@@ -676,123 +672,3 @@ may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
-
-Files: fanart.jpg
-License: Creative Commons Zero
-
-CC0 1.0 Universal
-
-Statement of Purpose
-
-The laws of most jurisdictions throughout the world automatically confer
-exclusive Copyright and Related Rights (defined below) upon the creator and
-subsequent owner(s) (each and all, an "owner") of an original work of
-authorship and/or a database (each, a "Work").
-
-Certain owners wish to permanently relinquish those rights to a Work for the
-purpose of contributing to a commons of creative, cultural and scientific
-works ("Commons") that the public can reliably and without fear of later
-claims of infringement build upon, modify, incorporate in other works, reuse
-and redistribute as freely as possible in any form whatsoever and for any
-purposes, including without limitation commercial purposes. These owners may
-contribute to the Commons to promote the ideal of a free culture and the
-further production of creative, cultural and scientific works, or to gain
-reputation or greater distribution for their Work in part through the use and
-efforts of others.
-
-For these and/or other purposes and motivations, and without any expectation
-of additional consideration or compensation, the person associating CC0 with a
-Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
-and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
-and publicly distribute the Work under its terms, with knowledge of his or her
-Copyright and Related Rights in the Work and the meaning and intended legal
-effect of CC0 on those rights.
-
-1. Copyright and Related Rights. A Work made available under CC0 may be
-protected by copyright and related or neighboring rights ("Copyright and
-Related Rights"). Copyright and Related Rights include, but are not limited
-to, the following:
-
- i. the right to reproduce, adapt, distribute, perform, display, communicate,
- and translate a Work;
-
- ii. moral rights retained by the original author(s) and/or performer(s);
-
- iii. publicity and privacy rights pertaining to a person's image or likeness
- depicted in a Work;
-
- iv. rights protecting against unfair competition in regards to a Work,
- subject to the limitations in paragraph 4(a), below;
-
- v. rights protecting the extraction, dissemination, use and reuse of data in
- a Work;
-
- vi. database rights (such as those arising under Directive 96/9/EC of the
- European Parliament and of the Council of 11 March 1996 on the legal
- protection of databases, and under any national implementation thereof,
- including any amended or successor version of such directive); and
-
- vii. other similar, equivalent or corresponding rights throughout the world
- based on applicable law or treaty, and any national implementations thereof.
-
-2. Waiver. To the greatest extent permitted by, but not in contravention of,
-applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
-unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
-and Related Rights and associated claims and causes of action, whether now
-known or unknown (including existing as well as future claims and causes of
-action), in the Work (i) in all territories worldwide, (ii) for the maximum
-duration provided by applicable law or treaty (including future time
-extensions), (iii) in any current or future medium and for any number of
-copies, and (iv) for any purpose whatsoever, including without limitation
-commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
-the Waiver for the benefit of each member of the public at large and to the
-detriment of Affirmer's heirs and successors, fully intending that such Waiver
-shall not be subject to revocation, rescission, cancellation, termination, or
-any other legal or equitable action to disrupt the quiet enjoyment of the Work
-by the public as contemplated by Affirmer's express Statement of Purpose.
-
-3. Public License Fallback. Should any part of the Waiver for any reason be
-judged legally invalid or ineffective under applicable law, then the Waiver
-shall be preserved to the maximum extent permitted taking into account
-Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
-is so judged Affirmer hereby grants to each affected person a royalty-free,
-non transferable, non sublicensable, non exclusive, irrevocable and
-unconditional license to exercise Affirmer's Copyright and Related Rights in
-the Work (i) in all territories worldwide, (ii) for the maximum duration
-provided by applicable law or treaty (including future time extensions), (iii)
-in any current or future medium and for any number of copies, and (iv) for any
-purpose whatsoever, including without limitation commercial, advertising or
-promotional purposes (the "License"). The License shall be deemed effective as
-of the date CC0 was applied by Affirmer to the Work. Should any part of the
-License for any reason be judged legally invalid or ineffective under
-applicable law, such partial invalidity or ineffectiveness shall not
-invalidate the remainder of the License, and in such case Affirmer hereby
-affirms that he or she will not (i) exercise any of his or her remaining
-Copyright and Related Rights in the Work or (ii) assert any associated claims
-and causes of action with respect to the Work, in either case contrary to
-Affirmer's express Statement of Purpose.
-
-4. Limitations and Disclaimers.
-
- a. No trademark or patent rights held by Affirmer are waived, abandoned,
- surrendered, licensed or otherwise affected by this document.
-
- b. Affirmer offers the Work as-is and makes no representations or warranties
- of any kind concerning the Work, express, implied, statutory or otherwise,
- including without limitation warranties of title, merchantability, fitness
- for a particular purpose, non infringement, or the absence of latent or
- other defects, accuracy, or the present or absence of errors, whether or not
- discoverable, all to the greatest extent permissible under applicable law.
-
- c. Affirmer disclaims responsibility for clearing rights of other persons
- that may apply to the Work or any use thereof, including without limitation
- any person's Copyright and Related Rights in the Work. Further, Affirmer
- disclaims responsibility for obtaining any necessary consents, permissions
- or other rights required for any use of the Work.
-
- d. Affirmer understands and acknowledges that Creative Commons is not a
- party to this document and has no duty or obligation with respect to this
- CC0 or use of the Work.
-
-For more information, please see
-<http://creativecommons.org/publicdomain/zero/1.0/>
diff --git a/plugin.video.viaplay/README.md b/plugin.video.viaplay/README.md
index 7f7f7b3..484c599 100644
--- a/plugin.video.viaplay/README.md
+++ b/plugin.video.viaplay/README.md
@@ -1,40 +1,26 @@
-# Viaplay Kodi add-on #
-Before reading further, please note that this add-on is unoffical and is not endorsed or supported by Viaplay in any way. Not all features are supported or thoroughly tested and may not work as intended.
+# Viaplay for Kodi #
+This is a Kodi add-on that allows you to stream content from Viaplay in Kodi.
-If you're interested in helping out with the development, then please just send me a pull request. If you're reporting a bug, please activate debug logging in Kodi and attach the log file.
-
-Feedback and constructive input are of course always welcome.
+## Disclaimer ##
+This add-on is unoffical and is not endorsed or supported by Viaplay in any way. Not all features may work or has been thoroughly tested.
## Dependencies: ##
This add-on is available in the official Kodi repository and all dependencies will be installed automatically when installed from there. However, if you're installing straight from git, please make sure you've got the following modules installed:
- * Requests >= 2.9.1 (http://mirrors.kodi.tv/addons/jarvis/script.module.requests)
- * iso8601 (http://mirrors.kodi.tv/addons/jarvis/script.module.iso8601)
-
-This add-on supports Kodi Krypton or later. While it may work fine on older versions as well, it is unsupported and you're encouraged to upgrade.
-
-## Features: ##
- * Support for Sweden, Denmark, Norway and Finland
- * Internal video playback
- * Categories
- * Movies
- * TV shows
- * Kids
- * Sports
- * Store
- * Subtitles
- * Search
- * A-Ö alphabetical listing
- * Parental control
+ * script.module.requests >= 2.9.1 (http://mirrors.kodi.tv/addons/krypton/script.module.requests/)
+ * script.module.iso8601 (http://mirrors.kodi.tv/addons/krypton/script.module.iso8601/)
+ * script.module.inputstreamhelper >= 0.2.2 (http://mirrors.kodi.tv/addons/krypton/script.module.inputstreamhelper/)
-Currently unsupported features:
+This add-on requires Kodi 17.4 or higher with InputStream Adaptive installed. Kodi 18 is required for Android based devices.
- * Viasat TV To Go
- * Starred
- * Activity list
- * ... And quite possibly more that I'm forgetting about!
+## DRM protected streams ##
+Viaplay's content is DRM protected and requires the proprietary decryption module Widevine CDM for playback. You will be prompted to install this if you're attempting to play a stream without the binary installed.
-## Roadmap ##
+Most Android devices have built-in support for Widevine DRM and doesn't require any additional binaries. You can see if your Android device supports Widevine DRM using the [DRM Info](https://play.google.com/store/apps/details?id=com.androidfung.drminfo) app available in Play Store.
+
+## Support ##
+Please report any issues or bug reports on the [GitHub Issues](https://github.com/emilsvennesson/kodi-viaplay/issues) page. Remember to include a full, non-cut off Kodi debug log. See the [Kodi wiki page](http://kodi.wiki/view/Log_file/Advanced) for more detailed instructions on how to obtain the log file.
+
+Additional support/discussion about the add-on can be found in the [Viaplay add-on thread](https://forum.kodi.tv/showthread.php?tid=286387).
- * Continue work towards feature completeness
- * MPEG-DASH support using inputstream.adaptive
- * Code cleanup
+## License ##
+This add-on is licensed under the **GNU GENERAL PUBLIC LICENSE Version 3**. Please see the [LICENSE.txt](LICENSE.txt) file for details.
diff --git a/plugin.video.viaplay/addon.py b/plugin.video.viaplay/addon.py
index a2a0a98..8d881b6 100644
--- a/plugin.video.viaplay/addon.py
+++ b/plugin.video.viaplay/addon.py
@@ -3,601 +3,415 @@
A Kodi add-on for Viaplay
"""
import sys
-import os
-import urllib
import urlparse
from datetime import datetime
-from resources.lib.vialib import vialib
-
-import xbmc
-import xbmcaddon
-import xbmcvfs
-import xbmcgui
-import xbmcplugin
-
-addon = xbmcaddon.Addon()
-addon_path = xbmc.translatePath(addon.getAddonInfo('path'))
-addon_profile = xbmc.translatePath(addon.getAddonInfo('profile'))
-language = addon.getLocalizedString
-logging_prefix = '[%s-%s]' % (addon.getAddonInfo('id'), addon.getAddonInfo('version'))
-
-if not xbmcvfs.exists(addon_profile):
- xbmcvfs.mkdir(addon_profile)
-
-_url = sys.argv[0] # get the plugin url in plugin:// notation
-_handle = int(sys.argv[1]) # get the plugin handle as an integer number
-
-username = addon.getSetting('email')
-password = addon.getSetting('password')
-
-if addon.getSetting('country') == '0':
- country = 'se'
-elif addon.getSetting('country') == '1':
- country = 'dk'
-elif addon.getSetting('country') == '2':
- country = 'no'
-else:
- country = 'fi'
-
-vp = vialib(username, password, addon_profile, country, debug=True)
-
-
-def addon_log(string):
- msg = '%s: %s' % (logging_prefix, string)
- xbmc.log(msg=msg, level=xbmc.LOGDEBUG)
-
-
-def main_menu():
- items = []
- data = vp.make_request(url=vp.base_url, method='get')
- categories = vp.get_categories(input=data, method='data')
-
- for category in categories:
- category_name = category['name']
- title = category['title']
- if category['type'] != 'editorial':
- if category_name == 'sport':
- parameters = {
- 'action': 'sports_menu',
- 'url': category['href']
- }
- else:
- parameters = {
- 'action': 'list_categories',
- 'url': category['href'],
- 'category_name': category_name
- }
-
- items = add_item(title, parameters, items=items)
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- list_search(data)
- xbmcplugin.endOfDirectory(_handle)
-
-
-def list_categories(url, category_name):
- items = []
- categories = vp.get_categories(url)
-
- for category in categories:
- if category_name == 'kids':
- title = '%s: %s' % (category['group']['title'].title(), category['title'])
+from resources.lib.kodihelper import KodiHelper
+
+base_url = sys.argv[0]
+handle = int(sys.argv[1])
+helper = KodiHelper(base_url, handle)
+
+
+def run():
+ try:
+ router(sys.argv[2][1:]) # trim the leading '?' from the plugin call paramstring
+ except helper.vp.ViaplayError as error:
+ if error.value == 'MissingSessionCookieError':
+ if helper.authorize():
+ router(sys.argv[2][1:])
else:
- title = category['title']
+ show_error(error.value)
- parameters = {
- 'action': 'list_sortings',
- 'url': category['href']
- }
- items = add_item(title, parameters, items=items)
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- xbmcplugin.endOfDirectory(_handle)
-
-
-def list_sortings(url):
- items = []
- sortings = vp.get_sortings(url)
- if sortings:
- for sorting in sortings:
- title = sorting['title']
- sorting_url = sorting['href']
- try:
- if sorting['id'] == 'alphabetical':
- parameters = {
- 'action': 'list_alphabetical_letters',
- 'url': sorting_url
- }
- else:
- parameters = {
- 'action': 'list_products',
- 'url': sorting_url
- }
- except TypeError:
- parameters = {
- 'action': 'list_products',
- 'url': sorting_url
- }
-
- items = add_item(title, parameters, items=items)
- list_products_alphabetical(url)
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- xbmcplugin.endOfDirectory(_handle)
-
-
-def list_products_alphabetical(url):
- """List all products in alphabetical order."""
- title = language(30013)
- parameters = {
- 'action': 'list_products',
- 'url': url + '?sort=alphabetical'
- }
- add_item(title, parameters)
+def root_page():
+ pages = helper.vp.get_root_page()
+ for page in pages:
+ params = {
+ 'action': page['name'],
+ 'url': page['href']
+ }
+ helper.add_item(page['title'], params)
+ helper.eod()
-def list_alphabetical_letters(url):
- items = []
- letters = vp.get_letters(url)
- for letter in letters:
- if letter == '0-9':
- query = '#' # 0-9 needs to be sent as a number sign
- else:
- query = letter.lower()
+def start_page(url):
+ collections = helper.vp.get_collections(url)
- parameters = {
+ for i in collections:
+ params = {
'action': 'list_products',
- 'url': url + '&letter=' + urllib.quote(query)
+ 'url': i['_links']['self']['href']
}
+ helper.add_item(i['title'], params)
+ helper.eod()
- items = add_item(letter, parameters, items=items)
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- xbmcplugin.endOfDirectory(_handle)
+def vod_page(url):
+ """List categories and collections from the VOD pages (movies, series, kids, store)."""
+ collections = helper.vp.get_collections(url)
-def list_next_page(data):
- if vp.get_next_page(data):
- title = language(30018)
- parameters = {
+ categories_item(url)
+ for i in collections:
+ params = {
'action': 'list_products',
- 'url': vp.get_next_page(data)
+ 'url': i['_links']['self']['href']
}
+ helper.add_item(i['title'], params)
+ helper.eod()
- add_item(title, parameters)
+def categories_page(url):
+ categories = helper.vp.make_request(url, 'get')['_links']['viaplay:categoryFilters']
-def list_products(url, filter_event=False):
- items = []
- data = vp.make_request(url=url, method='get')
- if filter_event:
- filter_event = filter_event.split(', ')
+ for i in categories:
+ params = {
+ 'action': 'sortings_page',
+ 'url': i['href']
+ }
+ helper.add_item(i['title'], params)
+ helper.eod()
- products = vp.get_products(input=data, method='data', filter_event=filter_event)
-
- for product in products:
- content = product['type']
- try:
- playid = product['system']['guid']
- streamtype = 'guid'
- except KeyError:
- """The guid is not always available from the category listing.
- Send the self URL and let play_video grab the guid from there instead
- as it always provides more detailed data about each product."""
- playid = product['_links']['self']['href']
- streamtype = 'url'
-
- parameters = {
- 'action': 'play_video',
- 'playid': playid.encode('utf-8'),
- 'streamtype': streamtype,
- 'content': content
+
+def sortings_page(url):
+ sortings = helper.vp.make_request(url, 'get')['_links']['viaplay:sortings']
+
+ for i in sortings:
+ params = {
+ 'action': 'list_products',
+ 'url': i['href']
}
+ helper.add_item(i['title'], params)
+ helper.eod()
- if content == 'episode':
- title = product['content']['series']['episodeTitle']
- playable = True
- watched = True
- set_content = 'episodes'
-
- elif content == 'sport':
- now = datetime.now()
- date_today = now.date()
- product_name = unicode(product['content']['title'])
-
- if date_today == product['event_date'].date():
- start_time = '%s %s' % (language(30027), product['event_date'].strftime('%H:%M'))
- else:
- start_time = product['event_date'].strftime('%Y-%m-%d %H:%M')
-
- title = '[B]%s:[/B] %s' % (coloring(start_time, product['event_status']), product_name)
-
- if product['event_status'] == 'upcoming':
- parameters = {
- 'action': 'dialog',
- 'dialog_type': 'ok',
- 'heading': language(30017),
- 'message': '%s [B]%s[/B].' % (language(30016), start_time)
- }
- playable = False
- else:
- playable = True
-
- watched = False
- set_content = 'movies'
-
- elif content == 'movie':
- movie_name = product['content']['title'].encode('utf-8')
- movie_year = str(product['content']['production']['year'])
- title = '%s (%s)' % (movie_name, movie_year)
-
- if product['system']['availability']['planInfo']['isRental']:
- title = title + ' *' # mark rental products with an asterisk
-
- playable = True
- watched = True
- set_content = 'movies'
-
- elif content == 'series':
- title = product['content']['series']['title'].encode('utf-8')
- season_url = product['_links']['viaplay:page']['href']
- parameters = {
- 'action': 'list_seasons',
- 'url': season_url
+
+def sports_page(url):
+ collections = helper.vp.get_collections(url)
+ schedule_added = False
+
+ for i in collections:
+ if 'viaplay:seeTableau' in i['_links'] and not schedule_added:
+ params = {
+ 'action': 'sports_schedule_page',
+ 'url': i['_links']['viaplay:seeTableau']['href']
}
- playable = False
- watched = True
- set_content = 'tvshows'
+ helper.add_item(i['_links']['viaplay:seeTableau']['title'], params)
+ schedule_added = True
- items = add_item(title, parameters, items=items, playable=playable, watched=watched, set_content=set_content,
- set_info=return_info(product, content), set_art=return_art(product, content))
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- list_next_page(data)
- xbmcplugin.endOfDirectory(_handle)
+ if i['totalProductCount'] < 1:
+ continue # hide empty collections
+ params = {
+ 'action': 'list_products',
+ 'url': i['_links']['self']['href']
+ }
+ helper.add_item(i['title'], params)
+ helper.eod()
-def list_seasons(url):
- """List all series seasons."""
- seasons = vp.get_seasons(url)
- if len(seasons) == 1:
- # list products if there's only one season
- season_url = seasons[0]['_links']['self']['href']
- list_products(season_url)
- else:
- items = []
- for season in seasons:
- season_url = season['_links']['self']['href']
- title = '%s %s' % (language(30014), season['title'])
- parameters = {
- 'action': 'list_products',
- 'url': season_url
- }
+def sports_schedule_page(url):
+ dates = helper.vp.make_request(url=url, method='get')['_links']['viaplay:days']
- items = add_item(title, parameters, items=items)
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- xbmcplugin.endOfDirectory(_handle)
-
-
-def return_info(product, content):
- """Return the product information in a xbmcgui.setInfo friendly dict.
- Supported content types: episode, series, movie, sport"""
- cast = []
- mediatype = None
- title = None
- tvshowtitle = None
- season = None
- episode = None
- plot = None
- director = None
- try:
- duration = int(product['content']['duration']['milliseconds']) / 1000
- except KeyError:
- duration = None
- try:
- imdb_code = product['content']['imdb']['id']
- except KeyError:
- imdb_code = None
- try:
- rating = float(product['content']['imdb']['rating'])
- except KeyError:
- rating = None
- try:
- votes = str(product['content']['imdb']['votes'])
- except KeyError:
- votes = None
- try:
- year = int(product['content']['production']['year'])
- except KeyError:
- year = None
- try:
- genres = []
- for genre in product['_links']['viaplay:genres']:
- genres.append(genre['title'])
- genre = ', '.join(genres)
- except KeyError:
- genre = None
- try:
- mpaa = product['content']['parentalRating']
- except KeyError:
- mpaa = None
-
- if content == 'episode':
- mediatype = 'episode'
- title = product['content']['series']['episodeTitle'].encode('utf-8')
- tvshowtitle = product['content']['series']['title'].encode('utf-8')
- season = int(product['content']['series']['season']['seasonNumber'])
- episode = int(product['content']['series']['episodeNumber'])
- plot = product['content']['synopsis'].encode('utf-8')
-
- elif content == 'series':
- mediatype = 'tvshow'
- title = product['content']['series']['title'].encode('utf-8')
- tvshowtitle = product['content']['series']['title'].encode('utf-8')
- try:
- plot = product['content']['series']['synopsis'].encode('utf-8')
- except KeyError:
- plot = product['content']['synopsis'].encode('utf-8') # needed for alphabetical listing
-
- elif content == 'movie':
- mediatype = 'movie'
- title = product['content']['title'].encode('utf-8')
- plot = product['content']['synopsis'].encode('utf-8')
- try:
- for actor in product['content']['people']['actors']:
- cast.append(actor)
- except KeyError:
- pass
- try:
- directors = []
- for director in product['content']['people']['directors']:
- directors.append(director)
- director = ', '.join(directors)
- except KeyError:
- pass
-
- elif content == 'sport':
- mediatype = 'video'
- title = product['content']['title'].encode('utf-8')
- plot = product['content']['synopsis'].encode('utf-8')
-
- info = {
- 'mediatype': mediatype,
- 'title': title,
- 'tvshowtitle': tvshowtitle,
- 'season': season,
- 'episode': episode,
- 'year': year,
- 'plot': plot,
- 'duration': duration,
- 'code': imdb_code,
- 'rating': rating,
- 'votes': votes,
- 'genre': genre,
- 'director': director,
- 'mpaa': mpaa,
- 'cast': cast
+ for date in dates:
+ params = {
+ 'action': 'list_products',
+ 'url': date['href']
+ }
+ helper.add_item(date['date'], params)
+ helper.eod()
+
+
+def channels_page(url):
+ channels_dict = helper.vp.get_channels(url)
+
+ for channel in channels_dict['channels']:
+ params = {
+ 'action': 'list_products',
+ 'url': channel['_links']['self']['href']
+ }
+ art = {
+ 'thumb': channel['content']['images']['fallback']['template'].split('{')[0],
+ 'fanart': channel['content']['images']['fallback']['template'].split('{')[0]
+ }
+
+ for program in channel['_embedded']['viaplay:products']: # get current live program
+ if helper.vp.get_event_status(program) == 'live':
+ if 'content' in program:
+ current_program_title = coloring(program['content']['title'].encode('utf-8'), 'live')
+ else: # no broadcast
+ current_program_title = coloring(helper.language(30049).encode('utf-8'), 'no_broadcast')
+ break
+
+ list_title = '[B]{0}[/B]: {1}'.format(channel['content']['title'], current_program_title)
+
+ helper.add_item(list_title, params, art=art)
+
+ if channels_dict['next_page']:
+ list_next_page(channels_dict['next_page'], 'tve')
+ helper.eod()
+
+
+def categories_item(url):
+ title = helper.language(30041)
+ params = {
+ 'action': 'categories_page',
+ 'url': url
}
+ helper.add_item(title, params)
- return info
+def list_next_page(url, action):
+ title = helper.language(30018)
+ params = {
+ 'action': action,
+ 'url': url
+ }
+ helper.add_item(title, params)
-def return_art(product, content):
- """Return the available art in a xbmcgui.setArt friendly dict."""
- try:
- boxart = product['content']['images']['boxart']['url'].split('.jpg')[0] + '.jpg'
- except KeyError:
- boxart = None
- try:
- hero169 = product['content']['images']['hero169']['template'].split('.jpg')[0] + '.jpg'
- except KeyError:
- hero169 = None
- try:
- coverart23 = product['content']['images']['coverart23']['template'].split('.jpg')[0] + '.jpg'
- except KeyError:
- coverart23 = None
- try:
- coverart169 = product['content']['images']['coverart23']['template'].split('.jpg')[0] + '.jpg'
- except KeyError:
- coverart169 = None
- try:
- landscape = product['content']['images']['landscape']['url'].split('.jpg')[0] + '.jpg'
- except KeyError:
- landscape = None
- if content == 'episode' or content == 'sport':
- thumbnail = landscape
- else:
- thumbnail = boxart
- fanart = hero169
- banner = landscape
- cover = coverart23
- poster = boxart
+def list_products(url, filter_event=False, search_query=None):
+ if filter_event:
+ filter_event = filter_event.split(', ')
- art = {
- 'thumb': thumbnail,
- 'fanart': fanart,
- 'banner': banner,
- 'cover': cover,
- 'poster': poster
+ products_dict = helper.vp.get_products(url, filter_event=filter_event, search_query=search_query)
+ for product in products_dict['products']:
+ if product['type'] == 'series':
+ add_series(product)
+ elif product['type'] == 'episode':
+ add_episode(product)
+ elif product['type'] == 'movie':
+ add_movie(product)
+ elif product['type'] == 'sport':
+ add_sports_event(product)
+ elif product['type'] == 'tvEvent':
+ add_tv_event(product)
+ else:
+ helper.log('product type: {0} is not (yet) supported.'.format(product['type']))
+ return False
+
+ if products_dict['next_page']:
+ list_next_page(products_dict['next_page'], 'list_products')
+ helper.eod()
+
+
+def add_movie(movie):
+ params = {}
+ if movie['system'].get('guid'):
+ params['guid'] = movie['system']['guid']
+ else:
+ params['url'] = movie['_links']['self']['href']
+ params['action'] = 'play'
+
+ details = movie['content']
+
+ movie_info = {
+ 'mediatype': 'movie',
+ 'title': details['title'],
+ 'plot': details.get('synopsis'),
+ 'genre': ', '.join([x['title'] for x in movie['_links']['viaplay:genres']]),
+ 'year': details['production'].get('year'),
+ 'duration': int(details['duration'].get('milliseconds')) // 1000 if 'duration' in details else None,
+ 'cast': details['people'].get('actors', []) if 'people' in details else [],
+ 'director': ', '.join(details['people'].get('directors', [])) if 'people' in details else [],
+ 'mpaa': details.get('parentalRating'),
+ 'rating': float(details['imdb'].get('rating')) if 'imdb' in details else None,
+ 'votes': str(details['imdb'].get('votes')) if 'imdb' in details else None,
+ 'code': details['imdb'].get('id') if 'imdb' in details else None
}
- return art
+ helper.add_item(movie_info['title'], params=params, info=movie_info, art=add_art(details['images'], 'movie'),
+ content='movies', playable=True)
-def list_search(data):
- title = data['_links']['viaplay:search']['title']
- parameters = {
- 'action': 'search',
- 'url': data['_links']['viaplay:search']['href']
+def add_series(show):
+ params = {
+ 'action': 'seasons_page',
+ 'url': show['_links']['viaplay:page']['href']
}
- add_item(title, parameters)
+ details = show['content']
+
+ series_info = {
+ 'mediatype': 'tvshow',
+ 'title': details['series']['title'],
+ 'tvshowtitle': details['series']['title'],
+ 'plot': details['synopsis'] if details.get('synopsis') else details['series'].get('synopsis'),
+ 'genre': ', '.join([x['title'] for x in show['_links']['viaplay:genres']]),
+ 'year': details['production'].get('year') if 'production' in details else None,
+ 'cast': details['people'].get('actors', []) if 'people' in details else [],
+ 'director': ', '.join(details['people'].get('directors', [])) if 'people' in details else None,
+ 'mpaa': details.get('parentalRating'),
+ 'rating': float(details['imdb'].get('rating')) if 'imdb' in details else None,
+ 'votes': str(details['imdb'].get('votes')) if 'imdb' in details else None,
+ 'code': details['imdb'].get('id') if 'imdb' in details else None,
+ 'season': int(details['series']['seasons']) if details['series'].get('seasons') else None
+ }
+ helper.add_item(series_info['title'], params=params, folder=True, info=series_info,
+ art=add_art(details['images'], 'series'), content='tvshows')
-def get_user_input(heading):
- keyboard = xbmc.Keyboard('', heading)
- keyboard.doModal()
- if keyboard.isConfirmed():
- user_input = keyboard.getText()
- addon_log('User input string: %s' % user_input)
- else:
- user_input = None
- if user_input and len(user_input) > 0:
- return user_input
- else:
- return None
+def add_episode(episode):
+ params = {
+ 'action': 'play',
+ 'guid': episode['system']['guid']
+ }
+
+ details = episode['content']
+
+ episode_info = {
+ 'mediatype': 'episode',
+ 'title': details.get('title'),
+ 'list_title': details['series']['episodeTitle'] if details['series'].get('episodeTitle') else details.get(
+ 'title'),
+ 'tvshowtitle': details['series'].get('title'),
+ 'plot': details['synopsis'] if details.get('synopsis') else details['series'].get('synopsis'),
+ 'duration': details['duration']['milliseconds'] // 1000 if 'duration' in details else None,
+ 'genre': ', '.join([x['title'] for x in episode['_links']['viaplay:genres']]),
+ 'year': details['production'].get('year') if 'production' in details else None,
+ 'cast': details['people'].get('actors', []) if 'people' in details else [],
+ 'director': ', '.join(details['people'].get('directors', [])) if 'people' in details else None,
+ 'mpaa': details.get('parentalRating'),
+ 'rating': float(details['imdb'].get('rating')) if 'imdb' in details else None,
+ 'votes': str(details['imdb'].get('votes')) if 'imdb' in details else None,
+ 'code': details['imdb'].get('id') if 'imdb' in details else None,
+ 'season': int(details['series']['season'].get('seasonNumber')),
+ 'episode': int(details['series'].get('episodeNumber'))
+ }
+
+ helper.add_item(episode_info['list_title'], params=params, info=episode_info,
+ art=add_art(details['images'], 'episode'), content='episodes', playable=True)
-def get_numeric_input(heading):
- dialog = xbmcgui.Dialog()
- numeric_input = dialog.numeric(0, heading)
+def add_sports_event(event):
+ now = datetime.now()
+ date_today = now.date()
+ event_date = helper.vp.parse_datetime(event['epg']['start'], localize=True)
+ event_status = helper.vp.get_event_status(event)
- if len(numeric_input) > 0:
- return str(numeric_input)
+ if date_today == event_date.date():
+ start_time = '{0} {1}'.format(helper.language(30027), event_date.strftime('%H:%M'))
else:
- return None
+ start_time = event_date.strftime('%Y-%m-%d %H:%M')
+
+ if event_status == 'upcoming':
+ params = {
+ 'action': 'dialog',
+ 'dialog_type': 'ok',
+ 'heading': helper.language(30017),
+ 'message': helper.language(30016).format(start_time)
+ }
+ playable = False
+ else:
+ params = {
+ 'action': 'play',
+ 'guid': event['system']['guid']
+ }
+ playable = True
+
+ details = event['content']
+
+ event_info = {
+ 'mediatype': 'video',
+ 'title': details.get('title'),
+ 'plot': details['synopsis'],
+ 'year': int(details['production'].get('year')),
+ 'genre': details['format'].get('title'),
+ 'list_title': '[B]{0}:[/B] {1}'.format(coloring(start_time, event_status),
+ details.get('title').encode('utf-8'))
+ }
+ helper.add_item(event_info['list_title'], params=params, playable=playable, info=event_info,
+ art=add_art(details['images'], 'sport'), content='episodes')
-def search(url):
- query = get_user_input(language(30015))
- if query:
- url = '%s?query=%s' % (url, urllib.quote(query))
- list_products(url)
+def add_tv_event(event):
+ now = datetime.now()
+ date_today = now.date()
+ start_time_obj = helper.vp.parse_datetime(event['epg']['startTime'], localize=True)
+ event_status = helper.vp.get_event_status(event)
-def play_video(input, streamtype, content, pincode=None):
- if streamtype == 'url':
- url = input
- guid = vp.get_products(input=url, method='url')['system']['guid']
+ if date_today == start_time_obj.date():
+ start_time = '{0} {1}'.format(helper.language(30027), start_time_obj.strftime('%H:%M'))
+ else:
+ start_time = start_time_obj.strftime('%Y-%m-%d %H:%M')
+
+ if event_status == 'upcoming':
+ params = {
+ 'action': 'dialog',
+ 'dialog_type': 'ok',
+ 'heading': helper.language(30017),
+ 'message': helper.language(30016).format(start_time)
+ }
+ playable = False
else:
- guid = input
+ params = {
+ 'action': 'play',
+ 'guid': event['system']['guid']
+ }
+ playable = True
+
+ details = event['content']
+ event_info = {
+ 'mediatype': 'video',
+ 'title': details.get('title'),
+ 'plot': details.get('synopsis'),
+ 'year': details['production'].get('year'),
+ 'list_title': '[B]{0}:[/B] {1}'.format(coloring(start_time, event_status),
+ details.get('title').encode('utf-8'))
+ }
+ art = {
+ 'thumb': event['content']['images']['landscape']['template'].split('{')[0] if 'landscape' in details['images'] else None,
+ 'fanart': event['content']['images']['landscape']['template'].split('{')[0] if 'landscape' in details['images'] else None
+ }
- try:
- video_urls = vp.get_video_urls(guid, pincode=pincode)
- if video_urls:
- playitem = xbmcgui.ListItem(path=video_urls['manifest_url'])
- playitem.setProperty('IsPlayable', 'true')
- if addon.getSetting('subtitles') == 'true':
- playitem.setSubtitles(vp.download_subtitles(video_urls['subtitle_urls']))
- xbmcplugin.setResolvedUrl(_handle, True, listitem=playitem)
- else:
- dialog(dialog_type='ok', heading=language(30005), message=language(30038))
-
- except vp.AuthFailure as error:
- if error.value == 'ParentalGuidancePinChallengeNeededError':
- if pincode:
- dialog(dialog_type='ok', heading=language(30033), message=language(30034))
- else:
- pincode = get_numeric_input(language(30032))
- if pincode:
- play_video(input, streamtype, content, pincode)
- else:
- show_auth_error(error.value)
- except vp.LoginFailure:
- dialog(dialog_type='ok', heading=language(30005), message=language(30006))
+ helper.add_item(event_info['list_title'], params=params, playable=playable, info=event_info, art=art, content='episodes')
-def sports_menu(url):
- items = []
- event_date = ['today', 'upcoming', 'archive']
- for date in event_date:
- if date == 'today':
- title = language(30027)
- elif date == 'upcoming':
- title = language(30028)
- else:
- title = language(30029)
- if date == 'today':
- parameters = {
- 'action': 'list_sports_today',
- 'url': url
- }
- else:
+def seasons_page(url):
+ """List all series seasons."""
+ seasons = helper.vp.get_seasons(url)
+ if len(seasons) == 1: # list products if there's only one season
+ list_products(seasons[0]['_links']['self']['href'])
+ else:
+ for season in seasons:
+ title = helper.language(30014).format(season['title'])
parameters = {
- 'action': 'list_sports_dates',
- 'url': url,
- 'event_date': date
+ 'action': 'list_products',
+ 'url': season['_links']['self']['href']
}
- items = add_item(title, parameters, items=items)
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- xbmcplugin.endOfDirectory(_handle)
+ helper.add_item(title, parameters)
+ helper.eod()
-def list_sports_today(url):
- items = []
- event_status = [language(30037), language(30031)]
- for status in event_status:
- if status == language(30037):
- filter = 'live, upcoming'
- else:
- filter = 'archive'
- parameters = {
- 'action': 'list_products_sports_today',
- 'url': url,
- 'filter_sports_event': filter
- }
+def add_art(images, content_type):
+ artwork = {}
- items = add_item(status, parameters, items=items)
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- xbmcplugin.endOfDirectory(_handle)
+ for i in images:
+ image_url = images[i]['template'].split('{')[0] # get rid of template
+ if i == 'landscape':
+ if content_type == 'episode' or 'sport':
+ artwork['thumb'] = image_url
+ artwork['banner'] = image_url
+ elif i == 'hero169':
+ artwork['fanart'] = image_url
+ elif i == 'coverart23':
+ artwork['cover'] = image_url
+ elif i == 'boxart':
+ if content_type != 'episode' or 'sport':
+ artwork['thumb'] = image_url
+ artwork['poster'] = image_url
-def list_sports_dates(url, event_date):
- items = []
- dates = vp.get_sports_dates(url, event_date)
- for date in dates:
- title = date['date']
- parameters = {
- 'action': 'list_products',
- 'url': date['href']
- }
-
- items = add_item(title, parameters, items=items)
- xbmcplugin.addDirectoryItems(_handle, items, len(items))
- xbmcplugin.endOfDirectory(_handle)
-
-
-def dialog(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
+ return artwork
-def add_item(title, parameters, items=False, folder=True, playable=False, set_info=False, set_art=False,
- watched=False, set_content=False):
- listitem = xbmcgui.ListItem(label=title)
- if playable:
- listitem.setProperty('IsPlayable', 'true')
- folder = False
- if set_art:
- listitem.setArt(set_art)
- else:
- listitem.setArt({'icon': addon.getAddonInfo('icon')})
- listitem.setArt({'fanart': addon.getAddonInfo('fanart')})
- if set_info:
- listitem.setInfo('video', set_info)
- if not watched:
- listitem.addStreamInfo('video', {'duration': 0})
- if set_content:
- xbmcplugin.setContent(_handle, set_content)
-
- recursive_url = _url + '?' + urllib.urlencode(parameters)
-
- if items is False:
- xbmcplugin.addDirectoryItem(_handle, recursive_url, listitem, folder)
- else:
- items.append((recursive_url, listitem, folder))
- return items
+def search(url):
+ query = helper.get_user_input(helper.language(30015))
+ if query:
+ list_products(url, search_query=query)
def coloring(text, meaning):
@@ -608,54 +422,61 @@ def coloring(text, meaning):
color = 'FFF16C00'
elif meaning == 'archive':
color = 'FFFF0EE0'
+ elif meaning == 'no_broadcast':
+ color = 'FFFF3333'
colored_text = '[COLOR=%s]%s[/COLOR]' % (color, text)
return colored_text
-def show_auth_error(error):
+def show_error(error):
if error == 'UserNotAuthorizedForContentError':
- message = language(30020)
+ message = helper.language(30020)
elif error == 'PurchaseConfirmationRequiredError':
- message = language(30021)
+ message = helper.language(30021)
elif error == 'UserNotAuthorizedRegionBlockedError':
- message = language(30022)
+ message = helper.language(30022)
+ elif error == 'ConcurrentStreamsLimitReachedError':
+ message = helper.language(30050)
else:
message = error
- dialog(dialog_type='ok', heading=language(30017), message=message)
+ helper.dialog(dialog_type='ok', heading=helper.language(30017), message=message)
def router(paramstring):
"""Router function that calls other functions depending on the provided paramstring."""
params = dict(urlparse.parse_qsl(paramstring))
- if params:
- if params['action'] == 'list_categories':
- list_categories(params['url'], params['category_name'])
- elif params['action'] == 'sports_menu':
- sports_menu(params['url'])
- elif params['action'] == 'list_seasons':
- list_seasons(params['url'])
- elif params['action'] == 'list_products':
+ vod_pages = ['series', 'movie', 'kids', 'rental']
+ products_pages = ['viaplay:starred', 'viaplay:watched', 'viaplay:purchased']
+
+ if 'action' in params:
+ if params['action'] in vod_pages:
+ vod_page(params['url'])
+ elif params['action'] in products_pages:
list_products(params['url'])
- elif params['action'] == 'list_sports_today':
- list_sports_today(params['url'])
- elif params['action'] == 'list_products_sports_today':
- list_products(params['url'], params['filter_sports_event'])
- elif params['action'] == 'play_video':
- play_video(params['playid'], params['streamtype'], params['content'])
- elif params['action'] == 'list_sortings':
- list_sortings(params['url'])
- elif params['action'] == 'list_alphabetical_letters':
- list_alphabetical_letters(params['url'])
- elif params['action'] == 'search':
+ elif params['action'] == 'sport':
+ sports_page(params['url'])
+ elif params['action'] == 'tve':
+ channels_page(params['url'])
+ elif params['action'] == 'categories_page':
+ categories_page(params['url'])
+ elif params['action'] == 'sortings_page':
+ sortings_page(params['url'])
+ if params['action'] == 'viaplay:root':
+ start_page(params['url'])
+ elif params['action'] == 'viaplay:search':
search(params['url'])
- elif params['action'] == 'list_sports_dates':
- list_sports_dates(params['url'], params['event_date'])
+ elif params['action'] == 'viaplay:logout':
+ helper.log_out()
+ elif params['action'] == 'sports_schedule_page':
+ sports_schedule_page(params['url'])
+ elif params['action'] == 'play':
+ helper.play(guid=params.get('guid'), url=params.get('url'))
+ elif params['action'] == 'seasons_page':
+ seasons_page(params['url'])
+ elif params['action'] == 'list_products':
+ list_products(params['url'])
elif params['action'] == 'dialog':
- dialog(params['dialog_type'], params['heading'], params['message'])
+ helper.dialog(params['dialog_type'], params['heading'], params['message'])
else:
- main_menu()
-
-
-def run():
- router(sys.argv[2][1:]) # trim the leading '?' from the plugin call paramstring
+ root_page()
diff --git a/plugin.video.viaplay/addon.xml b/plugin.video.viaplay/addon.xml
index 706fa13..11d1a79 100644
--- a/plugin.video.viaplay/addon.xml
+++ b/plugin.video.viaplay/addon.xml
@@ -1,20 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon id="plugin.video.viaplay"
- version="1.0.8"
+ version="2.0.1"
name="Viaplay"
provider-name="emilsvennesson">
<requires>
<import addon="xbmc.python" version="2.25.0"/>
<import addon="script.module.requests" version="2.9.1"/>
<import addon="script.module.iso8601" version="0.1.11"/>
+ <import addon="script.module.inputstreamhelper" version="0.2.2"/>
</requires>
<extension point="xbmc.python.pluginsource" library="default.py">
<provides>video</provides>
</extension>
<extension point="xbmc.addon.metadata">
- <description lang="en">Watch content from Viaplay.</description>
- <description lang="sv">Titta på innehåll från Viaplay.</description>
- <news>2017.04.10 v1.0.8[CR]+ Fix audio on newer Viaplay content[CR]+ Fix icon/fanart[CR]+ Move addon code to addon.py</news>
+ <description lang="en_GB">Watch content from Viaplay.</description>
+ <description lang="sv_SE">Titta på innehåll från Viaplay.</description>
+ <news>2017.12.27 v2.0.1
+ + New dependency: script.module.inputstreamhelper
+ + Switch to registration code activation
+ + Finally replace HLS with MPEG-DASH (Widevine DRM is now required)
+ + Channels support (previously Viasat TV To Go)
+ + Implement dynamically acquired 'theme pages' in addition to browsing through categories
+ + Starred, Watched and Purchased pages now work
+ + Open add-on settings on first run
+ + Major rewrite and cleanup of the code
+ + Big improvements all-around and a massively enhanced experience
+ </news>
<platform>all</platform>
<language>sv dk no fi en</language>
<license>GNU GENERAL PUBLIC LICENSE. Version 3, 29 June 2007</license>
diff --git a/plugin.video.viaplay/changelog.txt b/plugin.video.viaplay/changelog.txt
index a1b9b82..6177449 100644
--- a/plugin.video.viaplay/changelog.txt
+++ b/plugin.video.viaplay/changelog.txt
@@ -1,4 +1,18 @@
-2017.04.10 v1.0.8
+2017.12.27 v2.0.1
++ Minor changes needed for approval in the official repository
+
+2017.12.27 v2.0.0
++ New dependency: script.module.inputstreamhelper
++ Switch to registration code activation
++ Finally replace HLS with MPEG-DASH (Widevine DRM is now required)
++ Channels support (previously Viasat TV To Go)
++ Implement dynamically acquired 'theme pages' in addition to browsing through categories
++ Starred, Watched and Purchased pages now work
++ Open add-on settings on first run
++ Major rewrite and cleanup of the code
++ Big improvements all-around and a massively enhanced experience
+
+2017.04.10 v1.0.8
+ Fix audio on newer Viaplay content
+ Fix icon/fanart
+ Move addon code to addon.py
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>