# A Kodi Addon to play video hosted on the peertube service (http://joinpeertube.org/) # # This is just a Proof-Of-Concept atm but I hope I will be able to make it evolve to # something worth using. # # TODO: - Delete downloaded files by default # - Allow people to choose if they want to keep their download after watching? # - Make sure we are seeding when downloading and watching # - When downloaded torrents are kept, do we want to seed them all the time, # or only when the addon is running, or only when kodi is playing one,...? # - Do sanity checks on received data # - Handle languages better (with .po files) import time, sys import urllib2, json from urlparse import parse_qsl import xbmc, xbmcgui, xbmcaddon, xbmcplugin, xbmcvfs import AddonSignals class PeertubeAddon(): """ Main class of the addon """ def __init__(self, plugin, plugin_id): """ Initialisation of the PeertubeAddon class :param: None :return: None """ xbmc.log('PeertubeAddon: Initialising', xbmc.LOGDEBUG) # Save addon URL and ID self.plugin_url = plugin self.plugin_id = plugin_id # Get an Addon instance addon = xbmcaddon.Addon() # Select preferred instance by default self.selected_inst = addon.getSetting('preferred_instance') # Get the number of videos to show per page # TODO: Why doesn't it work? #self.items_per_page = addon.getSetting('items_per_page') self.items_per_page = 20 # Nothing to play at initialisation self.play = 0 self.torrent_name = '' return None def create_videos_list(self, videos) """ Create an array of xmbcgui.ListIten's to be displayed as a folder in Kodi's UI :param videos: dict :result listing: dict """ # Create a list for our items listing = [] # Return when no videos are found if videos['total'] == 0: xbmc.log('PeertubeAddon: No videos found', xbmc.LOGDEBUG) return else: xbmc.log('PeertubeAddon: Found ' + str(videos['total']) + ' videos', xbmc.LOGDEBUG) # Insert a 'Previous' button when start > 0 # TODO: See if icon can be changed here and for the "Next" button (by an arraow for example) if int(start) > 0: start = int(start) - self.items_per_page list_item = xbmcgui.ListItem(label='Previous') url = '{0}?action=browse&start={1}'.format(self.plugin_url, str(start)) listing.append((url, list_item, True)) # Create a list for our items. for video in videos['data']: # Create a list item with a text label list_item = xbmcgui.ListItem(label=video['name']) # Add thumbnail list_item.setArt({'thumb': self.selected_inst + '/' + video['thumbnailPath']}) # Set a fanart image for the list item. #list_item.setProperty('fanart_image', video['thumb']) # Compute media info from video's metadata info = {'title': video['name'], 'playcount': video['views'], 'plotoutline': video['description'], 'duration': video['duration'] } # Add a rating based on likes and dislikes if video['likes'] > 0 or video['dislikes'] > 0: info['rating'] = video['likes']/(video['likes'] + video['dislikes']) # Set additional info for the list item. list_item.setInfo('video', info) # This is mandatory for playable items! list_item.setProperty('IsPlayable', 'true') # Find smallest file's torrentUrl # TODO: Get the best quality torrent given settings and/or available bandwidth # See how they do that in the peerTube client's code min_size = -1 resp = urllib2.urlopen(self.selected_inst + '/api/v1/videos/' + video['uuid']) metadata = json.load(resp) for f in metadata['files']: if f['size'] < min_size or min_size == -1: torrent_url = f['torrentUrl'] # Add our item to the listing as a 3-element tuple. url = '{0}?action=play&url={1}'.format(self.plugin_url, torrent_url) listing.append((url, list_item, False)) # Insert a 'Next' button when there are more videos to list if videos['total'] > ( int(start) + 1 ) * self.items_per_page: start = int(start) + self.items_per_page list_item = xbmcgui.ListItem(label='Next') url = '{0}?action=browse&start={1}'.format(self.plugin_url, str(start)) listing.append((url, list_item, True)) return listing def search_videos(self, start): """ Search for videos on selected instance :param start: string :result: None """ # Show a 'Search video' dialog search = xbmcgui.Dialog().input(heading='Search video on ' + self.selected_inst, type=xbmcgui.INPUT_ALPHANUM) # Go back to main menu when user cancels if not search: self.main_menu() # Search for videos on selected PeerTube instance # TODO: Handle failures # Make count configurable # Sort videos by rating ( + make the sort method configurabe) xbmc.log('PeertubeAddon: Searching for videos on instance ' + self.selected_inst, xbmc.LOGDEBUG) req = self.selected_inst + '/api/v1/searchs/videos?search=' + search + '&count=' + str(self.items_per_page) + '&start=' + start resp = urllib2.urlopen(req) videos = json.load(resp) # Create array of xmbcgui.ListItem's listing = create_videos_list(videos) # Add our listing to Kodi. xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) xbmcplugin.endOfDirectory(self.plugin_id) def list_videos(self, start): """ Create the list of playable videos in the Kodi interface. :param start: string :return: None """ # Get the list of videos published by the instance # TODO: Handle failures # Make count configurable # Sort videos by rating ( + make the sort method configurabe) xbmc.log('PeertubeAddon: Listing videos from instance ' + self.selected_inst, xbmc.LOGDEBUG) req = self.selected_inst + '/api/v1/videos?count=' + str(self.items_per_page) + '&start=' + start resp = urllib2.urlopen(req) videos = json.load(resp) # Create array of xmbcgui.ListItem's listing = create_videos_list(videos) # Add our listing to Kodi. xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) xbmcplugin.endOfDirectory(self.plugin_id) def play_video_continue(self, data): """ Callback function to let the play_video function resume when the PeertubeDownloader has downloaded all the torrent's metadata :param data: dict :return: None """ xbmc.log('PeertubeAddon: Received metadata_downloaded signal, will start playing media', xbmc.LOGDEBUG) self.play = 1 self.torrent_f = data['file'] return def play_video(self, torrent_url): """ Start the torrent's download and play it while being downloaded :param torrent_url: str :return: None """ xbmc.log('PeertubeAddon: playing video ' + torrent_url, xbmc.LOGDEBUG) # Start a downloader thread AddonSignals.sendSignal('start_download', {'url': torrent_url}) # Wait until the PeerTubeDownloader has downloaded all the torrent's metadata + a little bit more # TODO: Add a timeout AddonSignals.registerSlot('plugin.video.peertube', 'metadata_downloaded', self.play_video_continue) while self.play == 0: xbmc.sleep(1000) xbmc.sleep(3000) # Pass the item to the Kodi player for actual playback. play_item = xbmcgui.ListItem(path=self.torrent_f) xbmcplugin.setResolvedUrl(self.plugin_id, True, listitem=play_item) def main_menu(self): """ """ # Create a list for our items. listing = [] # 1st menu entry list_item = xbmcgui.ListItem(label='Browse selected instance') url = '{0}?action=browse&start=0'.format(self.plugin_url) listing.append((url, list_item, True)) # 2nd menu entry list_item = xbmcgui.ListItem(label='Search on selected instance') url = '{0}?action=search&start=0'.format(self.plugin_url) listing.append((url, list_item, False)) # 3rd menu entry list_item = xbmcgui.ListItem(label='Select other instance') url = '{0}?action=select_inst'.format(self.plugin_url) listing.append((url, list_item, False)) # Add our listing to Kodi. xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) # Add a sort method for the virtual folder items (alphabetically, ignore articles) xbmcplugin.addSortMethod(self.plugin_id, xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE) # Finish creating a virtual folder. xbmcplugin.endOfDirectory(self.plugin_id) def router(self, paramstring): """ Router function that calls other functions depending on the provided paramstring :param paramstring: dict :return: None """ # Parse a URL-encoded paramstring to the dictionary of # {: } elements params = dict(parse_qsl(paramstring[1:])) # Check the parameters passed to the plugin if params: if params['action'] == 'browse': # List videos on selected instance self.list_videos(params['start']) elif params['action'] == 'search': # Search for videos on selecgted instance self.search_videos() elif params['action'] == 'play': # Play video from provided URL. self.play_video(params['url']) else: # Display the addon's main menu when the plugin is called from Kodi UI without any parameters self.main_menu() if __name__ == '__main__': # Initialise addon addon = PeertubeAddon(sys.argv[0], int(sys.argv[1])) # Call the router function and pass the plugin call parameters to it. addon.router(sys.argv[2])