diff --git a/contributing.md b/contributing.md index b3f2f68..36b996d 100644 --- a/contributing.md +++ b/contributing.md @@ -77,13 +77,15 @@ The functions must be sorted alphabetically to make the maintenance easier. ## Coding style -The code is still based on the design of the alpha version so the coding style -is not mature yet. -A redesign is planned but until then please: +Here are the rules to follow when modifying the code: * document the usage of functions following [Sphinx format](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#python-signatures) +* use double quotes instead of single quotes when defining strings (to avoid + escaping apostrophes which are common characters in English and other + languages) * follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) conventions. The - compliance can be checked with [pylint](https://www.pylint.org/) and the + compliance is checked in the `quality` job (more details are available in the + [CI file](.gitlab-ci.yml)). Pylint can also be run locally with the following commands: ```python @@ -91,11 +93,7 @@ python3 -m pip install -r misc/python_requirements.txt python3 -m pylint --rcfile=misc/pylint-rcfile.txt ``` -The pylint violations are also checked in the `quality` job. - -More details are available in the [CI file](.gitlab-ci.yml). - -Note: pylint is run with python3 to have latest features whereas the add-on +Note: pylint is run with python3 to have latest features even though the add-on only supports Kodi v18 Leia (which uses python2) ## How to release a new version of this add-on diff --git a/peertube.py b/peertube.py index 8c7d6dd..8c2a148 100644 --- a/peertube.py +++ b/peertube.py @@ -36,7 +36,7 @@ class PeertubeAddon(): """ # URL of the page which explains how to install libtorrent - HELP_URL = 'https://link.infini.fr/libtorrent-peertube-kodi' + HELP_URL = "https://link.infini.fr/libtorrent-peertube-kodi" def __init__(self, plugin, plugin_id): """ @@ -49,19 +49,19 @@ class PeertubeAddon(): self.plugin_id = plugin_id # Select preferred instance by default - self.selected_inst ='https://{}'\ - .format(get_setting('preferred_instance')) + self.selected_inst ="https://{}"\ + .format(get_setting("preferred_instance")) # Get the number of videos to show per page - self.items_per_page = int(get_setting('items_per_page')) + self.items_per_page = int(get_setting("items_per_page")) # Get the preferred resolution for video - self.preferred_resolution = get_setting('preferred_resolution') + self.preferred_resolution = get_setting("preferred_resolution") # Nothing to play at initialisation self.play = 0 - self.torrent_name = '' - self.torrent_f = '' + self.torrent_name = "" + self.torrent_f = "" # Check whether libtorrent could be imported by the service. The value # of the associated property is retrieved only once and stored in an @@ -69,14 +69,14 @@ class PeertubeAddon(): # of the service (we assume it is not possible to start the add-on # before the service) self.libtorrent_imported = \ - get_property('libtorrent_imported') == 'True' + get_property("libtorrent_imported") == "True" # Create a PeerTube object to send requests: settings which are used # only by this object are directly retrieved from the settings self.peertube = PeerTube(instance=self.selected_inst, count=self.items_per_page, - sort=get_setting('video_sort_method'), - video_filter=get_setting('video_filter')) + sort=get_setting("video_sort_method"), + video_filter=get_setting("video_filter")) def create_list(self, lst, data_type, start): """ @@ -86,68 +86,68 @@ class PeertubeAddon(): """ # Create a list for our items. listing = [] - for data in lst['data']: + for data in lst["data"]: # Create a list item with a text label - list_item = xbmcgui.ListItem(label=data['name']) + list_item = xbmcgui.ListItem(label=data["name"]) - if data_type == 'videos': + if data_type == "videos": # Add thumbnail list_item.setArt({ - 'thumb': '{0}/{1}'.format(self.selected_inst, - data['thumbnailPath'])}) + "thumb": "{0}/{1}".format(self.selected_inst, + data["thumbnailPath"])}) # Set a fanart image for the list item. - # list_item.setProperty('fanart_image', data['thumb']) + # list_item.setProperty("fanart_image", data["thumb"]) # Compute media info from item's metadata - info = {'title': data['name'], - 'playcount': data['views'], - 'plotoutline': data['description'], - 'duration': data['duration'] + info = {"title": data["name"], + "playcount": data["views"], + "plotoutline": data["description"], + "duration": data["duration"] } # For videos, add a rating based on likes and dislikes - if data['likes'] > 0 or data['dislikes'] > 0: - info['rating'] = data['likes'] / ( - data['likes'] + data['dislikes']) + if data["likes"] > 0 or data["dislikes"] > 0: + info["rating"] = data["likes"] / ( + data["likes"] + data["dislikes"]) # Set additional info for the list item. - list_item.setInfo('video', info) + list_item.setInfo("video", info) # Videos are playable - list_item.setProperty('IsPlayable', 'true') + list_item.setProperty("IsPlayable", "true") # Build the Kodi URL to play the associated video only with the # id of the video. The instance is omitted because the # currently selected instance will be used automatically. url = self.build_kodi_url({ - 'action': 'play_video', - 'id': data['uuid'] + "action": "play_video", + "id": data["uuid"] }) - elif data_type == 'instances': + elif data_type == "instances": # TODO: Add a context menu to select instance as preferred # Instances are not playable - list_item.setProperty('IsPlayable', 'false') + list_item.setProperty("IsPlayable", "false") # Set URL to select this instance url = self.build_kodi_url({ - 'action': 'select_instance', - 'url': data['host'] + "action": "select_instance", + "url": data["host"] }) # Add our item to the listing as a 3-element tuple. listing.append((url, list_item, False)) - # Add a 'Next page' button when there are more data to show + # Add a "Next page" button when there are more data to show start = int(start) + self.items_per_page - if lst['total'] > start: - list_item = xbmcgui.ListItem(label='Next page ({0})' + if lst["total"] > start: + list_item = xbmcgui.ListItem(label="Next page ({0})" .format(start/self.items_per_page)) url = self.build_kodi_url({ - 'action': 'browse_{0}'.format(data_type), - 'start': start}) + "action": "browse_{0}".format(data_type), + "start": start}) listing.append((url, list_item, True)) return listing @@ -168,8 +168,8 @@ class PeertubeAddon(): else: # If an instance was provided (external call), ensure the URL is # prefixed with HTTPS - if not instance.startswith('https://'): - instance = 'https://{}'.format(instance) + if not instance.startswith("https://"): + instance = "https://{}".format(instance) # Retrieve the information about the video metadata = self.peertube.get_video(video_id) @@ -178,46 +178,46 @@ class PeertubeAddon(): # different resolutions available for a video may be stored in "files" # or "streamingPlaylists[].files". Note that "files" will always exist # in the response but may be empty. - if len(metadata['files']) != 0: - files = metadata['files'] + if len(metadata["files"]) != 0: + files = metadata["files"] else: - files = metadata['streamingPlaylists'][0]['files'] + files = metadata["streamingPlaylists"][0]["files"] - debug('Looking for the best resolution matching the user preferences') + debug("Looking for the best resolution matching the user preferences") current_res = 0 higher_res = -1 - torrent_url = '' + torrent_url = "" for f in files: # Get the resolution - res = f['resolution']['id'] + res = f["resolution"]["id"] if res == self.preferred_resolution: # Stop directly when we find the exact same resolution as the # user's preferred one - debug('Found video with preferred resolution') - torrent_url = f['torrentUrl'] + debug("Found video with preferred resolution") + torrent_url = f["torrentUrl"] break elif res < self.preferred_resolution and res > current_res: # Otherwise, try to find the best one just below the user's # preferred one - debug('Found video with good lower resolution ({0})' - .format(f['resolution']['label'])) - torrent_url = f['torrentUrl'] + debug("Found video with good lower resolution ({0})" + .format(f["resolution"]["label"])) + torrent_url = f["torrentUrl"] current_res = res elif (res > self.preferred_resolution and (res < higher_res or higher_res == -1)): # In the worst case, we'll take the one just above the user's # preferred one - debug('Saving video with higher resolution ({0}) as a possible' - ' alternative'.format(f['resolution']['label'])) - backup_url = f['torrentUrl'] + debug("Saving video with higher resolution ({0}) as a possible" + " alternative".format(f["resolution"]["label"])) + backup_url = f["torrentUrl"] higher_res = res # When we didn't find a resolution equal or lower than the user's # preferred one, use the resolution just above the preferred one if not torrent_url: - debug('Using video with higher resolution as alternative') + debug("Using video with higher resolution as alternative") torrent_url = backup_url return torrent_url @@ -232,7 +232,7 @@ class PeertubeAddon(): # Ask the user which keywords must be searched for keywords = xbmcgui.Dialog().input( - heading='Search videos on {}'.format(self.selected_inst), + heading="Search videos on {}".format(self.selected_inst), type=xbmcgui.INPUT_ALPHANUM) # Go back to main menu when user cancels @@ -244,12 +244,12 @@ class PeertubeAddon(): # Exit directly when no result is found if not results: - notif_warning(title='No videos found', - message='No videos found matching the query.') + notif_warning(title="No videos found", + message="No videos found matching the query.") return # Create array of xmbcgui.ListItem's - listing = self.create_list(results, 'videos', start) + listing = self.create_list(results, "videos", start) # Add our listing to Kodi. xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) @@ -267,7 +267,7 @@ class PeertubeAddon(): results = self.peertube.list_videos(start) # Create array of xmbcgui.ListItem's - listing = self.create_list(results, 'videos', start) + listing = self.create_list(results, "videos", start) # Add our listing to Kodi. xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) @@ -283,7 +283,7 @@ class PeertubeAddon(): results = list_instances(start) # Create array of xmbcgui.ListItem's - listing = self.create_list(results, 'instances', start) + listing = self.create_list(results, "instances", start) # Add our listing to Kodi. xbmcplugin.addDirectoryItems(self.plugin_id, listing, len(listing)) @@ -297,9 +297,9 @@ class PeertubeAddon(): :param data: dict """ - debug('Received metadata_downloaded signal, will start playing media') + debug("Received metadata_downloaded signal, will start playing media") self.play = 1 - self.torrent_f = data['file'] + self.torrent_f = data["file"] def play_video(self, torrent_url): """ @@ -309,21 +309,21 @@ class PeertubeAddon(): # If libtorrent could not be imported, display a message and do not try # download nor play the video as it will fail. if not self.libtorrent_imported: - open_dialog(title='Error: libtorrent could not be imported', - message='PeerTube cannot play videos without' - ' libtorrent.\nPlease follow the instructions' - ' at {}'.format(self.HELP_URL)) + open_dialog(title="Error: libtorrent could not be imported", + message="PeerTube cannot play videos without" + " libtorrent.\nPlease follow the instructions" + " at {}".format(self.HELP_URL)) return - debug('Starting torrent download ({0})'.format(torrent_url)) + debug("Starting torrent download ({0})".format(torrent_url)) # Start a downloader thread - AddonSignals.sendSignal('start_download', {'url': torrent_url}) + AddonSignals.sendSignal("start_download", {"url": torrent_url}) # Wait until the PeerTubeDownloader has downloaded all the torrent's # metadata - AddonSignals.registerSlot('plugin.video.peertube', - 'metadata_downloaded', + AddonSignals.registerSlot("plugin.video.peertube", + "metadata_downloaded", self.play_video_continue) timeout = 0 while self.play == 0 and timeout < 10: @@ -332,35 +332,35 @@ class PeertubeAddon(): # Abort in case of timeout if timeout == 10: - notif_error(title='Download timeout', - message='Timeout fetching {}'.format(torrent_url)) + notif_error(title="Download timeout", + message="Timeout fetching {}".format(torrent_url)) return else: # Wait a little before starting playing the torrent xbmc.sleep(3000) # Pass the item to the Kodi player for actual playback. - debug('Starting video playback ({0})'.format(torrent_url)) + debug("Starting video playback ({0})".format(torrent_url)) play_item = xbmcgui.ListItem(path=self.torrent_f) xbmcplugin.setResolvedUrl(self.plugin_id, True, listitem=play_item) def select_instance(self, instance): """ - Change currently selected instance to 'instance' parameter + Change currently selected instance to "instance" parameter :param instance: str """ # Update the object attribute even though it is not used currently but # it may be useful in case reuselanguageinvoker is enabled. - self.selected_inst = 'https://{}'.format(instance) + self.selected_inst = "https://{}".format(instance) # Update the preferred instance in the settings so that this choice is # reused on the next run and the next call of the add-on - set_setting('preferred_instance', instance) + set_setting("preferred_instance", instance) # Notify the user and log the event - message = '{0} is now the selected instance'.format(self.selected_inst) - notif_info(title='Current instance changed', + message = "{0} is now the selected instance".format(self.selected_inst) + notif_info(title="Current instance changed", message=message) debug(message) @@ -371,7 +371,7 @@ class PeertubeAddon(): encoded in the URL """ - return '{0}?{1}'.format(self.plugin_url, urlencode(parameters)) + return "{0}?{1}".format(self.plugin_url, urlencode(parameters)) def main_menu(self): """ @@ -382,18 +382,18 @@ class PeertubeAddon(): listing = [] # 1st menu entry - list_item = xbmcgui.ListItem(label='Browse selected instance') - url = self.build_kodi_url({'action': 'browse_videos', 'start': 0}) + list_item = xbmcgui.ListItem(label="Browse selected instance") + url = self.build_kodi_url({"action": "browse_videos", "start": 0}) listing.append((url, list_item, True)) # 2nd menu entry - list_item = xbmcgui.ListItem(label='Search on selected instance') - url = self.build_kodi_url({'action': 'search_videos', 'start': 0}) + list_item = xbmcgui.ListItem(label="Search on selected instance") + url = self.build_kodi_url({"action": "search_videos", "start": 0}) listing.append((url, list_item, True)) # 3rd menu entry - list_item = xbmcgui.ListItem(label='Select other instance') - url = self.build_kodi_url({'action': 'browse_instances', 'start': 0}) + list_item = xbmcgui.ListItem(label="Select other instance") + url = self.build_kodi_url({"action": "browse_instances", "start": 0}) listing.append((url, list_item, True)) # Add our listing to Kodi. @@ -415,27 +415,27 @@ class PeertubeAddon(): # Check the parameters passed to the plugin if params: - action = params['action'] - if action == 'browse_videos': + action = params["action"] + if action == "browse_videos": # Browse videos on selected instance - self.browse_videos(params['start']) - elif action == 'search_videos': + self.browse_videos(params["start"]) + elif action == "search_videos": # Search for videos on selected instance - self.search_videos(params['start']) - elif action == 'browse_instances': + self.search_videos(params["start"]) + elif action == "browse_instances": # Browse peerTube instances - self.browse_instances(params['start']) - elif action == 'play_video': + self.browse_instances(params["start"]) + elif action == "play_video": # This action comes with the id of the video to play as # parameter. The instance may also be in the parameters. Use # these parameters to retrieve the complete URL (containing the # resolution). - url = self.get_video_url(instance=params.get('instance'), - video_id=params.get('id')) + url = self.get_video_url(instance=params.get("instance"), + video_id=params.get("id")) # Play the video using the URL self.play_video(url) - elif action == 'select_instance': - self.select_instance(params['url']) + elif action == "select_instance": + self.select_instance(params["url"]) else: # Display the addon's main menu when the plugin is called from # Kodi UI without any parameters @@ -443,13 +443,13 @@ class PeertubeAddon(): # Display a warning if libtorrent could not be imported if not self.libtorrent_imported: - open_dialog(title='Error: libtorrent could not be imported', - message='You can still browse and search videos' - ' but you will not be able to play them.\n' - 'Please follow the instructions at {}' + open_dialog(title="Error: libtorrent could not be imported", + message="You can still browse and search videos" + " but you will not be able to play them.\n" + "Please follow the instructions at {}" .format(self.HELP_URL)) -if __name__ == '__main__': +if __name__ == "__main__": # Initialise addon addon = PeertubeAddon(sys.argv[0], int(sys.argv[1])) diff --git a/resources/lib/kodi_utils.py b/resources/lib/kodi_utils.py index 1d86aaa..b0b53b0 100644 --- a/resources/lib/kodi_utils.py +++ b/resources/lib/kodi_utils.py @@ -19,7 +19,7 @@ def debug(message): (the name is hard-coded to avoid calling xbmcaddon each time since the name should not change) """ - xbmc.log('[PeerTube] {}'.format(message), xbmc.LOGDEBUG) + xbmc.log("[PeerTube] {}".format(message), xbmc.LOGDEBUG) def get_property(name): """Retrieve the value of a window property related to the add-on @@ -29,7 +29,7 @@ def get_property(name): :return: Value of the window property :rtype: str """ - return xbmcgui.Window(10000).getProperty('peertube_{}'.format(name)) + return xbmcgui.Window(10000).getProperty("peertube_{}".format(name)) def get_setting(setting_name): """Retrieve the value of a setting @@ -85,7 +85,7 @@ def set_property(name, value): actual name of the property is prefixed with "peertube_") :param str value: New value of the property """ - xbmcgui.Window(10000).setProperty('peertube_{}'.format(name), value) + xbmcgui.Window(10000).setProperty("peertube_{}".format(name), value) def set_setting(setting_name, setting_value): """Modify the value of a setting diff --git a/resources/lib/peertube.py b/resources/lib/peertube.py index 5812489..60e5a44 100644 --- a/resources/lib/peertube.py +++ b/resources/lib/peertube.py @@ -34,10 +34,10 @@ class PeerTube: # The value "video_filter" is directly retrieved from the settings so # it must be converted into one of the expected values by the REST APIs - if 'all-local' in video_filter: - self.filter = 'all-local' + if "all-local" in video_filter: + self.filter = "all-local" else: - self.filter = 'local' + self.filter = "local" def _request(self, method, url, params=None, data=None): """Call a REST API on the instance diff --git a/service.py b/service.py index a513a63..888d6df 100644 --- a/service.py +++ b/service.py @@ -39,7 +39,7 @@ class PeertubeDownloader(Thread): :param str message: Message to log (will be prefixed with the name of the class) """ - debug('PeertubeDownloader: {}'.format(message)) + debug("PeertubeDownloader: {}".format(message)) def run(self): """ @@ -48,20 +48,20 @@ class PeertubeDownloader(Thread): :return: None """ - self.debug('Opening BitTorent session') + self.debug("Opening BitTorent session") # Open BitTorrent session ses = libtorrent.session() ses.listen_on(6881, 6891) # Add torrent - self.debug('Adding torrent {}'.format(self.torrent)) - h = ses.add_torrent({'url': self.torrent, 'save_path': self.temp_dir}) + self.debug("Adding torrent {}".format(self.torrent)) + h = ses.add_torrent({"url": self.torrent, "save_path": self.temp_dir}) # Set sequential mode to allow watching while downloading h.set_sequential_download(True) # Download torrent - self.debug('Downloading torrent {}'.format(self.torrent)) + self.debug("Downloading torrent {}".format(self.torrent)) signal_sent = 0 while not h.is_seed(): xbmc.sleep(1000) @@ -69,11 +69,11 @@ class PeertubeDownloader(Thread): # Inform addon that all the metadata has been downloaded and that # it may start playing the torrent if s.state >=3 and signal_sent == 0: - self.debug('Received all torrent metadata, notifying' - ' PeertubeAddon') + self.debug("Received all torrent metadata, notifying" + " PeertubeAddon") i = h.torrent_file() f = self.temp_dir + i.name() - AddonSignals.sendSignal('metadata_downloaded', {'file': f}) + AddonSignals.sendSignal("metadata_downloaded", {"file": f}) signal_sent = 1 class PeertubeService(): @@ -86,8 +86,8 @@ class PeertubeService(): PeertubeService initialisation function """ # Create our temporary directory - self.temp = '{}{}'.format(xbmc.translatePath('special://temp'), - 'plugin.video.peertube/') + self.temp = "{}{}".format(xbmc.translatePath("special://temp"), + "plugin.video.peertube/") if not xbmcvfs.exists(self.temp): xbmcvfs.mkdir(self.temp) @@ -97,17 +97,17 @@ class PeertubeService(): :param str message: Message to log (will be prefixed with the name of the class) """ - debug('PeertubeService: {}'.format(message)) + debug("PeertubeService: {}".format(message)) def download_torrent(self, data): """ - Start a downloader thread to download torrent specified by data['url'] + Start a downloader thread to download torrent specified by data["url"] :param data: dict :return: None """ - self.debug('Received a start_download signal') - downloader = PeertubeDownloader(data['url'], self.temp) + self.debug("Received a start_download signal") + downloader = PeertubeDownloader(data["url"], self.temp) downloader.start() def run(self): @@ -118,25 +118,25 @@ class PeertubeService(): thread when needed, and exit when Kodi is shutting down. """ - self.debug('Starting') + self.debug("Starting") # Launch the download_torrent callback function when the - # 'start_download' signal is received - AddonSignals.registerSlot('plugin.video.peertube', - 'start_download', + # "start_download" signal is received + AddonSignals.registerSlot("plugin.video.peertube", + "start_download", self.download_torrent) # Monitor Kodi's shutdown signal - self.debug('Service started, waiting for signals') + self.debug("Service started, waiting for signals") monitor = xbmc.Monitor() while not monitor.abortRequested(): if monitor.waitForAbort(1): # Abort was requested while waiting. We must exit # TODO: Clean temporary directory - self.debug('Exiting') + self.debug("Exiting") break -if __name__ == '__main__': +if __name__ == "__main__": # Create a PeertubeService instance service = PeertubeService() @@ -146,12 +146,12 @@ if __name__ == '__main__': LIBTORRENT_IMPORTED = True except ImportError as exception: LIBTORRENT_IMPORTED = False - service.debug('The libtorrent library could not be imported because of' - ' the following error:\n{}'.format(exception)) + service.debug("The libtorrent library could not be imported because of" + " the following error:\n{}".format(exception)) # Save whether libtorrent could be imported as a window property so that # this information can be retrieved by the add-on - set_property('libtorrent_imported', str(LIBTORRENT_IMPORTED)) + set_property("libtorrent_imported", str(LIBTORRENT_IMPORTED)) # Start the service service.run()