summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoremilsvennesson <emilsvennesson@users.noreply.github.com>2017-12-29 20:06:24 +0100
committerMartijn Kaijser <martijn@xbmc.org>2018-01-13 10:21:04 +0100
commit27854c6fbe810992f76591a99ba7e6c3f958b060 (patch)
treed5a4a6bd8903ac9b6dc5dffa22dc5558c76eecf3
parent54eab88b5f830047899d3a6b3349c3db84ad0cd1 (diff)
[plugin.video.cmore] 0.2.0
-rw-r--r--plugin.video.cmore/LICENSE.txt21
-rw-r--r--plugin.video.cmore/README.md22
-rw-r--r--plugin.video.cmore/__init__.py1
-rw-r--r--plugin.video.cmore/addon.py399
-rw-r--r--plugin.video.cmore/addon.xml31
-rw-r--r--plugin.video.cmore/changelog.txt5
-rw-r--r--plugin.video.cmore/default.py5
-rw-r--r--plugin.video.cmore/resources/__init__.py1
-rw-r--r--plugin.video.cmore/resources/fanart.jpgbin0 -> 56624 bytes
-rw-r--r--plugin.video.cmore/resources/icon.pngbin0 -> 52285 bytes
-rw-r--r--plugin.video.cmore/resources/language/resource.language.en_gb/strings.po146
-rw-r--r--plugin.video.cmore/resources/lib/Widevine.py29
-rw-r--r--plugin.video.cmore/resources/lib/WidevineHTTPRequestHandler.py33
-rw-r--r--plugin.video.cmore/resources/lib/__init__.py1
-rw-r--r--plugin.video.cmore/resources/lib/cmore.py328
-rw-r--r--plugin.video.cmore/resources/lib/kodihelper.py237
-rw-r--r--plugin.video.cmore/resources/settings.xml15
-rw-r--r--plugin.video.cmore/service.py52
18 files changed, 1326 insertions, 0 deletions
diff --git a/plugin.video.cmore/LICENSE.txt b/plugin.video.cmore/LICENSE.txt
new file mode 100644
index 0000000..7dbae24
--- /dev/null
+++ b/plugin.video.cmore/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Nils Emil Svensson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugin.video.cmore/README.md b/plugin.video.cmore/README.md
new file mode 100644
index 0000000..b1b8f5e
--- /dev/null
+++ b/plugin.video.cmore/README.md
@@ -0,0 +1,22 @@
+# C More for Kodi #
+This is a Kodi add-on that allows you to stream content from C More in Kodi.
+
+## Disclaimer ##
+This add-on is unoffical and is not endorsed or supported by C More Entertainment in any way. Not all features may work or has been thoroughly tested.
+
+## Dependencies: ##
+ * script.module.requests >= 2.9.1 (http://mirrors.kodi.tv/addons/krypton/script.module.requests/)
+ * script.module.inputstreamhelper >= 0.2.2 (http://mirrors.kodi.tv/addons/krypton/script.module.inputstreamhelper/)
+
+This add-on requires Kodi 17.4 or higher with InputStream Adaptive installed. Kodi 18 is required for Android based devices and for subtitles support.
+
+## DRM protected streams ##
+Most of C More'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.
+
+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 by 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-cmore/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.
+
+## License ##
+This add-on is licensed under the **The MIT License**. Please see the [LICENSE.txt](LICENSE.txt) file for details.
diff --git a/plugin.video.cmore/__init__.py b/plugin.video.cmore/__init__.py
new file mode 100644
index 0000000..b53149b
--- /dev/null
+++ b/plugin.video.cmore/__init__.py
@@ -0,0 +1 @@
+# dummy file to init the directory
diff --git a/plugin.video.cmore/addon.py b/plugin.video.cmore/addon.py
new file mode 100644
index 0000000..a86c0fb
--- /dev/null
+++ b/plugin.video.cmore/addon.py
@@ -0,0 +1,399 @@
+import sys
+import urlparse
+import json
+
+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.c.CMoreError as error:
+ if error.value == 'SESSION_NOT_AUTHENTICATED' or error.value == 'User is not authenticated':
+ helper.login_process()
+ router(sys.argv[2][1:])
+ else:
+ helper.dialog('ok', helper.language(30028), error.value)
+
+
+def list_root_pages():
+ for page in helper.c.root_pages[helper.c.locale]:
+ if page == 'start':
+ title = helper.language(30020)
+ elif page == 'movies':
+ title = helper.language(30021)
+ elif page == 'series':
+ title = helper.language(30022)
+ elif page == 'sports':
+ title = helper.language(30023)
+ elif page == 'tv':
+ title = helper.language(30024)
+ elif page == 'programs':
+ title = helper.language(30025)
+ elif page == 'kids':
+ title = helper.language(30026)
+ else:
+ title = page
+
+ params = {
+ 'action': 'list_page',
+ 'page': page,
+ 'namespace': 'page',
+ 'root_page': 'true'
+ }
+
+ helper.add_item(title, params=params)
+ helper.add_item(helper.language(30030), params={'action': 'search'}) # search
+ helper.eod()
+
+
+def list_page(page=None, namespace=None, root_page=False, page_data=None, search_query=None):
+ if page:
+ page_dict = helper.c.parse_page(page, namespace, helper.get_as_bool(root_page))
+ elif page_data:
+ page_dict = json.loads(page_data)
+ elif search_query:
+ page_dict = helper.c.get_search_data(search_query)
+
+ if not page_dict:
+ if search_query:
+ helper.dialog('ok', helper.language(30017), '{0} \'{1}\'.'.format(helper.language(30031), search_query))
+ helper.log('No page data found.')
+ return False
+
+ for i in page_dict:
+ page = i.get('id')
+ # search queries doesn't include the videoId in response
+ if i.get('videoId') or search_query:
+ if i.get('type') == 'movie':
+ list_movie(i)
+ elif i.get('type') == 'series':
+ list_show(i)
+ elif i.get('type') == 'live_event':
+ list_live_event(i)
+ elif 'displayableDate' in i:
+ list_event_date(i)
+ elif 'channel' in i:
+ list_channel(i)
+ elif 'namespace' in i: # theme pages
+ list_genres(i, page)
+ elif 'page_data' in i: # parsed containers
+ list_containers(i)
+ helper.eod()
+
+
+def list_genres(i, page):
+ params = {
+ 'action': 'list_page',
+ 'namespace': i['namespace'],
+ 'page': page,
+ 'root_page': 'false'
+ }
+
+ helper.add_item(i['headline'], params)
+
+
+def list_channel(i):
+ params = {
+ 'action': 'play',
+ 'video_id': i['channel']['videoId']
+ }
+
+ program_info = {
+ 'mediatype': 'video',
+ 'title': i['programs'][0]['title'],
+ 'genre': i['programs'][0].get('mainCategory'),
+ 'plot': i['programs'][0].get('description')
+ }
+
+ channel_art = {
+ 'fanart': helper.c.get_image_url(i['programs'][0].get('landscapeImage')),
+ 'thumb': helper.c.get_image_url(i['programs'][0].get('landscapeImage')),
+ 'cover': helper.c.get_image_url(i['programs'][0].get('landscapeImage')),
+ 'icon': helper.c.get_image_url(i['channel']['logoUrl'])
+ }
+
+ channel_colored = coloring(i['channel']['title'], 'live').encode('utf-8')
+ time_colored = coloring(i['programs'][0]['caption'], 'live').encode('utf-8')
+ program_title = i['programs'][0]['title'].encode('utf-8')
+ list_title = '[B]{0} {1}[/B]: {2}'.format(channel_colored, time_colored, program_title)
+
+ helper.add_item(list_title, params=params, info=program_info, art=channel_art, content='episodes', playable=True)
+
+
+def list_containers(i):
+ headline = i['attributes']['headline'].encode('utf-8')
+ if i['attributes'].get('subtitle'):
+ subtitle = i['attributes']['subtitle'].encode('utf-8')
+ title = '{0}: {1}'.format(subtitle, headline)
+ else:
+ title = headline
+
+ params = {
+ 'action': 'list_page_with_page_data',
+ 'page_data': json.dumps(i['page_data'])
+ }
+
+ helper.add_item(title, params)
+
+
+def list_event_date(date):
+ params = {
+ 'action': 'list_page_with_page_data',
+ 'page_data': json.dumps(date['events'])
+ }
+
+ helper.add_item(date['displayableDate'], params)
+
+
+def coloring(text, meaning):
+ """Return the text wrapped in appropriate color markup."""
+ if meaning == 'live':
+ color = 'FF03F12F'
+ elif meaning == 'archive':
+ color = 'FFFF0EE0'
+ elif meaning == 'upcoming':
+ color = 'FFF16C00'
+
+ colored_text = '[COLOR=%s]%s[/COLOR]' % (color, text)
+
+ return colored_text
+
+
+def list_live_event(event):
+ if event.get('commentators'):
+ if ',' in event.get('commentators'):
+ commentators = event.get('commentators').split(',')
+ else:
+ commentators = [event['commentators']]
+ else:
+ commentators = []
+
+ if helper.c.parse_datetime(event['startTime']) > helper.c.get_current_time():
+ event_status = 'upcoming'
+ params = {'action': 'noop'}
+ playable = False
+ else:
+ if 'liveEventEnd' in event: # if sports content is on-demand
+ event_status = 'archive'
+ else:
+ event_status = 'live'
+ params = {
+ 'action': 'play',
+ 'video_id': event['videoId']
+ }
+ playable = True
+
+ list_title = '[B]{0}:[/B] {1}'.format(coloring(event['displayableStartTime'].encode('utf-8'), event_status), event['title'].encode('utf-8'))
+
+ event_info = {
+ 'mediatype': 'video',
+ 'title': event.get('title'),
+ 'plot': event.get('description'),
+ 'plotoutline': event.get('caption'),
+ 'year': int(event['year']) if event.get('year') else None,
+ 'genre': ', '.join(event['subCategories']) if event.get('subCategories') else None,
+ 'cast': commentators
+ }
+
+ event_art = {
+ 'fanart': helper.c.get_image_url(event.get('landscapeImage')),
+ 'thumb': helper.c.get_image_url(event.get('landscapeImage')),
+ 'banner': helper.c.get_image_url(event.get('ultraforgeImage')),
+ 'cover': helper.c.get_image_url(event.get('landscapeImage')),
+ 'icon': helper.c.get_image_url(event.get('categoryIcon'))
+ }
+
+ helper.add_item(list_title, params=params, info=event_info, art=event_art, content='episodes', playable=playable)
+
+
+def list_movie(movie):
+ params = {
+ 'action': 'play',
+ 'video_id': movie['videoId']
+ }
+
+ movie_info = {
+ 'mediatype': 'movie',
+ 'title': movie.get('title'),
+ 'plot': movie.get('description') if movie.get('description') else movie.get('caption'),
+ 'cast': movie.get('actors', []),
+ 'genre': extract_genre_year(movie.get('caption'), 'genre'),
+ 'duration': movie.get('duration'),
+ 'year': int(extract_genre_year(movie.get('caption'), 'year'))
+ }
+
+ movie_art = {
+ 'fanart': helper.c.get_image_url(movie.get('landscapeImage')),
+ 'thumb': helper.c.get_image_url(movie.get('posterImage')),
+ 'banner': helper.c.get_image_url(movie.get('ultraforgeImage')),
+ 'cover': helper.c.get_image_url(movie.get('landscapeImage')),
+ 'poster': helper.c.get_image_url(movie.get('posterImage'))
+ }
+
+ helper.add_item(movie['title'], params=params, info=movie_info, art=movie_art, content='movies', playable=True)
+
+
+def extract_genre_year(caption, what_to_extract):
+ """Try to extract the genre or year from the caption."""
+ if what_to_extract == 'year':
+ list_index = 1
+ else:
+ list_index = 0
+
+ if caption:
+ try:
+ return caption.split(', ')[list_index]
+ except IndexError:
+ pass
+
+ return None
+
+
+def list_show(show):
+ params = {
+ 'action': 'list_episodes_or_seasons',
+ 'page_id': show['id']
+ }
+
+ show_info = {
+ 'mediatype': 'tvshow',
+ 'title': show.get('title'),
+ 'plot': show.get('description') if show.get('description') else show.get('caption'),
+ 'cast': show.get('actors', []),
+ 'genre': extract_genre_year(show.get('caption'), 'genre'),
+ 'duration': show.get('duration'),
+ 'year': extract_genre_year(show.get('caption'), 'year')
+ }
+
+ show_art = {
+ 'fanart': helper.c.get_image_url(show.get('landscapeImage')),
+ 'thumb': helper.c.get_image_url(show.get('posterImage')),
+ 'banner': helper.c.get_image_url(show.get('ultraforgeImage')),
+ 'cover': helper.c.get_image_url(show.get('landscapeImage')),
+ 'poster': helper.c.get_image_url(show.get('posterImage'))
+ }
+
+ helper.add_item(show['title'], params=params, info=show_info, art=show_art, content='tvshows')
+
+
+def list_episodes_or_seasons(page_id):
+ series = helper.c.get_content_details('series', page_id)
+ if len(series['availableSeasons']) > 1:
+ list_seasons(series)
+ else:
+ list_episodes(series_data=series)
+
+
+def list_seasons(series):
+ for season in series['availableSeasons']:
+ title = '{0} {1}'.format(helper.language(30029), str(season))
+ params = {
+ 'action': 'list_episodes',
+ 'page_id': series['id'],
+ 'season': str(season)
+ }
+ helper.add_item(title, params)
+ helper.eod()
+
+
+def list_episodes(page_id=None, season=None, series_data=None):
+ if page_id:
+ series = helper.c.get_content_details('series', page_id, season)
+ else:
+ series = series_data
+
+ for i in series['capiAssets']:
+ params = {
+ 'action': 'play',
+ 'video_id': i['videoId']
+ }
+
+ title = i.get('title').replace(': ', '').encode('utf-8')
+
+ episode_info = {
+ 'mediatype': 'episode',
+ 'title': title,
+ 'tvshowtitle': i['seriesTitle'] if i.get('seriesTitle') else title,
+ 'plot': i.get('description'),
+ 'year': int(i['year']) if i.get('year') else None,
+ 'season': int(i['season']) if i.get('season') else None,
+ 'episode': int(i['episode']) if i.get('episode') else None,
+ 'genre': ', '.join(i['subCategories']) if i.get('subCategories') else None,
+ 'cast': i.get('actors', []),
+ 'duration': i.get('duration')
+ }
+
+ list_title = add_season_episode_to_title(title, episode_info['season'], episode_info['episode'])
+
+ episode_art = {
+ 'fanart': helper.c.get_image_url(i.get('landscapeImage')),
+ 'thumb': helper.c.get_image_url(i.get('landscapeImage')),
+ 'banner': helper.c.get_image_url(i.get('ultraforgeImage')),
+ 'cover': helper.c.get_image_url(i.get('landscapeImage')),
+ 'poster': helper.c.get_image_url(i.get('posterImage'))
+ }
+
+ helper.add_item(list_title, params=params, info=episode_info, art=episode_art, content='episodes', playable=True)
+ helper.eod()
+
+
+def add_season_episode_to_title(title, season, episode):
+ if season and episode:
+ if int(season) <= 9:
+ season_format = '0' + str(season)
+ else:
+ season_format = str(season)
+ if int(episode) <= 9:
+ episode_format = '0' + str(episode)
+ else:
+ episode_format = str(episode)
+
+ return '[B]S{0}E{1}[/B] {2}'.format(season_format, episode_format, title)
+ else:
+ helper.log('No season/episode information found.')
+ return title
+
+
+def search():
+ search_query = helper.get_user_input(helper.language(30030))
+ if search_query:
+ list_page(search_query=search_query)
+ else:
+ helper.log('No search query provided.')
+ return False
+
+
+def router(paramstring):
+ """Router function that calls other functions depending on the provided paramstring."""
+ params = dict(urlparse.parse_qsl(paramstring))
+ if 'setting' in params:
+ if params['setting'] == 'get_operator':
+ helper.get_operator()
+ elif params['setting'] == 'set_locale':
+ helper.set_locale()
+ elif params['setting'] == 'reset_credentials':
+ helper.reset_credentials()
+ elif 'action' in params:
+ if helper.check_for_prerequisites():
+ if params['action'] == 'noop':
+ pass
+ elif params['action'] == 'play':
+ helper.play_item(params['video_id'])
+ elif params['action'] == 'list_page':
+ list_page(params['page'], params['namespace'], params['root_page'])
+ elif params['action'] == 'list_page_with_page_data':
+ list_page(page_data=params['page_data'])
+ elif params['action'] == 'list_episodes_or_seasons':
+ list_episodes_or_seasons(params['page_id'])
+ elif params['action'] == 'list_episodes':
+ list_episodes(page_id=params['page_id'], season=params['season'])
+ elif params['action'] == 'search':
+ search()
+ else:
+ if helper.check_for_prerequisites():
+ list_root_pages()
diff --git a/plugin.video.cmore/addon.xml b/plugin.video.cmore/addon.xml
new file mode 100644
index 0000000..d170533
--- /dev/null
+++ b/plugin.video.cmore/addon.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon id="plugin.video.cmore"
+ version="0.2.0"
+ name="C More"
+ provider-name="emilsvennesson">
+ <requires>
+ <import addon="xbmc.python" version="2.25.0"/>
+ <import addon="script.module.inputstreamhelper" version="0.2.2"/>
+ <import addon="script.module.requests" version="2.9.1"/>
+ </requires>
+ <extension point="xbmc.python.pluginsource" library="default.py">
+ <provides>video</provides>
+ </extension>
+ <extension point="xbmc.service" library="service.py" start="login" />
+ <extension point="xbmc.addon.metadata">
+ <description lang="en_GB">Watch content from C More.</description>
+ <description lang="sv_SE">Titta på innehåll från C More.</description>
+ <news>2017.12.29 v0.2.0
+ + Initial stable release.
+ </news>
+ <platform>all</platform>
+ <license>MIT License</license>
+ <source>https://github.com/emilsvennesson/kodi-cmore</source>
+ <language>sv dk no en</language>
+ <disclaimer>This add-on is completely unofficial and is not endorsed by C More Entertainment in any way.</disclaimer>
+ <assets>
+ <icon>resources/icon.png</icon>
+ <fanart>resources/fanart.jpg</fanart>
+ </assets>
+ </extension>
+</addon>
diff --git a/plugin.video.cmore/changelog.txt b/plugin.video.cmore/changelog.txt
new file mode 100644
index 0000000..9fce1eb
--- /dev/null
+++ b/plugin.video.cmore/changelog.txt
@@ -0,0 +1,5 @@
+2017.12.29 v0.2.0
++ Initial stable release.
+
+2017.04.29 v0.1.0
++ Unreleased development version
diff --git a/plugin.video.cmore/default.py b/plugin.video.cmore/default.py
new file mode 100644
index 0000000..4014178
--- /dev/null
+++ b/plugin.video.cmore/default.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+import addon
+
+if __name__ == '__main__':
+ addon.run()
diff --git a/plugin.video.cmore/resources/__init__.py b/plugin.video.cmore/resources/__init__.py
new file mode 100644
index 0000000..b53149b
--- /dev/null
+++ b/plugin.video.cmore/resources/__init__.py
@@ -0,0 +1 @@
+# dummy file to init the directory
diff --git a/plugin.video.cmore/resources/fanart.jpg b/plugin.video.cmore/resources/fanart.jpg
new file mode 100644
index 0000000..129ee29
--- /dev/null
+++ b/plugin.video.cmore/resources/fanart.jpg
Binary files differ
diff --git a/plugin.video.cmore/resources/icon.png b/plugin.video.cmore/resources/icon.png
new file mode 100644
index 0000000..00e1162
--- /dev/null
+++ b/plugin.video.cmore/resources/icon.png
Binary files differ
diff --git a/plugin.video.cmore/resources/language/resource.language.en_gb/strings.po b/plugin.video.cmore/resources/language/resource.language.en_gb/strings.po
new file mode 100644
index 0000000..8ba1142
--- /dev/null
+++ b/plugin.video.cmore/resources/language/resource.language.en_gb/strings.po
@@ -0,0 +1,146 @@
+# C More language file
+msgid ""
+msgstr ""
+"Project-Id-Version: kodi-cmore\n"
+"Report-Msgid-Bugs-To: https://github.com/emilsvennesson/kodi-cmore\n"
+"POT-Creation-Date: 2017-04-19 21:43+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: English\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+msgctxt "#30000"
+msgid "Account"
+msgstr ""
+
+msgctxt "#30001"
+msgid "Country"
+msgstr ""
+
+msgctxt "#30002"
+msgid "Sweden"
+msgstr ""
+
+msgctxt "#30003"
+msgid "Denmark"
+msgstr ""
+
+msgctxt "#30004"
+msgid "Norway"
+msgstr ""
+
+msgctxt "#30005"
+msgid "Finland"
+msgstr ""
+
+msgctxt "#30006"
+msgid "Username"
+msgstr ""
+
+msgctxt "#30007"
+msgid "Password"
+msgstr ""
+
+msgctxt "#30008"
+msgid "Login with TV provider"
+msgstr ""
+
+msgctxt "#30009"
+msgid "C More account"
+msgstr ""
+
+msgctxt "#30010"
+msgid "Select TV provider"
+msgstr ""
+
+msgctxt "#30011"
+msgid "TV provider"
+msgstr ""
+
+msgctxt "#30012"
+msgid "Site"
+msgstr ""
+
+msgctxt "#30013"
+msgid "cmore.se"
+msgstr ""
+
+msgctxt "#30014"
+msgid "cmore.dk"
+msgstr ""
+
+msgctxt "#30015"
+msgid "cmore.no"
+msgstr ""
+
+msgctxt "#30016"
+msgid "cmore.fi"
+msgstr ""
+
+msgctxt "#30017"
+msgid "Information"
+msgstr ""
+
+msgctxt "#30018"
+msgid "Please enter your login credentials in the add-on settings."
+msgstr ""
+
+msgctxt "#30019"
+msgid "Reset TV provider"
+msgstr ""
+
+msgctxt "#30020"
+msgid "Start"
+msgstr ""
+
+msgctxt "#30021"
+msgid "Movies"
+msgstr ""
+
+msgctxt "#30022"
+msgid "Series"
+msgstr ""
+
+msgctxt "#30023"
+msgid "Sports"
+msgstr ""
+
+msgctxt "#30024"
+msgid "Channels"
+msgstr ""
+
+msgctxt "#30025"
+msgid "TV programs"
+msgstr ""
+
+msgctxt "#30026"
+msgid "Kids"
+msgstr ""
+
+msgctxt "#30027"
+msgid "Reset credentials"
+msgstr ""
+
+msgctxt "#30028"
+msgid "Error"
+msgstr ""
+
+msgctxt "#30029"
+msgid "Season"
+msgstr ""
+
+msgctxt "#30030"
+msgid "Search"
+msgstr ""
+
+msgctxt "#30031"
+msgid "No results found for"
+msgstr ""
+
+msgctxt "#30032"
+msgid "[B]Run the add-on to complete the setup process.[/B]"
+msgstr ""
diff --git a/plugin.video.cmore/resources/lib/Widevine.py b/plugin.video.cmore/resources/lib/Widevine.py
new file mode 100644
index 0000000..d974bb3
--- /dev/null
+++ b/plugin.video.cmore/resources/lib/Widevine.py
@@ -0,0 +1,29 @@
+import json
+import xml.etree.ElementTree as ET
+from kodihelper import KodiHelper
+
+helper = KodiHelper()
+
+
+class Widevine(object):
+ license_url = helper.c.config['settings']['drmProxy']
+
+ def get_kid(self, mpd_url):
+ """Parse the KID from the MPD manifest."""
+ mpd_data = helper.c.make_request(mpd_url, 'get')
+ mpd_root = ET.fromstring(mpd_data)
+
+ for i in mpd_root.iter('{urn:mpeg:dash:schema:mpd:2011}ContentProtection'):
+ if '{urn:mpeg:cenc:2013}default_KID' in i.attrib:
+ return i.attrib['{urn:mpeg:cenc:2013}default_KID']
+
+ def get_license(self, mpd_url, wv_challenge, token):
+ """Acquire the Widevine license from the license server and return it."""
+ post_data = {
+ 'drm_info': [x for x in bytearray(wv_challenge)], # convert challenge to a list of bytes
+ 'kid': self.get_kid(mpd_url),
+ 'token': token
+ }
+
+ wv_license = helper.c.make_request(self.license_url, 'post', payload=json.dumps(post_data))
+ return wv_license
diff --git a/plugin.video.cmore/resources/lib/WidevineHTTPRequestHandler.py b/plugin.video.cmore/resources/lib/WidevineHTTPRequestHandler.py
new file mode 100644
index 0000000..ade620d
--- /dev/null
+++ b/plugin.video.cmore/resources/lib/WidevineHTTPRequestHandler.py
@@ -0,0 +1,33 @@
+import BaseHTTPServer
+import urlparse
+import urllib
+
+from Widevine import Widevine
+
+wv = Widevine()
+
+
+class WidevineHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ def do_HEAD(self):
+ self.send_response(200)
+
+ def do_POST(self):
+ length = int(self.headers['content-length'])
+ wv_challenge = self.rfile.read(length)
+ query = dict(urlparse.parse_qsl(urlparse.urlsplit(self.path).query))
+ mpd_url = query['mpd_url']
+ token = query['license_url'].split('token=')[1]
+
+ try:
+ wv_license = wv.get_license(mpd_url, wv_challenge, token)
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(wv_license)
+ self.finish()
+ except Exception as ex:
+ self.send_response(400)
+ self.wfile.write(ex.value)
+
+ def log_message(self, format, *args):
+ """Disable the BaseHTTPServer log."""
+ return
diff --git a/plugin.video.cmore/resources/lib/__init__.py b/plugin.video.cmore/resources/lib/__init__.py
new file mode 100644
index 0000000..b53149b
--- /dev/null
+++ b/plugin.video.cmore/resources/lib/__init__.py
@@ -0,0 +1 @@
+# dummy file to init the directory
diff --git a/plugin.video.cmore/resources/lib/cmore.py b/plugin.video.cmore/resources/lib/cmore.py
new file mode 100644
index 0000000..4787383
--- /dev/null
+++ b/plugin.video.cmore/resources/lib/cmore.py
@@ -0,0 +1,328 @@
+# -*- coding: utf-8 -*-
+"""
+A Kodi-agnostic library for C More
+"""
+import os
+import json
+import codecs
+import time
+from datetime import datetime, timedelta
+
+import requests
+
+
+class CMore(object):
+ def __init__(self, settings_folder, locale, debug=False):
+ self.debug = debug
+ self.locale = locale
+ self.locale_suffix = self.locale.split('_')[1].lower()
+ self.http_session = requests.Session()
+ self.settings_folder = settings_folder
+ self.credentials_file = os.path.join(settings_folder, 'credentials')
+ self.base_url = 'https://cmore-mobile-bff.b17g.services'
+ self.config_path = os.path.join(self.settings_folder, 'configuration.json')
+ self.config_version = '3.6.1'
+ self.config = self.get_config()
+ self.client = 'cmore-android'
+ # hopefully, this can be acquired dynamically in the future
+ self.root_pages = {
+ 'sv_SE': ['start', 'movies', 'series', 'sports', 'tv', 'programs', 'kids'],
+ 'da_DK': ['start', 'movies', 'series', 'sports', 'tv', 'kids'],
+ 'nb_NO': ['start', 'movies', 'series', 'tv', 'kids']
+ }
+
+ class CMoreError(Exception):
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
+
+ def log(self, string):
+ """C More class log method."""
+ if self.debug:
+ try:
+ print('[C More]: %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('[C More]: %s' % string.replace(bom, ''))
+ except:
+ pass
+
+ def make_request(self, url, method, params=None, payload=None, headers=None):
+ """Make an HTTP request. Return the response."""
+ 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)
+
+ return self.parse_response(req.content)
+
+ def parse_response(self, response):
+ """Try to load JSON data into dict and raise potential API errors."""
+ try:
+ response = json.loads(response)
+ if 'error' in response:
+ if 'message' in response['error']:
+ raise self.CMoreError(response['error']['message'])
+ elif 'description' in response['error']:
+ raise self.CMoreError(response['error']['description'])
+ elif 'code' in response['error']:
+ raise self.CMoreError(response['error']['error'])
+
+ except ValueError: # when response is not in json
+ pass
+
+ return response
+
+ def get_config(self):
+ """Return the config in a dict. Re-download if the config version doesn't match self.config_version."""
+ try:
+ config = json.load(open(self.config_path))['data']
+ except IOError:
+ self.download_config()
+ config = json.load(open(self.config_path))['data']
+
+ config_version = int(str(config['settings']['currentAppVersion']).replace('.', ''))
+ version_to_use = int(str(self.config_version).replace('.', ''))
+ config_lang = config['bootstrap']['suggested_site']['locale']
+ if version_to_use > config_version or config_lang != self.locale:
+ self.download_config()
+ config = json.load(open(self.config_path))['data']
+
+ return config
+
+ def download_config(self):
+ """Download the C More app configuration."""
+ url = self.base_url + '/configuration'
+ params = {
+ 'device': 'android_tab',
+ 'locale': self.locale
+ }
+ config_data = self.make_request(url, 'get', params=params)
+ with open(self.config_path, 'w') as fh_config:
+ fh_config.write(json.dumps(config_data))
+
+ def save_credentials(self, credentials):
+ """Save credentials in JSON format."""
+ credentials_dict = json.loads(credentials)['data']
+ if self.get_credentials().get('remember_me'):
+ credentials_dict['remember_me'] = {}
+ credentials_dict['remember_me']['token'] = self.get_credentials()['remember_me']['token'] # resave token
+ with open(self.credentials_file, 'w') as fh_credentials:
+ fh_credentials.write(json.dumps(credentials_dict))
+
+ def reset_credentials(self):
+ """Overwrite credentials with empty JSON data."""
+ credentials = {}
+ with open(self.credentials_file, 'w') as fh_credentials:
+ fh_credentials.write(json.dumps(credentials))
+
+ def get_credentials(self):
+ """Get JSON credentials file from disk and load it into a dictionary."""
+ try:
+ with open(self.credentials_file, 'r') as fh_credentials:
+ credentials_dict = json.loads(fh_credentials.read())
+ return credentials_dict
+ except IOError:
+ self.reset_credentials()
+ with open(self.credentials_file, 'r') as fh_credentials:
+ return json.loads(fh_credentials.read())
+
+ def get_operators(self):
+ """Return a list of TV operators supported by the C More login system."""
+ url = self.config['links']['accountAPI'] + 'operators'
+ params = {
+ 'client': self.client,
+ 'country_code': self.locale_suffix
+ }
+ data = self.make_request(url, 'get', params=params)
+
+ return data['data']['operators']
+
+ def login(self, username=None, password=None, operator=None):
+ """Complete login process for C More."""
+ url = self.config['links']['accountAPI'] + 'session'
+ params = {
+ 'client': self.client,
+ 'legacy': 'true'
+ }
+
+ if self.get_credentials().get('remember_me'):
+ method = 'put'
+ payload = {
+ 'locale': self.locale,
+ 'remember_me': self.get_credentials()['remember_me']['token']
+ }
+ else:
+ method = 'post'
+ payload = {
+ 'username': username,
+ 'password': password
+ }
+ if operator:
+ payload['country_code'] = self.locale_suffix
+ payload['operator'] = operator
+
+ credentials = self.make_request(url, method, params=params, payload=payload)
+ self.save_credentials(json.dumps(credentials))
+
+ def get_page(self, page_id, namespace='page'):
+ url = self.config['links']['pageAPI'] + page_id
+ params = {
+ 'locale': self.locale,
+ 'namespace': namespace
+ }
+ headers = {'Authorization': 'Bearer {0}'.format(self.get_credentials().get('jwt_token'))}
+ data = self.make_request(url, 'get', params=params, headers=headers)
+
+ return data['data']
+
+ def get_content_details(self, page_type, page_id, season=None, size='999', page='1'):
+ url = self.config['links']['contentDetailsAPI'] + '{0}/{1}'.format(page_type, page_id)
+ params = {'locale': self.locale}
+ if season:
+ params['season'] = season
+ params['size'] = size
+ params['page'] = page
+
+ headers = {'Authorization': 'Bearer {0}'.format(self.get_credentials().get('jwt_token'))}
+ data = self.make_request(url, 'get', params=params, headers=headers)
+
+ return data['data']
+
+ def parse_page(self, page_id, namespace='page', root_page=False):
+ page = self.get_page(page_id, namespace)
+ if 'targets' in page:
+ return page['targets'] # movie/series items on theme-pages
+ if 'nowPlaying' in page:
+ return page['nowPlaying'] # tv channels/program info
+ if 'scheduledEvents' in page:
+ return page['scheduledEvents'] # sports events
+ if page.get('containers'):
+ if page['containers'].get('page_link_container'):
+ if page['containers']['page_link_container']['pageLinks'] and root_page:
+ # no parsing needed as it's already in the 'correct' format
+ return page['containers']['page_link_container']['pageLinks']
+ if 'genre_containers' in page['containers']:
+ return self.parse_containers(page['containers']['genre_containers'])
+ if 'section_containers' in page['containers']:
+ return self.parse_containers(page['containers']['section_containers'])
+
+ # if nothing matches
+ self.log('Failed to parse page.')
+ return False
+
+ def parse_containers(self, containers):
+ """Parse containers in a sane format. See addon.py for implementation examples."""
+ parsed_containers = []
+ for i in containers:
+ if i['pageLink']['id']:
+ parsed_containers.append(i['pageLink'])
+ else:
+ container = {
+ 'id': i['id'],
+ 'attributes': i['attributes'],
+ 'page_data': i['targets']
+
+ }
+ parsed_containers.append(container)
+
+ return parsed_containers
+
+ def get_stream(self, video_id):
+ """Return a dict with stream URL and Widevine license URL."""
+ stream = {}
+ allowed_formats = ['ism', 'mpd']
+ url = self.config['links']['vimondRestAPI'] + 'api/tve_web/asset/{0}/play.json'.format(video_id)
+ params = {'protocol': 'VUDASH'}
+ headers = {'Authorization': 'Bearer {0}'.format(self.get_credentials().get('vimond_token'))}
+ data_dict = self.make_request(url, 'get', params=params, headers=headers)['playback']
+ stream['drm_protected'] = data_dict['drmProtected']
+
+ if isinstance(data_dict['items']['item'], list):
+ for i in data_dict['items']['item']:
+ if i['mediaFormat'] in allowed_formats:
+ stream['mpd_url'] = i['url']
+ if stream['drm_protected']:
+ stream['license_url'] = i['license']['@uri']
+ stream['drm_type'] = i['license']['@name']
+ break
+ else:
+ stream['mpd_url'] = data_dict['items']['item']['url']
+ if stream['drm_protected']:
+ stream['license_url'] = data_dict['items']['item']['license']['@uri']
+ stream['drm_type'] = data_dict['items']['item']['license']['@name']
+
+ live_stream_offset = self.parse_stream_offset(video_id)
+ if live_stream_offset:
+ stream['mpd_url'] = '{0}?t={1}'.format(stream['mpd_url'], live_stream_offset)
+
+ return stream
+
+ def parse_stream_offset(self, video_id):
+ """Calculate offset parameter needed for on-demand sports content."""
+ url = self.config['links']['vimondRestAPI'] + 'api/tve_web/asset/{0}.json'.format(video_id)
+ params = {'expand': 'metadata'}
+ headers = {'Authorization': 'Bearer {0}'.format(self.get_credentials().get('jwt_token'))}
+ data = self.make_request(url, 'get', params=params, headers=headers)['asset']
+
+ if 'live-event-end' in data['metadata']:
+ utc_time_difference = int(data['liveBroadcastTime'].split('+')[1][1])
+ start_time_local = self.parse_datetime(data['liveBroadcastTime'])
+ end_time_local = self.parse_datetime(data['metadata']['live-event-end']['$'])
+
+ start_time_utc = start_time_local - timedelta(hours=utc_time_difference)
+ end_time_utc = end_time_local - timedelta(hours=utc_time_difference)
+ offset = '{0}-{1}'.format(start_time_utc.isoformat(), end_time_utc.isoformat())
+ return offset
+ else:
+ return None
+
+ def get_image_url(self, image_url):
+ """Request the image from their image proxy. Can be extended to resize/add image effects automatically.
+ See https://imageproxy.b17g.services/docs for more information."""
+ if image_url:
+ return '{0}?source={1}'.format(self.config['links']['imageProxy'], image_url)
+ else:
+ return None
+
+ def get_search_data(self, query, page='0', page_size='300'):
+ url = self.config['links']['searchAPI']
+ params = {
+ 'query': query,
+ 'page': page,
+ 'pageSize': page_size,
+ 'locale': self.locale
+ }
+ headers = {'Authorization': 'Bearer {0}'.format(self.get_credentials().get('jwt_token'))}
+ data = self.make_request(url, 'get', params=params, headers=headers)
+
+ return data['data']['hits']
+
+ @staticmethod
+ def parse_datetime(event_date):
+ """Parse date string to datetime object."""
+ date_time_format = '%Y-%m-%dT%H:%M:%S+' + event_date.split('+')[1] # summer/winter time changes format
+ datetime_obj = datetime(*(time.strptime(event_date, date_time_format)[0:6]))
+ return datetime_obj
+
+ @staticmethod
+ def get_current_time():
+ """Return the current local time."""
+ return datetime.now()
diff --git a/plugin.video.cmore/resources/lib/kodihelper.py b/plugin.video.cmore/resources/lib/kodihelper.py
new file mode 100644
index 0000000..8dc64fd
--- /dev/null
+++ b/plugin.video.cmore/resources/lib/kodihelper.py
@@ -0,0 +1,237 @@
+import os
+import urllib
+import re
+
+from cmore import CMore
+
+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)
+ self.c = CMore(self.addon_profile, self.get_setting('locale'), 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 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 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 check_for_prerequisites(self):
+ if self.set_locale(self.get_setting('locale')) and self.set_login_credentials() and self.check_for_credentials():
+ return True
+ else:
+ return False
+
+ def set_login_credentials(self):
+ username = self.get_setting('username')
+ password = self.get_setting('password')
+
+ if self.get_setting('tv_provider_login'):
+ operator = self.get_operator(self.get_setting('operator'))
+ if not operator:
+ return False
+ else:
+ operator = None
+ self.set_setting('operator_title', '')
+ self.set_setting('operator', '')
+
+ if not username or not password:
+ if operator:
+ return self.set_tv_provider_credentials()
+ else:
+ self.dialog('ok', self.language(30017), self.language(30018))
+ self.get_addon().openSettings()
+ return False
+ else:
+ return True
+
+ def check_for_credentials(self):
+ if not self.c.get_credentials():
+ self.login_process()
+ return True
+
+ def login_process(self):
+ username = self.get_setting('username')
+ password = self.get_setting('password')
+ operator = self.get_setting('operator')
+ self.c.login(username, password, operator)
+
+ def set_tv_provider_credentials(self):
+ operator = self.get_setting('operator')
+ operators = self.c.get_operators()
+ for i in operators:
+ if operator == i['name']:
+ username_type = i['username']
+ password_type = i['password']
+ info_message = re.sub('<[^<]+?>', '', i['login']) # strip html tags
+ break
+ self.dialog('ok', self.get_setting('operator_title'), message=info_message)
+ username = self.get_user_input(username_type)
+ password = self.get_user_input(password_type, hidden=True)
+
+ if username and password:
+ self.set_setting('username', username)
+ self.set_setting('password', password)
+ return True
+ else:
+ return False
+
+ def set_locale(self, locale=None):
+ countries = ['sv_SE', 'da_DK', 'nb_NO']
+ if not locale:
+ options = [self.language(30013), self.language(30014), self.language(30015)]
+ selected_locale = self.dialog('select', self.language(30012), options=options)
+ if selected_locale is None:
+ selected_locale = 0 # default to .se
+ self.set_setting('locale_title', options[selected_locale])
+ self.set_setting('locale', countries[selected_locale])
+ self.reset_credentials() # reset credentials when locale is changed
+
+ return True
+
+ def get_operator(self, operator=None):
+ if not operator:
+ self.set_setting('tv_provider_login', 'true')
+ operators = self.c.get_operators()
+ options = [x['title'] for x in operators]
+
+ selected_operator = self.dialog('select', self.language(30010), options=options)
+ if selected_operator is not None:
+ operator = operators[selected_operator]['name']
+ operator_title = operators[selected_operator]['title']
+ self.set_setting('operator', operator)
+ self.set_setting('operator_title', operator_title)
+
+ return self.get_setting('operator')
+
+ def reset_credentials(self):
+ self.c.reset_credentials()
+ self.set_setting('operator', '')
+ self.set_setting('operator_title', '')
+ self.set_setting('username', '')
+ self.set_setting('password', '')
+
+ 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_item(self, video_id):
+ wv_proxy_base = 'http://localhost:' + str(self.get_setting('wv_proxy_port'))
+ stream = self.c.get_stream(video_id)
+ if stream['drm_protected']:
+ drm = 'widevine'
+ else:
+ drm = None
+
+ ia_helper = inputstreamhelper.Helper('mpd', drm=drm)
+ if ia_helper.check_inputstream():
+ playitem = xbmcgui.ListItem(path=stream['mpd_url'])
+ playitem.setProperty('inputstreamaddon', 'inputstream.adaptive')
+ playitem.setProperty('inputstream.adaptive.manifest_type', 'mpd')
+ if drm:
+ playitem.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha')
+ wv_proxy_url = '{0}?mpd_url={1}&license_url={2}'.format(wv_proxy_base, stream['mpd_url'], stream['license_url'])
+ playitem.setProperty('inputstream.adaptive.license_key', wv_proxy_url + '||R{SSM}|')
+ xbmcplugin.setResolvedUrl(self.handle, True, listitem=playitem)
+
+ def get_as_bool(self, string):
+ if string == 'true':
+ return True
+ else:
+ return False
diff --git a/plugin.video.cmore/resources/settings.xml b/plugin.video.cmore/resources/settings.xml
new file mode 100644
index 0000000..8813f5d
--- /dev/null
+++ b/plugin.video.cmore/resources/settings.xml
@@ -0,0 +1,15 @@
+<settings>
+ <category label="30000">
+ <setting id="locale_title" type="action" option="close" action="RunPlugin(plugin://plugin.video.cmore/?setting=set_locale)" label="30012" default="cmore.se"/>
+ <setting id="tv_provider_login" type="bool" label="30008" enable="eq(1,)" default="false" />
+ <setting id="username" type="text" label="30006" visible="eq(-1,false)" enable="!eq(-1,) + eq(1,)" default=""/>
+ <setting id="password" type="text" label="30007" visible="eq(-2,false)" option="hidden" enable="!eq(-1,) + eq(0,)" default=""/>
+ <setting id="operator_title" type="text" label="30011" visible="eq(-3,true) + !eq(0,)" enable="false" subsetting="true" default=""/>
+ <setting id="reset_operator" type="action" label="30019" action="RunPlugin(plugin://plugin.video.cmore/?setting=reset_credentials)" visible="eq(-4,true) + !eq(-1,)"/>
+ <setting id="reset_credentials" type="action" label="30027" action="RunPlugin(plugin://plugin.video.cmore/?setting=reset_credentials)" visible="eq(-5,false) + !eq(-4,)" subsetting="true"/>
+ <setting id="infotext" type="text" label="30032" visible="eq(-6,true) + eq(3,)" enable="false" subsetting="true" default=""/>
+ <setting id="wv_proxy_port" value="8000" visible="false"/>
+ <setting id="locale" type="text" visible="false" default="sv_SE"/>
+ <setting id="operator" type="text" visible="false" default=""/>
+ </category>
+</settings>
diff --git a/plugin.video.cmore/service.py b/plugin.video.cmore/service.py
new file mode 100644
index 0000000..568ddbb
--- /dev/null
+++ b/plugin.video.cmore/service.py
@@ -0,0 +1,52 @@
+# Author: asciidisco
+# Module: service
+# Created on: 13.01.2017
+# License: MIT https://goo.gl/5bMj3H
+
+import threading
+import SocketServer
+import socket
+from xbmc import Monitor
+from resources.lib.kodihelper import KodiHelper
+from resources.lib.WidevineHTTPRequestHandler import WidevineHTTPRequestHandler
+
+# helper function to select an unused port on the host machine
+def select_unused_port():
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.bind(('127.0.0.1', 0))
+ addr, port = sock.getsockname()
+ sock.close()
+ return port
+
+helper = KodiHelper()
+
+# pick & store a port for the proxy service
+wv_proxy_port = select_unused_port()
+helper.set_setting('wv_proxy_port', str(wv_proxy_port))
+helper.log('Port {0} selected'.format(str(wv_proxy_port)))
+
+# server defaults
+SocketServer.TCPServer.allow_reuse_address = True
+# configure the proxy server
+wv_proxy = SocketServer.TCPServer(('127.0.0.1', wv_proxy_port), WidevineHTTPRequestHandler)
+wv_proxy.server_activate()
+wv_proxy.timeout = 1
+
+if __name__ == '__main__':
+ monitor = Monitor()
+ # start thread for proxy server
+ proxy_thread = threading.Thread(target=wv_proxy.serve_forever)
+ proxy_thread.daemon = True
+ proxy_thread.start()
+
+ # kill the services if kodi monitor tells us to
+ while not monitor.abortRequested():
+ if monitor.waitForAbort(5):
+ wv_proxy.shutdown()
+ break
+
+ # wv-proxy service shutdown sequence
+ wv_proxy.server_close()
+ wv_proxy.socket.close()
+ wv_proxy.shutdown()
+ helper.log('wv-proxy stopped')