diff --git a/Makefile b/Makefile index d388aa20..fd9f3ca9 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,10 @@ addon_xml = addon.xml # Collect information to build as sensible package name name = $(shell xmllint --xpath 'string(/addon/@id)' $(addon_xml)) version = $(shell xmllint --xpath 'string(/addon/@version)' $(addon_xml)) +git_branch = $(shell git rev-parse --abbrev-ref HEAD) git_hash = $(shell git rev-parse --short HEAD) -zip_name = $(name)-$(version)-$(git_hash).zip +zip_name = $(name)-$(version)-$(git_branch)-$(git_hash).zip include_files = main.py addon.xml LICENSE README.md resources/ include_paths = $(patsubst %,$(name)/%,$(include_files)) exclude_files = \*.new \*.orig \*.pyc \*.pyo diff --git a/addon.xml b/addon.xml index f7907121..1b9549cb 100644 --- a/addon.xml +++ b/addon.xml @@ -2,6 +2,8 @@ + + diff --git a/resources/lib/kodiutils.py b/resources/lib/kodiutils.py index 42a65339..be3fa94d 100644 --- a/resources/lib/kodiutils.py +++ b/resources/lib/kodiutils.py @@ -85,6 +85,11 @@ def get_global_setting(setting): return None +def get_cond_visibility(condition): + ''' Test a condition in XBMC ''' + return xbmc.getCondVisibility(condition) + + def has_socks(): ''' Test if socks is installed, and remember this information ''' if not hasattr(has_socks, 'installed'): diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index 00ad402f..3ed341cf 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -12,7 +12,7 @@ import routing from resources.lib import kodilogging from resources.lib import vtmgostream -from resources.lib.kodiutils import get_setting, get_global_setting, notification, show_ok_dialog, show_settings +from resources.lib.kodiutils import get_cond_visibility, get_global_setting, get_setting, notification, show_ok_dialog, show_settings from resources.lib.vtmgo import VtmGo, Content ADDON = Addon() @@ -42,7 +42,7 @@ def index(): listitem.setInfo('video', { 'plot': 'Watch channels live via Internet', }) - xbmcplugin.addDirectoryItem(plugin.handle, plugin.url_for(show_live), listitem, True) + xbmcplugin.addDirectoryItem(plugin.handle, plugin.url_for(show_livetv), listitem, True) # Only provide YouTube option when plugin.video.youtube is available if xbmc.getCondVisibility('System.HasAddon(plugin.video.youtube)') != 0: @@ -79,8 +79,8 @@ def check_credentials(): show_settings() -@plugin.route('/live') -def show_live(): +@plugin.route('/livetv') +def show_livetv(): try: _vtmGo = VtmGo() channels = _vtmGo.get_live() @@ -90,36 +90,35 @@ def show_live(): for channel in channels: listitem = ListItem(channel.name, offscreen=True) - listitem.setArt({ - 'icon': channel.logo, - }) - description = '[B][COLOR red]Geo-blocked[/COLOR][/B]\n\n' - if channel.epg: - description += 'Now: %s - %s\n' % ( - channel.epg[0].start.strftime('%H:%M'), - channel.epg[0].end.strftime('%H:%M') - ) - description += channel.epg[0].title + '\n' - description += '\n' + # Try to use the white icons for thumbnails (used for icons as well) + if get_cond_visibility('System.HasAddon(resource.images.studios.white)') == 1: + thumb = 'resource://resource.images.studios.white/{studio}.png'.format(studio=channel.name) + else: + thumb = channel.logo - if len(channel.epg) > 1: - description += 'Next: %s - %s\n' % ( - channel.epg[1].start.strftime('%H:%M'), - channel.epg[1].end.strftime('%H:%M') - ) - description += channel.epg[1].title + '\n' - description += '\n' + # Try to use the coloured icons for fanart + if get_cond_visibility('System.HasAddon(resource.images.studios.coloured)') == 1: + fanart = 'resource://resource.images.studios.coloured/{studio}.png'.format(studio=channel.name) + elif get_cond_visibility('System.HasAddon(resource.images.studios.white)') == 1: + fanart = 'resource://resource.images.studios.white/{studio}.png'.format(studio=channel.name) + else: + fanart = channel.logo listitem.setInfo('video', { - 'plot': description, + 'plot': _format_plot(channel), 'playcount': 0, 'studio': channel.name, 'mediatype': channel.mediatype, }) + listitem.setArt({ + 'icon': channel.logo, + 'fanart': fanart, + 'thumb': thumb, + }) listitem.setProperty('IsPlayable', 'true') - xbmcplugin.addDirectoryItem(plugin.handle, plugin.url_for(play_live, channel=channel.id) + '?.pvr', listitem) + xbmcplugin.addDirectoryItem(plugin.handle, plugin.url_for(play_livetv, channel=channel.id) + '?.pvr', listitem) # Sort live channels by default like in VTM GO. xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_UNSORTED) @@ -207,7 +206,7 @@ def show_movie(movie): }) listitem.setInfo('video', { 'title': movie_obj.name, - 'plot': _format_remaining(movie_obj.remaining) + movie_obj.description, + 'plot': _format_plot(movie_obj), 'duration': movie_obj.duration, 'year': movie_obj.year, 'mediatype': movie_obj.mediatype, @@ -247,7 +246,7 @@ def show_program(program, season=None): 'tvshowtitle': program_obj.name, 'title': 'All seasons', 'subtitle': program_obj.description, - 'plot': '[B]%s[/B]\n%s' % (program_obj.name, program_obj.description), + 'plot': _format_plot(program_obj), 'set': program_obj.name, }) xbmcplugin.addDirectoryItem(plugin.handle, plugin.url_for(show_program, program=program, season='all'), listitem, True) @@ -262,7 +261,7 @@ def show_program(program, season=None): 'tvshowtitle': program_obj.name, 'title': 'Season %d' % s.number, 'subtitle': program_obj.description, - 'plot': '[B]%s[/B]\n%s' % (program_obj.name, program_obj.description), + 'plot': _format_plot(program_obj), 'set': program_obj.name, 'season': season, }) @@ -290,12 +289,13 @@ def show_program(program, season=None): 'tvshowtitle': program_obj.name, 'title': episode.name, 'subtitle': program_obj.description, - 'plot': _format_remaining(episode.remaining) + episode.description, + 'plot': _format_plot(episode), 'duration': episode.duration, 'season': episode.season, 'episode': episode.number, 'mediatype': episode.mediatype, 'set': program_obj.name, + 'studio': episode.channel, }) listitem.addStreamInfo('video', { 'duration': episode.duration, @@ -317,12 +317,31 @@ def show_program(program, season=None): def show_youtube(): from resources.lib import YOUTUBE for entry in YOUTUBE: + # Try to use the white icons for thumbnails (used for icons as well) + if get_cond_visibility('System.HasAddon(resource.images.studios.white)') == 1: + thumb = 'resource://resource.images.studios.white/{studio}.png'.format(**entry) + else: + thumb = 'DefaultTags.png' + + # Try to use the coloured icons for fanart + if get_cond_visibility('System.HasAddon(resource.images.studios.coloured)') == 1: + fanart = 'resource://resource.images.studios.coloured/{studio}.png'.format(**entry) + elif get_cond_visibility('System.HasAddon(resource.images.studios.white)') == 1: + fanart = 'resource://resource.images.studios.white/{studio}.png'.format(**entry) + else: + fanart = 'DefaultTags.png' + listitem = ListItem(entry.get('label'), offscreen=True) listitem.setInfo('video', { 'plot': 'Watch [B]%(label)s[/B] on YouTube' % entry, 'studio': entry.get('studio'), 'mediatype': 'video', }) + listitem.setArt({ + 'icon': 'DefaultTags.png', + 'fanart': fanart, + 'thumb': thumb, + }) xbmcplugin.addDirectoryItem(plugin.handle, entry.get('path'), listitem, True) # Sort by default like in our dict. @@ -372,8 +391,8 @@ def show_search(): xbmcplugin.endOfDirectory(plugin.handle) -@plugin.route('/play/live/') -def play_live(channel): +@plugin.route('/play/livetv/') +def play_livetv(channel): _stream('channels', channel) @@ -387,17 +406,46 @@ def play_episode(episode): _stream('episodes', episode) -def _format_remaining(days): - if days is None: - return '' - if days == 0: - availability = 'Available until midnight' - elif days == 1: - availability = '%d day remaining' % days - else: - availability = '%d days remaining' % days +def _format_plot(obj): + plot = '' + + # Add program name to plot + if hasattr(obj, 'name'): + plot += '[B]{name}[/B]\n'.format(name=obj.name) + + if hasattr(obj, 'geoblocked') and obj.geoblocked: + plot += '[COLOR red]Geo-blocked[/COLOR]\n' + + if hasattr(obj, 'remaining') and obj.remaining is not None: + if obj.remaining == 0: + plot += '[COLOR blue]Available until midnight[/COLOR]\n' + elif obj.remaining == 1: + plot += '[COLOR blue]One day remaining[/COLOR]\n' + else: + plot += '[COLOR blue]{days} days remaining[/COLOR]\n'.format(days=obj.remaining) + + if plot: + plot += '\n' + + if hasattr(obj, 'description'): + plot += obj.description + + if hasattr(obj, 'epg'): + if obj.epg: + plot += '[COLOR yellow][B]Now:[/B] %s - %s\n' % ( + obj.epg[0].start.strftime('%H:%M'), + obj.epg[0].end.strftime('%H:%M'), + ) + plot += '» %s[/COLOR]\n' % obj.epg[0].title + + if len(obj.epg) > 1: + plot += '[B]Next:[/B] %s - %s\n' % ( + obj.epg[1].start.strftime('%H:%M'), + obj.epg[1].end.strftime('%H:%M'), + ) + plot += '» %s\n' % obj.epg[1].title - return '[B][COLOR blue]%s[/COLOR][/B]\n\n' % availability + return plot def _stream(strtype, strid): diff --git a/resources/lib/vtmgo.py b/resources/lib/vtmgo.py index d48366be..b6656d20 100644 --- a/resources/lib/vtmgo.py +++ b/resources/lib/vtmgo.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals +import re +import os.path import json import logging try: # Python 3 @@ -30,6 +32,7 @@ def __init__(self, channel_id=None, name=None, logo=None, epg=None): self.name = name self.logo = logo self.epg = epg + self.geoblocked = True self.mediatype = 'video' def __repr__(self): @@ -70,7 +73,7 @@ class Content: CONTENT_TYPE_MOVIE = 'MOVIE' CONTENT_TYPE_PROGRAM = 'PROGRAM' - def __init__(self, video_id=None, title=None, description=None, cover=None, video_type=None): + def __init__(self, video_id=None, title=None, description=None, cover=None, video_type=None, geoblocked=None): """ Defines a Category from the Catalogue. :type video_id: basestring @@ -78,12 +81,14 @@ def __init__(self, video_id=None, title=None, description=None, cover=None, vide :type description: basestring :type cover: basestring :type video_type: basestring + :type geoblocked: boolean """ self.id = video_id self.title = title self.description = description if description else '' self.cover = cover self.type = video_type + self.geoblocked = geoblocked # If it is a TV show we return None to get a folder icon self.mediatype = 'movie' if video_type == self.CONTENT_TYPE_MOVIE else None @@ -92,7 +97,7 @@ def __repr__(self): class Movie: - def __init__(self, movie_id=None, name=None, description=None, year=None, cover=None, duration=None, remaining=None): + def __init__(self, movie_id=None, name=None, description=None, year=None, cover=None, duration=None, remaining=None, geoblocked=None, channel=None): self.id = movie_id self.name = name self.description = description if description else '' @@ -100,6 +105,8 @@ def __init__(self, movie_id=None, name=None, description=None, year=None, cover= self.cover = cover self.duration = duration self.remaining = remaining + self.geoblocked = geoblocked + self.channel = channel self.mediatype = 'movie' def __repr__(self): @@ -107,7 +114,7 @@ def __repr__(self): class Program: - def __init__(self, program_id=None, name=None, description=None, cover=None, seasons=None): + def __init__(self, program_id=None, name=None, description=None, cover=None, seasons=None, geoblocked=None, channel=None): """ Defines a Program. :type program_id: basestring @@ -121,6 +128,8 @@ def __init__(self, program_id=None, name=None, description=None, cover=None, sea self.description = description if description else '' self.cover = cover self.seasons = seasons + self.geoblocked = geoblocked + self.channel = channel self.mediatype = 'tvshow' def __repr__(self): @@ -128,7 +137,7 @@ def __repr__(self): class Season: - def __init__(self, number=None, episodes=None, cover=None): + def __init__(self, number=None, episodes=None, cover=None, channel=None): """ :type number: basestring @@ -138,21 +147,24 @@ def __init__(self, number=None, episodes=None, cover=None): self.number = int(number) self.episodes = episodes self.cover = cover + self.channel = channel def __repr__(self): return "%r" % self.__dict__ class Episode: - def __init__(self, episode_id=None, number=None, season=None, name=None, description=None, cover=None, duration=None, remaining=None): + def __init__(self, episode_id=None, number=None, season=None, name=None, description=None, cover=None, duration=None, remaining=None, geoblocked=None, channel=None): self.id = episode_id self.number = int(number) self.season = int(season) - self.name = name + self.name = re.compile('^%d. ' % number).sub('', name) # Strip episode from name self.description = description if description else '' self.cover = cover self.duration = int(duration) if duration else None self.remaining = int(remaining) if remaining is not None else None + self.geoblocked = geoblocked + self.channel = channel self.mediatype = 'episode' def __repr__(self): @@ -178,18 +190,18 @@ def get_live(self): info = json.loads(response) channels = [] - for item in info['channels']: + for item in info.get('channels'): epg = [] - for item_epg in item['broadcasts']: + for item_epg in item.get('broadcasts', []): epg.append(LiveChannelEpg( - title=item_epg['name'], - start=dateutil.parser.parse(item_epg['startsAt']), - end=dateutil.parser.parse(item_epg['endsAt']), + title=item_epg.get('name'), + start=dateutil.parser.parse(item_epg.get('startsAt')), + end=dateutil.parser.parse(item_epg.get('endsAt')), )) channels.append(LiveChannel( - channel_id=item['channelId'], - logo=item['channelLogoUrl'], - name=item['name'], + channel_id=item.get('channelId'), + logo=item.get('channelLogoUrl'), + name=item.get('name'), epg=epg, )) @@ -200,10 +212,10 @@ def get_categories(self): info = json.loads(response) categories = [] - for item in info['catalogFilters']: + for item in info.get('catalogFilters', []): categories.append(Category( - category_id=item['id'], - title=item['title'], + category_id=item.get('id'), + title=item.get('title'), )) return categories @@ -216,12 +228,13 @@ def get_items(self, category=None): info = json.loads(response) items = [] - for item in info['pagedTeasers']['content']: + for item in info.get('pagedTeasers', {}).get('content', []): items.append(Content( - video_id=item['target']['id'], - title=item['title'], - cover=item['imageUrl'], - video_type=item['target']['type'], + video_id=item.get('target', {}).get('id'), + title=item.get('title'), + cover=item.get('imageUrl'), + video_type=item.get('target', {}).get('type'), + geoblocked=item.get('geoBlocked'), )) return items @@ -229,48 +242,59 @@ def get_items(self, category=None): def get_movie(self, movie_id): response = self._get_url('/vtmgo/movies/' + movie_id) info = json.loads(response) + movie = info.get('movie', {}) return Movie( - movie_id=info['movie']['id'], - name=info['movie']['name'], - description=info['movie']['description'], - duration=info['movie']['durationSeconds'], - cover=info['movie']['bigPhotoUrl'], - year=info['movie']['productionYear'], - remaining=info['movie']['remainingDaysAvailable'], + movie_id=movie.get('id'), + name=movie.get('name'), + description=movie.get('description'), + duration=movie.get('durationSeconds'), + cover=movie.get('bigPhotoUrl'), + year=movie.get('productionYear'), + geoblocked=movie.get('geoBlocked'), + remaining=movie.get('remainingDaysAvailable'), + channel=os.path.basename(movie.get('channelLogoUrl', 'vtm')).split('-')[0], ) def get_program(self, program_id): response = self._get_url('/vtmgo/programs/' + program_id) info = json.loads(response) + program = info.get('program', {}) + channel = os.path.basename(program.get('channelLogoUrl', 'vtm')).split('-')[0], seasons = {} - for item_season in info['program']['seasons']: + for item_season in program.get('seasons', []): episodes = {} - for item_episode in item_season['episodes']: - episodes[item_episode['index']] = Episode( - episode_id=item_episode['id'], - number=item_episode['index'], - season=item_season['index'], - name=item_episode['name'], - description=item_episode['description'], - duration=item_episode['durationSeconds'], - cover=item_episode['bigPhotoUrl'], - remaining=item_episode['remainingDaysAvailable'], + + for item_episode in item_season.get('episodes', []): + episodes[item_episode.get('index')] = Episode( + episode_id=item_episode.get('id'), + number=item_episode.get('index'), + season=item_season.get('index'), + name=item_episode.get('name'), + description=item_episode.get('description'), + duration=item_episode.get('durationSeconds'), + cover=item_episode.get('bigPhotoUrl'), + geoblocked=program.get('geoBlocked'), + remaining=item_episode.get('remainingDaysAvailable'), + channel=channel, ) - seasons[item_season['index']] = Season( - number=item_season['index'], + seasons[item_season.get('index')] = Season( + number=item_season.get('index'), episodes=episodes, - cover=item_season['episodes'][0]['bigPhotoUrl'], + cover=item_season.get('episodes', [{}])[0].get('bigPhotoUrl') if episodes else program.get('bigPhotoUrl'), + channel=channel, ) return Program( - program_id=info['program']['id'], - name=info['program']['name'], - description=info['program']['description'], - cover=info['program']['bigPhotoUrl'], + program_id=program.get('id'), + name=program.get('name'), + description=program.get('description'), + cover=program.get('bigPhotoUrl'), + geoblocked=program.get('geoBlocked'), seasons=seasons, + channel=channel, ) # def get_episodes(self, episode_id): @@ -284,11 +308,11 @@ def do_search(self, search): results = json.loads(response) items = [] - for item in results['suggestions']: + for item in results.get('suggestions', []): items.append(Content( - video_id=item['id'], - title=item['name'], - video_type=item['type'], + video_id=item.get('id'), + title=item.get('name'), + video_type=item.get('type'), )) return items