From 27854c6fbe810992f76591a99ba7e6c3f958b060 Mon Sep 17 00:00:00 2001 From: emilsvennesson Date: Fri, 29 Dec 2017 20:06:24 +0100 Subject: [plugin.video.cmore] 0.2.0 --- plugin.video.cmore/resources/__init__.py | 1 + plugin.video.cmore/resources/fanart.jpg | Bin 0 -> 56624 bytes plugin.video.cmore/resources/icon.png | Bin 0 -> 52285 bytes .../language/resource.language.en_gb/strings.po | 146 +++++++++ plugin.video.cmore/resources/lib/Widevine.py | 29 ++ .../resources/lib/WidevineHTTPRequestHandler.py | 33 +++ plugin.video.cmore/resources/lib/__init__.py | 1 + plugin.video.cmore/resources/lib/cmore.py | 328 +++++++++++++++++++++ plugin.video.cmore/resources/lib/kodihelper.py | 237 +++++++++++++++ plugin.video.cmore/resources/settings.xml | 15 + 10 files changed, 790 insertions(+) create mode 100644 plugin.video.cmore/resources/__init__.py create mode 100644 plugin.video.cmore/resources/fanart.jpg create mode 100644 plugin.video.cmore/resources/icon.png create mode 100644 plugin.video.cmore/resources/language/resource.language.en_gb/strings.po create mode 100644 plugin.video.cmore/resources/lib/Widevine.py create mode 100644 plugin.video.cmore/resources/lib/WidevineHTTPRequestHandler.py create mode 100644 plugin.video.cmore/resources/lib/__init__.py create mode 100644 plugin.video.cmore/resources/lib/cmore.py create mode 100644 plugin.video.cmore/resources/lib/kodihelper.py create mode 100644 plugin.video.cmore/resources/settings.xml (limited to 'plugin.video.cmore/resources') 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 Binary files /dev/null and b/plugin.video.cmore/resources/fanart.jpg differ diff --git a/plugin.video.cmore/resources/icon.png b/plugin.video.cmore/resources/icon.png new file mode 100644 index 0000000..00e1162 Binary files /dev/null and b/plugin.video.cmore/resources/icon.png 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 \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 @@ + + + + + + + + + + + + + + + -- cgit v1.2.3