diff --git a/addon.py b/addon.py index 0dbd563..e616e60 100644 --- a/addon.py +++ b/addon.py @@ -36,7 +36,7 @@ class Plugin(object): self.trans_format = addon.getSetting("trans_format") # Create connection - self.connection = libsonic_extra.Connection( + self.connection = libsonic_extra.SubsonicClient( self.url, self.username, self.password) def build_url(self, query): @@ -48,115 +48,6 @@ class Plugin(object): return urlparse.urlunparse(parts) - def walk_genres(self): - """ - Request Subsonic's genres list and iterate each item. - """ - - response = self.connection.getGenres() - - for genre in response["genres"]["genre"]: - yield genre - - def walk_artists(self): - """ - Request SubSonic's index and iterate each item. - """ - - response = self.connection.getArtists() - - for index in response["artists"]["index"]: - for artist in index["artist"]: - yield artist - - def walk_album_list2_genre(self, genre): - """ - Request all albums for a given genre. - """ - - offset = 0 - - while True: - response = self.connection.getAlbumList2( - ltype="byGenre", genre=genre, size=500, offset=offset) - - if not response["albumList2"]["album"]: - break - - for album in response["albumList2"]["album"]: - yield album - - offset += 500 - - def walk_album(self, album_id): - """ - Request Album and iterate each song. - """ - - response = self.connection.getAlbum(album_id) - - for song in response["album"]["song"]: - yield song - - def walk_playlists(self): - """ - Request SubSonic's playlists and iterate over each item. - """ - - response = self.connection.getPlaylists() - - for child in response["playlists"]["playlist"]: - yield child - - def walk_playlist(self, playlist_id): - """ - Request SubSonic's playlist items and iterate over each item. - """ - - response = self.connection.getPlaylist(playlist_id) - - for order, child in enumerate(response["playlist"]["entry"], start=1): - child["order"] = order - yield child - - def walk_directory(self, directory_id): - """ - Request a SubSonic music directory and iterate over each item. - """ - - response = self.connection.getMusicDirectory(directory_id) - - for child in response["directory"]["child"]: - if child.get("isDir"): - for child in self.walk_directory(child["id"]): - yield child - else: - yield child - - def walk_artist(self, artist_id): - """ - Request a SubSonic artist and iterate over each album. - """ - - response = self.connection.getArtist(artist_id) - - for child in response["artist"]["album"]: - yield child - - def walk_random_songs(self, size, genre=None, from_year=None, - to_year=None): - """ - Request random songs by genre and/or year and iterate over each song. - """ - - response = self.connection.getRandomSongs( - size=size, genre=genre, fromYear=from_year, toYear=to_year) - - for song in response["randomSongs"]["song"]: - song["id"] = int(song["id"]) - - yield song - def route(self): """ Map a Kodi request to certain action. @@ -260,7 +151,7 @@ class Plugin(object): Display playlists. """ - for playlist in self.walk_playlists(): + for playlist in self.connection.walk_playlists(): cover_art_url = self.connection.getCoverArtUrl( playlist["coverArt"]) url = self.build_url({ @@ -279,7 +170,7 @@ class Plugin(object): playlist_id = self.addon_args["playlist_id"][0] - for track in self.walk_playlist(playlist_id): + for track in self.connection.walk_playlist(playlist_id): self.add_track(track, show_artist=True) xbmcplugin.setContent(self.addon_handle, "songs") @@ -290,7 +181,7 @@ class Plugin(object): Display list of genres menu. """ - for genre in self.walk_genres(): + for genre in self.connection.walk_genres(): url = self.build_url({ "mode": "albums_by_genre_list", "foldername": genre["value"].encode("utf-8")}) @@ -308,7 +199,7 @@ class Plugin(object): genre = self.addon_args["foldername"][0].decode("utf-8") - for album in self.walk_album_list2_genre(genre): + for album in self.connection.walk_album_list_genre(genre): self.add_album(album, show_artist=True) xbmcplugin.setContent(self.addon_handle, "albums") @@ -319,7 +210,7 @@ class Plugin(object): Display artist list """ - for artist in self.walk_artists(): + for artist in self.connection.walk_artists(): cover_art_url = self.connection.getCoverArtUrl(artist["id"]) url = self.build_url({ "mode": "album_list", @@ -342,7 +233,7 @@ class Plugin(object): artist_id = self.addon_args["artist_id"][0] - for album in self.walk_artist(artist_id): + for album in self.connection.walk_artist(artist_id): self.add_album(album) xbmcplugin.setContent(self.addon_handle, "albums") @@ -355,7 +246,7 @@ class Plugin(object): album_id = self.addon_args["album_id"][0] - for track in self.walk_album(album_id): + for track in self.connection.walk_album(album_id): self.add_track(track) xbmcplugin.setContent(self.addon_handle, "songs") @@ -384,7 +275,7 @@ class Plugin(object): Display random genre list. """ - for genre in self.walk_genres(): + for genre in self.connection.walk_genres(): url = self.build_url({ "mode": "random_by_genre_track_list", "foldername": genre["value"].encode("utf-8")}) @@ -402,7 +293,7 @@ class Plugin(object): genre = self.addon_args["foldername"][0].decode("utf-8") - for track in self.walk_random_songs( + for track in self.connection.walk_random_songs( size=self.random_count, genre=genre): self.add_track(track, show_artist=True) @@ -419,7 +310,7 @@ class Plugin(object): to_year = xbmcgui.Dialog().input( "To year", type=xbmcgui.INPUT_NUMERIC) - for track in self.walk_random_songs( + for track in self.connection.walk_random_songs( size=self.random_count, from_year=from_year, to_year=to_year): self.add_track(track, show_artist=True) diff --git a/lib/libsonic_extra/__init__.py b/lib/libsonic_extra/__init__.py index 3b0b8fe..399931f 100644 --- a/lib/libsonic_extra/__init__.py +++ b/lib/libsonic_extra/__init__.py @@ -3,17 +3,6 @@ import urlparse import libsonic -def force_dict(value): - """ - Coerce the input value to a dict. - """ - - if type(value) == dict: - return value - else: - return {} - - def force_list(value): """ Coerce the input value to a list. @@ -34,22 +23,27 @@ def force_list(value): return [value] -class Connection(libsonic.Connection): +class SubsonicClient(libsonic.Connection): """ Extend `libsonic.Connection` with new features and fix a few issues. - - Add library name property. - Parse URL for host and port for constructor. - Make sure API results are of of uniform type. - - :param str name: Name of connection. - :param str url: Full URL (including protocol) of SubSonic server. - :param str username: Username of server. - :param str password: Password of server. + - Provide methods to intercept URL of binary requests. + - Add order property to playlist items. + - Add conventient `walk_*' methods to iterate over the API responses. """ def __init__(self, url, username, password): - self._intercept_url = False + """ + Construct a new SubsonicClient. + + :param str url: Full URL (including scheme) of the SubSonic server. + :param str username: Username of the server. + :param str password: Password of the server. + """ + + self.intercept_url = False # Parse SubSonic URL parts = urlparse.urlparse(url) @@ -69,10 +63,12 @@ class Connection(libsonic.Connection): port = parts.port or {"http": 80, "https": 443}[scheme] # Invoke original constructor - super(Connection, self).__init__(host, username, password, port=port) + super(SubsonicClient, self).__init__( + host, username, password, port=port) - def getArtists(self, *args, **kwargs): + def getIndexes(self, *args, **kwargs): """ + Improve the getIndexes method. Ensures IDs are integers. """ def _artists_iterator(artists): @@ -85,15 +81,33 @@ class Connection(libsonic.Connection): index["artist"] = list(_artists_iterator(index.get("artist"))) yield index - response = super(Connection, self).getArtists(*args, **kwargs) - response["artists"] = response.get("artists", {}) - response["artists"]["index"] = list( - _index_iterator(response["artists"].get("index"))) + def _children_iterator(children): + for child in force_list(children): + child["id"] = int(child["id"]) + + if "parent" in child: + child["parent"] = int(child["parent"]) + if "coverArt" in child: + child["coverArt"] = int(child["coverArt"]) + if "artistId" in child: + child["artistId"] = int(child["artistId"]) + if "albumId" in child: + child["albumId"] = int(child["albumId"]) + + yield child + + response = super(SubsonicClient, self).getIndexes(*args, **kwargs) + response["indexes"] = response.get("indexes", {}) + response["indexes"]["index"] = list( + _index_iterator(response["indexes"].get("index"))) + response["indexes"]["child"] = list( + _children_iterator(response["indexes"].get("child"))) return response def getPlaylists(self, *args, **kwargs): """ + Improve the getPlaylists method. Ensures IDs are integers. """ def _playlists_iterator(playlists): @@ -101,7 +115,7 @@ class Connection(libsonic.Connection): playlist["id"] = int(playlist["id"]) yield playlist - response = super(Connection, self).getPlaylists(*args, **kwargs) + response = super(SubsonicClient, self).getPlaylists(*args, **kwargs) response["playlists"]["playlist"] = list( _playlists_iterator(response["playlists"].get("playlist"))) @@ -109,66 +123,67 @@ class Connection(libsonic.Connection): def getPlaylist(self, *args, **kwargs): """ + Improve the getPlaylist method. Ensures IDs are integers and add an + order property to each entry. """ def _entries_iterator(entries): - for entry in force_list(entries): + for order, entry in enumerate(force_list(entries), start=1): entry["id"] = int(entry["id"]) + entry["order"] = order yield entry - response = super(Connection, self).getPlaylist(*args, **kwargs) + response = super(SubsonicClient, self).getPlaylist(*args, **kwargs) response["playlist"]["entry"] = list( _entries_iterator(response["playlist"].get("entry"))) return response + def getArtists(self, *args, **kwargs): + """ + Improve the getArtists method. Ensures IDs are integers. + """ + + def _artists_iterator(artists): + for artist in force_list(artists): + artist["id"] = int(artist["id"]) + yield artist + + def _index_iterator(index): + for index in force_list(index): + index["artist"] = list(_artists_iterator(index.get("artist"))) + yield index + + response = super(SubsonicClient, self).getArtists(*args, **kwargs) + response["artists"] = response.get("artists", {}) + response["artists"]["index"] = list( + _index_iterator(response["artists"].get("index"))) + + return response + def getArtist(self, *args, **kwargs): """ + Improve the getArtist method. Ensures IDs are integers. """ def _albums_iterator(albums): for album in force_list(albums): album["id"] = int(album["id"]) + + if "artistId" in album: + album["artistId"] = int(album["artistId"]) + yield album - response = super(Connection, self).getArtist(*args, **kwargs) + response = super(SubsonicClient, self).getArtist(*args, **kwargs) response["artist"]["album"] = list( _albums_iterator(response["artist"].get("album"))) return response - def getAlbum(self, *args, **kwargs): - "" - "" - - def _songs_iterator(songs): - for song in force_list(songs): - song["id"] = int(song["id"]) - yield song - - response = super(Connection, self).getAlbum(*args, **kwargs) - response["album"]["song"] = list( - _songs_iterator(response["album"].get("song"))) - - return response - - def getAlbumList2(self, *args, **kwargs): - "" - "" - - def _album_iterator(albums): - for album in force_list(albums): - album["id"] = int(album["id"]) - yield album - - response = super(Connection, self).getAlbumList2(*args, **kwargs) - response["albumList2"]["album"] = list( - _album_iterator(response["albumList2"].get("album"))) - - return response - def getMusicDirectory(self, *args, **kwargs): """ + Improve the getMusicDirectory method. Ensures IDs are integers. """ def _children_iterator(children): @@ -186,20 +201,53 @@ class Connection(libsonic.Connection): yield child - response = super(Connection, self).getMusicDirectory(*args, **kwargs) + response = super(SubsonicClient, self).getMusicDirectory( + *args, **kwargs) response["directory"]["child"] = list( _children_iterator(response["directory"].get("child"))) return response + def getAlbum(self, *args, **kwargs): + """ + Improve the getAlbum method. Ensures the IDs are real integers. + """ + + def _songs_iterator(songs): + for song in force_list(songs): + song["id"] = int(song["id"]) + yield song + + response = super(SubsonicClient, self).getAlbum(*args, **kwargs) + response["album"]["song"] = list( + _songs_iterator(response["album"].get("song"))) + + return response + + def getAlbumList2(self, *args, **kwargs): + """ + Improve the getAlbumList2 method. Ensures the IDs are real integers. + """ + + def _album_iterator(albums): + for album in force_list(albums): + album["id"] = int(album["id"]) + yield album + + response = super(SubsonicClient, self).getAlbumList2(*args, **kwargs) + response["albumList2"]["album"] = list( + _album_iterator(response["albumList2"].get("album"))) + + return response + def getCoverArtUrl(self, *args, **kwargs): """ Return an URL to the cover art. """ - self._intercept_url = True + self.intercept_url = True url = self.getCoverArt(*args, **kwargs) - self._intercept_url = False + self.intercept_url = False return url @@ -208,18 +256,21 @@ class Connection(libsonic.Connection): Return an URL to the file to stream. """ - self._intercept_url = True + self.intercept_url = True url = self.stream(*args, **kwargs) - self._intercept_url = False + self.intercept_url = False return url def _doBinReq(self, *args, **kwargs): """ - Intercept request URL. + Intercept request URL to provide the URL of the item that is requested. + + If the URL is intercepted, the request is not executed. A username and + password is added to provide direct access to the stream. """ - if self._intercept_url: + if self.intercept_url: parts = list(urlparse.urlparse( args[0].get_full_url() + "?" + args[0].data)) parts[4] = dict(urlparse.parse_qsl(parts[4])) @@ -228,4 +279,131 @@ class Connection(libsonic.Connection): return urlparse.urlunparse(parts) else: - return super(Connection, self)._doBinReq(*args, **kwargs) + return super(SubsonicClient, self)._doBinReq(*args, **kwargs) + + def walk_index(self): + """ + Request SubSonic's index and iterate each item. + """ + + response = self.getIndexes() + + for index in response["indexes"]["index"]: + for index in index["artist"]: + for item in self.walk_directory(index["id"]): + yield item + + for child in response["indexes"]["child"]: + if child.get("isDir"): + for child in self.walk_directory(child["id"]): + yield child + else: + yield child + + def walk_playlists(self): + """ + Request SubSonic's playlists and iterate over each item. + """ + + response = self.getPlaylists() + + for child in response["playlists"]["playlist"]: + yield child + + def walk_playlist(self, playlist_id): + """ + Request SubSonic's playlist items and iterate over each item. + """ + + response = self.getPlaylist(playlist_id) + + for order, child in response["playlist"]["entry"]: + yield child + + def walk_directory(self, directory_id): + """ + Request a SubSonic music directory and iterate over each item. + """ + + response = self.getMusicDirectory(directory_id) + + for child in response["directory"]["child"]: + if child.get("isDir"): + for child in self.walk_directory(child["id"]): + yield child + else: + yield child + + def walk_artist(self, artist_id): + """ + Request a SubSonic artist and iterate over each album. + """ + + response = self.getArtist(artist_id) + + for child in response["artist"]["album"]: + yield child + + def walk_artists(self): + """ + Request all artists and iterate over each item. + """ + + response = self.getArtists() + + for index in response["artists"]["index"]: + for artist in index["artist"]: + yield artist + + def walk_genres(self): + """ + Request all genres and iterate over each item. + """ + + response = self.getGenres() + + for genre in response["genres"]["genre"]: + yield genre + + def walk_album_list_genre(self, genre): + """ + Request all albums for a given genre and iterate over each album. + """ + + offset = 0 + + while True: + response = self.getAlbumList2( + ltype="byGenre", genre=genre, size=500, offset=offset) + + if not response["albumList2"]["album"]: + break + + for album in response["albumList2"]["album"]: + yield album + + offset += 500 + + def walk_album(self, album_id): + """ + Request an alum and iterate over each item. + """ + + response = self.getAlbum(album_id) + + for song in response["album"]["song"]: + yield song + + def walk_random_songs(self, size, genre=None, from_year=None, + to_year=None): + """ + Request random songs by genre and/or year and iterate over each song. + """ + + response = self.getRandomSongs( + size=size, genre=genre, fromYear=from_year, toYear=to_year) + + for song in response["randomSongs"]["song"]: + song["id"] = int(song["id"]) + + yield song