diff --git a/requirements.txt b/requirements.txt index df1cbe9..f30e92a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,4 @@ python-dateutil requests git+git://github.com/dagwieers/kodi-plugin-routing.git@setup#egg=routing tox -six sakee \ No newline at end of file diff --git a/resources/lib/addon.py b/resources/lib/addon.py index 70bf446..b7b8b1e 100644 --- a/resources/lib/addon.py +++ b/resources/lib/addon.py @@ -11,7 +11,7 @@ kodilogging.config() routing = Plugin() # pylint: disable=invalid-name -_LOGGER = logging.getLogger('addon') +_LOGGER = logging.getLogger(__name__) @routing.route('/') diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py index 88524fa..a248b31 100644 --- a/resources/lib/kodiutils.py +++ b/resources/lib/kodiutils.py @@ -26,7 +26,7 @@ 'unsorted', 'title' ] -_LOGGER = logging.getLogger('kodiutils') +_LOGGER = logging.getLogger(__name__) class TitleItem: @@ -109,9 +109,18 @@ def addon_path(): return get_addon_info('path') +def translate_path(path): + """Converts a Kodi special:// path to a normal path""" + try: # Kodi 19 alpha 2 and higher + from xbmcvfs import translatePath + except ImportError: # Kodi 19 alpha 1 and lower + return to_unicode(xbmc.translatePath(from_unicode(path))) + return translatePath(path) + + def addon_profile(): """Cache and return add-on profile""" - return to_unicode(xbmc.translatePath(ADDON.getAddonInfo('profile'))) + return translate_path(ADDON.getAddonInfo('profile')) def url_for(name, *args, **kwargs): diff --git a/resources/lib/modules/channels.py b/resources/lib/modules/channels.py index 0ac0140..0cd5e21 100644 --- a/resources/lib/modules/channels.py +++ b/resources/lib/modules/channels.py @@ -53,7 +53,7 @@ def show_channels(self): listing = [] for item in channels: title_item = Menu.generate_titleitem_channel(item) - title_item.path = kodiutils.url_for('show_channel', channel_id=item.uid) + title_item.path = kodiutils.url_for('show_channel', channel_id=item.get_combi_id()) title_item.is_playable = False listing.append(title_item) @@ -64,7 +64,7 @@ def show_channel(self, channel_id): :param str channel_id: The channel we want to display. """ - channel = self._channel_api.get_asset(channel_id) + channel = self._channel_api.get_asset(channel_id.split(':')[0]) listing = [] @@ -143,9 +143,15 @@ def show_channel_guide_detail(self, channel_id, date): :param str channel_id: The channel for which we want to show an EPG. :param str date: The date to show. """ - programs = self._epg_api.get_guide([channel_id], date) + # Lookup with CAPI + lookup_id = channel_id.split(':')[1] + programs = self._epg_api.get_guide_with_capi([lookup_id], date) - listing = [Menu.generate_titleitem_program(item, timeline=True) for item in programs.get(channel_id)] + # Lookup with TV API + # lookup_id = channel_id.split(':')[0] + # programs = self._epg_api.get_guide([lookup_id], date) + + listing = [Menu.generate_titleitem_program(item, timeline=True) for item in programs.get(lookup_id)] kodiutils.show_listing(listing, 30013, content='files') @@ -154,7 +160,7 @@ def show_channel_replay(self, channel_id): :param str channel_id: The channel for which we want to show the replay programs. """ - programs = self._channel_api.get_replay(channel_id) + programs = self._channel_api.get_replay(channel_id.split(':')[0]) listing = [] for item in programs: diff --git a/resources/lib/modules/iptvmanager.py b/resources/lib/modules/iptvmanager.py index 9736dd6..ef38ad7 100644 --- a/resources/lib/modules/iptvmanager.py +++ b/resources/lib/modules/iptvmanager.py @@ -7,6 +7,7 @@ from collections import defaultdict from resources.lib import kodiutils +from resources.lib.solocoo import Credit from resources.lib.solocoo.auth import AuthApi from resources.lib.solocoo.channel import ChannelApi from resources.lib.solocoo.epg import EpgApi @@ -54,7 +55,7 @@ def send_channels(self): streams.append(dict( name=channel.title, stream=kodiutils.url_for('play_asset', asset_id=channel.uid), - id=channel.uid, + id=channel.station_id, logo=channel.icon, preset=channel.number, )) @@ -72,12 +73,28 @@ def send_epg(self): # Load EPG data channels = channel_api.get_channels() for date in ['yesterday', 'today', 'tomorrow']: - for channel, programs in epg_api.get_guide([channel.uid for channel in channels], date).items(): + for channel, programs in epg_api.get_guide_with_capi([channel.station_id for channel in channels], date).items(): for program in programs: # Hide these items if program.title == EpgApi.EPG_NO_BROADCAST: continue + # Construct mapping for credits + program_credits = [] + for credit in program.credit: + if credit.role == Credit.ROLE_ACTOR: + program_credits.append({'type': 'actor', 'name': credit.person, 'role': credit.character}) + elif credit.role == Credit.ROLE_DIRECTOR: + program_credits.append({'type': 'director', 'name': credit.person}) + elif credit.role == Credit.ROLE_PRODUCER: + program_credits.append({'type': 'producer', 'name': credit.person}) + elif credit.role == Credit.ROLE_COMPOSER: + program_credits.append({'type': 'composer', 'name': credit.person}) + elif credit.role == Credit.ROLE_PRESENTER: + program_credits.append({'type': 'presenter', 'name': credit.person}) + elif credit.role == Credit.ROLE_GUEST: + program_credits.append({'type': 'guest', 'name': credit.person}) + epg[channel].append(dict( start=program.start.isoformat(), stop=program.end.isoformat(), @@ -85,9 +102,10 @@ def send_epg(self): description=program.description, subtitle=None, episode='S%dE%d' % (program.season, program.episode) if program.season and program.episode else None, - genre=None, + genre=program.genres, image=program.cover, date=None, + credits=program_credits, stream=kodiutils.url_for('play_asset', asset_id=program.uid) if program.replay else None)) return dict(version=1, epg=epg) diff --git a/resources/lib/modules/menu.py b/resources/lib/modules/menu.py index f110bf2..f74b701 100644 --- a/resources/lib/modules/menu.py +++ b/resources/lib/modules/menu.py @@ -10,6 +10,7 @@ from resources.lib import kodiutils from resources.lib.kodiutils import TitleItem +from resources.lib.solocoo import Credit _LOGGER = logging.getLogger(__name__) @@ -107,9 +108,9 @@ def generate_titleitem_program(cls, item, timeline=False): path=kodiutils.url_for('play_asset', asset_id=item.uid), art_dict={ 'cover': item.cover, - 'icon': item.preview, - 'thumb': item.preview, - 'fanart': item.preview, + 'icon': item.preview or item.cover, + 'thumb': item.preview or item.cover, + 'fanart': item.preview or item.cover, }, info_dict={ 'tvshowtitle': item.title, @@ -122,6 +123,11 @@ def generate_titleitem_program(cls, item, timeline=False): 'aired': item.start.strftime('%Y-%m-%d'), 'date': item.start.strftime('%d.%m.%Y'), 'duration': item.duration, + 'cast': + [(credit.person, credit.character) for credit in item.credit if credit.role == Credit.ROLE_ACTOR] + + [credit.person for credit in item.credit if credit.role in [Credit.ROLE_PRESENTER, Credit.ROLE_GUEST]], + 'director': [credit.person for credit in item.credit if credit.role in [Credit.ROLE_DIRECTOR, Credit.ROLE_PRODUCER]], + # 'credits': [credit.person for credit in item.credit if credit.role in [Credit.ROLE_COMPOSER]], }, prop_dict={ 'inputstream.adaptive.play_timeshift_buffer': 'true', # Play from the beginning diff --git a/resources/lib/modules/player.py b/resources/lib/modules/player.py index fb392ae..20d403e 100644 --- a/resources/lib/modules/player.py +++ b/resources/lib/modules/player.py @@ -7,10 +7,10 @@ from resources.lib import kodiutils from resources.lib.modules.menu import Menu +from resources.lib.solocoo import Program, Channel from resources.lib.solocoo.auth import AuthApi from resources.lib.solocoo.channel import ChannelApi from resources.lib.solocoo.exceptions import NotAvailableInOfferException, UnavailableException, InvalidTokenException -from resources.lib.solocoo.util import Program, Channel _LOGGER = logging.getLogger(__name__) @@ -32,20 +32,27 @@ def play_asset(self, asset_id): :param string asset_id: The ID of the asset to play. """ # Get asset info - asset = self._channel_api.get_asset(asset_id) + if len(asset_id) == 32: + # a locId is 32 chars + asset = self._channel_api.get_asset_by_locid(asset_id) + else: + # an asset_id is 40 chars + asset = self._channel_api.get_asset(asset_id) if isinstance(asset, Program): item = Menu.generate_titleitem_program(asset) elif isinstance(asset, Channel): item = Menu.generate_titleitem_channel(asset) + else: + raise Exception('Unknown asset type: %s' % asset) # Get stream info try: - stream_info = self._channel_api.get_stream(asset_id) + stream_info = self._channel_api.get_stream(asset.uid) except InvalidTokenException: # Retry with fresh tokens self._auth.login(True) - stream_info = self._channel_api.get_stream(asset_id) + stream_info = self._channel_api.get_stream(asset.uid) except (NotAvailableInOfferException, UnavailableException) as exc: _LOGGER.error(exc) kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable and can't be played right now. diff --git a/resources/lib/modules/search.py b/resources/lib/modules/search.py index 666b4e7..d8edba2 100644 --- a/resources/lib/modules/search.py +++ b/resources/lib/modules/search.py @@ -7,9 +7,9 @@ from resources.lib import kodiutils from resources.lib.modules.menu import Menu +from resources.lib.solocoo import Program, Channel from resources.lib.solocoo.auth import AuthApi from resources.lib.solocoo.search import SearchApi -from resources.lib.solocoo.util import Program, Channel _LOGGER = logging.getLogger(__name__) diff --git a/resources/lib/solocoo/__init__.py b/resources/lib/solocoo/__init__.py index b1f6da7..5dc273e 100644 --- a/resources/lib/solocoo/__init__.py +++ b/resources/lib/solocoo/__init__.py @@ -19,3 +19,101 @@ )), # and many more, ... ]) + + +class Channel: + """ Channel Object """ + + def __init__(self, uid, station_id, title, icon, preview, number, epg_now=None, epg_next=None, replay=False, radio=False, available=None): + """ + :param Program epg_now: The currently playing program on this channel. + :param Program epg_next: The next playing program on this channel. + """ + self.uid = uid + self.station_id = station_id + self.title = title + self.icon = icon + self.preview = preview + self.number = number + self.epg_now = epg_now + self.epg_next = epg_next + self.replay = replay + self.radio = radio + + self.available = available + + def __repr__(self): + return "%r" % self.__dict__ + + def get_combi_id(self): + """ Return a combination of the uid and the station_id. """ + return "%s:%s" % (self.uid, self.station_id) + + +class StreamInfo: + """ Stream information """ + + def __init__(self, url, protocol, drm_protocol, drm_license_url, drm_certificate): + self.url = url + self.protocol = protocol + self.drm_protocol = drm_protocol + self.drm_license_url = drm_license_url + self.drm_certificate = drm_certificate + + def __repr__(self): + return "%r" % self.__dict__ + + +class Program: + """ Program object """ + + def __init__(self, uid, title, description, cover, preview, start, end, duration, channel_id, formats, genres, replay, + restart, age, series_id=None, season=None, episode=None, credit=None, available=None): + """ + + :type credit: list[Credit] + """ + self.uid = uid + self.title = title + self.description = description + self.cover = cover + self.preview = preview + self.start = start + self.end = end + self.duration = duration + + self.age = age + self.channel_id = channel_id + + self.formats = formats + self.genres = genres + + self.replay = replay + self.restart = restart + + self.series_id = series_id + self.season = season + self.episode = episode + + self.credit = credit + + self.available = available + + def __repr__(self): + return "%r" % self.__dict__ + + +class Credit: + """ Credit object """ + + ROLE_ACTOR = 'Actor' + ROLE_COMPOSER = 'Composer' + ROLE_DIRECTOR = 'Director' + ROLE_GUEST = 'Guest' + ROLE_PRESENTER = 'Presenter' + ROLE_PRODUCER = 'Producer' + + def __init__(self, role, person, character=None): + self.role = role + self.person = person + self.character = character diff --git a/resources/lib/solocoo/auth.py b/resources/lib/solocoo/auth.py index 934e435..467de73 100644 --- a/resources/lib/solocoo/auth.py +++ b/resources/lib/solocoo/auth.py @@ -103,6 +103,10 @@ def __init__(self, username, password, tenant, token_path): # Do login so we have valid tokens self.login() + def get_tenant(self): + """ Return the tenant information. """ + return self._tenant + def login(self, force=False): """ Make a login request. diff --git a/resources/lib/solocoo/channel.py b/resources/lib/solocoo/channel.py index a9118a4..0a22a0a 100644 --- a/resources/lib/solocoo/channel.py +++ b/resources/lib/solocoo/channel.py @@ -11,9 +11,9 @@ import dateutil.tz from requests import HTTPError -from resources.lib.solocoo import SOLOCOO_API, util +from resources.lib.solocoo import SOLOCOO_API, util, StreamInfo from resources.lib.solocoo.exceptions import NotAvailableInOfferException, UnavailableException -from resources.lib.solocoo.util import parse_channel, StreamInfo, parse_program +from resources.lib.solocoo.util import parse_channel, parse_program _LOGGER = logging.getLogger(__name__) @@ -31,6 +31,7 @@ def __init__(self, auth): """ self._auth = auth self._tokens = self._auth.login() + self._tenant = self._auth.get_tenant() def get_channels(self): """ Get all channels. @@ -41,12 +42,34 @@ def get_channels(self): entitlements = self._auth.list_entitlements() offers = entitlements.get('offers', []) + # Fetch channel listing from TV API reply = util.http_get(SOLOCOO_API + '/bouquet', token_bearer=self._tokens.jwt_token) data = json.loads(reply.text) + # Fetch channel listing from CAPI + # We need this for the stationid that we can use to fetch a better EPG + capi_reply = util.http_get( + 'https://{domain}/{env}/capi.aspx'.format(domain=self._tenant.get('domain'), env=self._tenant.get('env')), + params={ + 'z': 'epg', + 'f_format': 'clx', # channel listing + 'd': 3, + 'v': 3, + 'u': self._tokens.device_serial, + 'a': self._tenant.get('app'), + # 111 = BIT_CHANNEL_DETAIL_ID + BIT_CHANNEL_DETAIL_NUMBER + BIT_CHANNEL_DETAIL_STATIONID + + # BIT_CHANNEL_DETAIL_TITLE + BIT_CHANNEL_DETAIL_GENRES + BIT_CHANNEL_DETAIL_FLAGS + 'cs': 111, + 'lng': 'nl_BE', + 'streams': 15, + }, + token_cookie=self._tokens.aspx_token) + capi_data = json.loads(capi_reply.text) + station_ids = {row.get('number'): str(row.get('stationid')) for row in capi_data[0][1]} + # Parse list to Channel objects channels = [ - parse_channel(channel.get('assetInfo', {}), offers) + parse_channel(channel.get('assetInfo', {}), offers, station_ids.get(channel.get('assetInfo', {}).get('params', {}).get('lcn'))) for channel in data.get('channels', []) if channel.get('alias', False) is False ] @@ -107,6 +130,19 @@ def get_asset(self, asset_id): raise Exception('Unknown asset type: %s' % data.get('type')) + def get_asset_by_locid(self, loc_id): + """ Convert a locId to a assetId. """ + reply = util.http_get( + 'https://{domain}/{env}/capi.aspx'.format(domain=self._tenant.get('domain'), env=self._tenant.get('env')), + params={ + 'z': 'converttotvapi', + 'locId': loc_id, + 'type': 'EPGProgram', + }, + token_cookie=self._tokens.aspx_token) + data = json.loads(reply.text) + return self.get_asset(data.get('assetId')) + def get_replay(self, channel_id): """ Get a list of programs that are replayable from the given channel. diff --git a/resources/lib/solocoo/epg.py b/resources/lib/solocoo/epg.py index e30dfd7..2bd08b3 100644 --- a/resources/lib/solocoo/epg.py +++ b/resources/lib/solocoo/epg.py @@ -11,7 +11,7 @@ import dateutil.tz from resources.lib.solocoo import SOLOCOO_API, util -from resources.lib.solocoo.util import parse_program +from resources.lib.solocoo.util import parse_program, parse_program_capi _LOGGER = logging.getLogger(__name__) @@ -21,6 +21,7 @@ class EpgApi: # Request this many channels at the same time EPG_CHUNK_SIZE = 40 + EPG_CAPI_CHUNK_SIZE = 100 EPG_NO_BROADCAST = 'Geen uitzending' @@ -31,6 +32,7 @@ def __init__(self, auth): """ self._auth = auth self._tokens = self._auth.login() # Login and make sure we have a token + self._tenant = self._auth.get_tenant() def get_guide(self, channels, date_from=None, date_to=None): """ Get the guide for the specified channels and date. @@ -79,6 +81,62 @@ def get_guide(self, channels, date_from=None, date_to=None): return programs + def get_guide_with_capi(self, channels, date_from=None, date_to=None): + """ Get the guide for the specified channels and date. Lookup by stationid. + + :param list|str channels: A single channel or a list of channels to fetch. + :param str|datetime date_from: The date of the guide we want to fetch. + :param str|datetime date_to: The date of the guide we want to fetch. + :rtype: dict[str, list[resources.lib.solocoo.util.Program]] + """ + # Allow to specify one channel, and we map it to a list + if not isinstance(channels, list): + channels = [channels] + + # Generate dates in UTC format + if date_from is not None: + date_from = self._parse_date(date_from) + else: + date_from = self._parse_date('today') + + if date_to is not None: + date_to = self._parse_date(date_to) + else: + date_to = (date_from + timedelta(days=1)) + + programs = {} + + for i in range(0, len(channels), self.EPG_CAPI_CHUNK_SIZE): + _LOGGER.debug('Fetching EPG at index %d', i) + + reply = util.http_get( + 'https://{domain}/{env}/capi.aspx'.format(domain=self._tenant.get('domain'), env=self._tenant.get('env')), + params={ + 'z': 'epg', + 'f_format': 'pg', # program guide + 'v': 3, # version + 'u': self._tokens.device_serial, + 'a': self._tenant.get('app'), + 's': '!'.join(channels[i:i + self.EPG_CAPI_CHUNK_SIZE]), # station id's separated with a ! + 'f': date_from.strftime("%s") + '000', # from timestamp + 't': date_to.strftime("%s") + '000', # to timestamp + # 736763 = BIT_EPG_DETAIL_ID | BIT_EPG_DETAIL_TITLE | BIT_EPG_DETAIL_DESCRIPTION | BIT_EPG_DETAIL_AGE | + # BIT_EPG_DETAIL_CATEGORY | BIT_EPG_DETAIL_START | BIT_EPG_DETAIL_END | BIT_EPG_DETAIL_FLAGS | + # BIT_EPG_DETAIL_COVER | BIT_EPG_DETAIL_SEASON_NO | BIT_EPG_DETAIL_EPISODE_NO | + # BIT_EPG_DETAIL_SERIES_ID | BIT_EPG_DETAIL_GENRES | BIT_EPG_DETAIL_CREDITS | BIT_EPG_DETAIL_FORMATS + 'cs': 736763, + 'lng': 'nl_BE', + }, + token_cookie=self._tokens.aspx_token) + + data = json.loads(reply.text) + + # Parse to a dict (channel: list[Program]) + programs.update({channel: [parse_program_capi(program, self._tenant) for program in programs] + for channel, programs in data[1].items()}) + + return programs + def get_program(self, uid): """ Get program details by calling the API. diff --git a/resources/lib/solocoo/util.py b/resources/lib/solocoo/util.py index c1a1254..a6382ac 100644 --- a/resources/lib/solocoo/util.py +++ b/resources/lib/solocoo/util.py @@ -11,6 +11,7 @@ import requests from requests import HTTPError +from resources.lib.solocoo import Channel, Program, Credit from resources.lib.solocoo.exceptions import InvalidTokenException _LOGGER = logging.getLogger(__name__) @@ -21,95 +22,6 @@ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' -class Channel: - """ Channel Object """ - - def __init__(self, uid, title, icon, preview, number, epg_now, epg_next, replay, radio=False, available=None): - """ - :param Program epg_now: The currently playing program on this channel. - :param Program epg_next: The next playing program on this channel. - """ - self.uid = uid - self.title = title - self.icon = icon - self.preview = preview - self.number = number - self.epg_now = epg_now - self.epg_next = epg_next - self.replay = replay - self.radio = radio - - self.available = available - - def __repr__(self): - return "%r" % self.__dict__ - - -class StreamInfo: - """ Stream information """ - - def __init__(self, url, protocol, drm_protocol, drm_license_url, drm_certificate): - self.url = url - self.protocol = protocol - self.drm_protocol = drm_protocol - self.drm_license_url = drm_license_url - self.drm_certificate = drm_certificate - - def __repr__(self): - return "%r" % self.__dict__ - - -class Program: - """ Program object """ - - def __init__(self, uid, title, description, cover, preview, start, end, duration, channel_id, formats, genres, replay, - restart, age, series_id=None, season=None, episode=None, credit=None, available=None): - self.uid = uid - self.title = title - self.description = description - self.cover = cover - self.preview = preview - self.start = start - self.end = end - self.duration = duration - - self.age = age - self.channel_id = channel_id - - self.formats = formats - self.genres = genres - - self.replay = replay - self.restart = restart - - self.series_id = series_id - self.season = season - self.episode = episode - - self.credit = credit - - self.available = available - - def __repr__(self): - return "%r" % self.__dict__ - - -class Credit: - """ Credit object """ - - ROLE_ACTOR = 'Actor' - ROLE_COMPOSER = 'Composer' - ROLE_DIRECTOR = 'Director' - ROLE_GUEST = 'Guest' - ROLE_PRESENTER = 'Presenter' - ROLE_PRODUCER = 'Producer' - - def __init__(self, role, person, character=None): - self.role = role - self.person = person - self.character = character - - def find_image(images, image_type): """ Find the largest image of the specified type. @@ -183,20 +95,22 @@ def check_deals_entitlement(deals, offers): return False -def parse_channel(channel, offers=None): +def parse_channel(channel, offers=None, station_id=None): """ Parse the API result of a channel into a Channel object. :param dict channel: The channel info from the API. :param List[str] offers: A list of offers that we have. + :param str station_id: The station ID of the CAPI. :returns: A channel that is parsed. :rtype: Channel """ return Channel( uid=channel.get('id'), + station_id=station_id, title=channel.get('title'), - icon=find_image(channel.get('images'), 'la'), - preview=find_image(channel.get('images'), 'lv'), + icon=find_image(channel.get('images'), 'la'), # landscape + preview=find_image(channel.get('images'), 'lv'), # live number=channel.get('params', {}).get('lcn'), epg_now=parse_program(channel.get('params', {}).get('now')), epg_next=parse_program(channel.get('params', {}).get('next')), @@ -207,7 +121,7 @@ def parse_channel(channel, offers=None): def parse_program(program, offers=None): - """ Parse a program dict. + """ Parse a program dict from the TV API. :param dict program: The program object to parse. :param List[str] offers: A list of offers that we have. @@ -229,7 +143,7 @@ def parse_program(program, offers=None): uid=program.get('id'), title=program.get('title'), description=program.get('desc'), - cover=find_image(program.get('images'), 'po'), # portrait + cover=find_image(program.get('images'), 'po'), # poster preview=find_image(program.get('images'), 'la'), # landscape start=start, end=end, @@ -251,6 +165,64 @@ def parse_program(program, offers=None): ) +def parse_program_capi(program, tenant): + """ Parse an program dict from the CAPI. + + :param dict program: The program object to parse. + :param dict tenant: The tenant object to help with some URL's. + + :returns: A program that is parsed. + :rtype: EpgProgram + """ + if not program: + return None + + # Parse dates + start = datetime.fromtimestamp(program.get('start') / 1000, dateutil.tz.gettz('CET')) + end = datetime.fromtimestamp(program.get('end') / 1000, dateutil.tz.gettz('CET')) + now = datetime.now().replace(tzinfo=dateutil.tz.gettz('CET')) + + # Parse credits + credit_list = [] + for credit in program.get('credits', []): + if not credit.get('r'): # Actor + credit_list.append(Credit(role=Credit.ROLE_ACTOR, person=credit.get('p'), character=credit.get('c'))) + elif credit.get('r') == 1: # Director + credit_list.append(Credit(role=Credit.ROLE_DIRECTOR, person=credit.get('p'))) + elif credit.get('r') == 3: # Producer + credit_list.append(Credit(role=Credit.ROLE_PRODUCER, person=credit.get('p'))) + elif credit.get('r') == 4: # Presenter + credit_list.append(Credit(role=Credit.ROLE_PRESENTER, person=credit.get('p'))) + elif credit.get('r') == 5: # Guest + credit_list.append(Credit(role=Credit.ROLE_GUEST, person=credit.get('p'))) + elif credit.get('r') == 7: # Composer + credit_list.append(Credit(role=Credit.ROLE_COMPOSER, person=credit.get('p'))) + + return Program( + uid=program.get('locId'), + title=program.get('title'), + description=program.get('description'), + cover='https://{domain}/{env}/mmchan/mpimages/447x251/{file}'.format(domain=tenant.get('domain'), + env=tenant.get('env'), + file=program.get('cover').split('/')[-1]) if program.get('cover') else None, + preview=None, + start=start, + end=end, + duration=(end - start).total_seconds(), + channel_id=None, + formats=[program.get('formats')], # We only have one format + genres=program.get('genres'), + replay=program.get('flags') & 16, # BIT_EPG_FLAG_REPLAY + restart=program.get('flags') & 32, # BIT_EPG_FLAG_RESTART + age=program.get('age'), + series_id=program.get('seriesId'), + season=program.get('seasonNo'), + episode=program.get('episodeNo'), + credit=credit_list, + available=(program.get('flags') & 16) and (start < now), # BIT_EPG_FLAG_REPLAY + ) + + def http_get(url, params=None, token_bearer=None, token_cookie=None): """ Make a HTTP GET request for the specified URL. diff --git a/tests/__init__.py b/tests/__init__.py index 9116bdc..a141852 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -9,17 +9,22 @@ import xbmcaddon +try: # Python 3 + from http.client import HTTPConnection +except ImportError: # Python 2 + from httplib import HTTPConnection + logging.basicConfig(level=logging.DEBUG) # Add logging to urllib -# import http.client -# http.client.HTTPConnection.debuglevel = 1 +HTTPConnection.debuglevel = 1 # Make UTF-8 the default encoding in Python 2 if sys.version_info[0] == 2: reload(sys) # pylint: disable=undefined-variable # noqa: F821 sys.setdefaultencoding("utf-8") # pylint: disable=no-member + # Set credentials based on environment data # Use the .env file with Pipenv to make this work nicely during development if os.environ.get('ADDON_USERNAME') and os.environ.get('ADDON_PASSWORD'): diff --git a/tests/test_channel.py b/tests/test_channel.py index 1a0d6ad..3bf83a0 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -9,10 +9,10 @@ import unittest from resources.lib import kodiutils +from resources.lib.solocoo import Channel, StreamInfo, Program from resources.lib.solocoo.auth import AuthApi from resources.lib.solocoo.channel import ChannelApi from resources.lib.solocoo.exceptions import NotAvailableInOfferException -from resources.lib.solocoo.util import Channel, StreamInfo, Program _LOGGER = logging.getLogger(__name__) @@ -34,6 +34,8 @@ def test_get_channels(self): print(channels) self.assertIsInstance(channels, list) self.assertIsInstance(channels[0], Channel) + self.assertIsInstance(channels[0].uid, str) + self.assertIsInstance(channels[0].station_id, str) def test_get_channel(self): api = ChannelApi(self._auth) diff --git a/tests/test_epg.py b/tests/test_epg.py index 8d2363c..7553b47 100644 --- a/tests/test_epg.py +++ b/tests/test_epg.py @@ -9,9 +9,9 @@ import unittest from resources.lib import kodiutils +from resources.lib.solocoo import Program from resources.lib.solocoo.auth import AuthApi from resources.lib.solocoo.epg import EpgApi -from resources.lib.solocoo.util import Program _LOGGER = logging.getLogger(__name__) @@ -37,6 +37,21 @@ def test_get_guide(self): self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], Program) + def test_get_guide_capi(self): + api = EpgApi(self._auth) + + channel_id = [ + '1790975744', # één + '1790975808', # canvas + ] + + guide = api.get_guide_with_capi(channel_id, 'today') + self.assertIsInstance(guide, dict) + + programs = guide.get(channel_id[0]) + self.assertIsInstance(programs, list) + self.assertIsInstance(programs[0], Program) + def test_get_program(self): api = EpgApi(self._auth) diff --git a/tox.ini b/tox.ini index 41396ec..9579396 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ deps = [flake8] builtins = func max-line-length = 160 -ignore = FI13,FI50,FI51,FI53,FI54,W503 +ignore = FI13,FI50,FI51,FI53,FI54,W503,W504 require-code = True min-version = 2.7 exclude = .git,.tox