diff --git a/README.md b/README.md index 31505a8..791da2c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ The following things are **not** supported: * Creating, editing and deleting playlists * Subsonics smart playlists * Searching for a combination of filters (artist and album, artist and track, etc.) - * Browsing more than 2 levels deep in the Subsonic directory tree ## Contributors diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index f7460bb..cd29d53 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -6,21 +6,55 @@ import logging logger = logging.getLogger(__name__) class SubidyLibraryProvider(backend.LibraryProvider): - root_directory = Ref.directory(uri=uri.ROOT_URI, name='Subsonic') + def __create_vdirs(): + vdir_templates = [ + dict(id="root", name="Subsonic"), + dict(id="artists", name="Artists"), + dict(id="albums", name="Albums"), + dict(id="rootdirs", name="Directories"), + ] + # Create a dict with the keys being the `id`s in `vdir_templates` + # and the values being objects containing the vdir `id`, + # the human readable name as `name`, and the URI as `uri`. + vdirs = {} + for template in vdir_templates: + vdir = template.copy() + vdir.update(uri=uri.get_vdir_uri(vdir["id"])) + vdirs[template['id']] = vdir + return vdirs + + _vdirs = __create_vdirs() + + def __raw_vdir_to_ref(vdir): + if vdir is None: + return None + return Ref.directory( + name=vdir['name'], + uri=vdir['uri']) + + root_directory = __raw_vdir_to_ref(_vdirs['root']) + + _raw_vdir_to_ref = staticmethod(__raw_vdir_to_ref) def __init__(self, *args, **kwargs): super(SubidyLibraryProvider, self).__init__(*args, **kwargs) self.subsonic_api = self.backend.subsonic_api - def browse_songs(self,album_id): + def browse_songs(self, album_id): return self.subsonic_api.get_songs_as_refs(album_id) - def browse_albums(self, artist_id): + def browse_albums(self, artist_id=None): return self.subsonic_api.get_albums_as_refs(artist_id) def browse_artists(self): return self.subsonic_api.get_artists_as_refs() + def browse_rootdirs(self): + return self.subsonic_api.get_rootdirs_as_refs() + + def browse_diritems(self, directory_id): + return self.subsonic_api.get_diritems_as_refs(directory_id) + def lookup_song(self, song_id): return self.subsonic_api.get_song_by_id(song_id) @@ -31,13 +65,27 @@ class SubidyLibraryProvider(backend.LibraryProvider): return self.subsonic_api.get_artist_by_id(artist_id) def browse(self, browse_uri): - type = uri.get_type(browse_uri) - if browse_uri == uri.ROOT_URI: + if browse_uri == uri.get_vdir_uri('root'): + root_vdir_names = ["rootdirs", "artists", "albums"] + root_vdirs = [self._vdirs[vdir_name] for vdir_name in root_vdir_names] + sorted_root_vdirs = sorted(root_vdirs, key=lambda vdir: vdir["name"]) + return [self._raw_vdir_to_ref(vdir) for vdir in sorted_root_vdirs] + elif browse_uri == uri.get_vdir_uri("rootdirs"): + return self.browse_rootdirs() + elif browse_uri == uri.get_vdir_uri("artists"): return self.browse_artists() - if type == uri.ARTIST: - return self.browse_albums(uri.get_artist_id(browse_uri)) - if type == uri.ALBUM: - return self.browse_songs(uri.get_album_id(browse_uri)) + elif browse_uri == uri.get_vdir_uri("albums"): + return self.browse_albums() + else: + uri_type = uri.get_type(browse_uri) + if uri_type == uri.DIRECTORY: + return self.browse_diritems(uri.get_directory_id(browse_uri)) + elif uri_type == uri.ARTIST: + return self.browse_albums(uri.get_artist_id(browse_uri)) + elif uri_type == uri.ALBUM: + return self.browse_songs(uri.get_album_id(browse_uri)) + else: + return [] def lookup_one(self, lookup_uri): type = uri.get_type(lookup_uri) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index c9f1a46..a7528cc 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -14,6 +14,7 @@ UNKNOWN_SONG = u'Unknown Song' UNKNOWN_ALBUM = u'Unknown Album' UNKNOWN_ARTIST = u'Unknown Artist' MAX_SEARCH_RESULTS = 100 +MAX_LIST_RESULTS = 500 ref_sort_key = lambda ref: ref.name @@ -32,7 +33,7 @@ def diritem_sort_key(item): if isdir: key = string_nums_nocase_sort_key(item['title']) else: - key = int(item['track']) + key = int(item.get('track', 1)) return (isdir, key) class SubsonicApi(): @@ -100,12 +101,27 @@ class SubsonicApi(): albums=[self.raw_album_to_album(album) for album in result.get('album') or []], tracks=[self.raw_song_to_track(song) for song in result.get('song') or []]) - def get_raw_artists(self): + try: + response = self.connection.getArtists() + except Exception as e: + logger.warning('Connecting to subsonic failed when loading list of artists.') + return [] + if response.get('status') != RESPONSE_OK: + logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + return [] + letters = response.get('artists').get('index') + if letters is not None: + artists = [artist for letter in letters for artist in letter.get('artist') or []] + return artists + logger.warning('Subsonic does not seem to have any artists in it\'s library.') + return [] + + def get_raw_rootdirs(self): try: response = self.connection.getIndexes() except Exception as e: - logger.warning('Connecting to subsonic failed when loading list of artists.') + logger.warning('Connecting to subsonic failed when loading list of rootdirs.') return [] if response.get('status') != RESPONSE_OK: logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) @@ -114,7 +130,7 @@ class SubsonicApi(): if letters is not None: artists = [artist for letter in letters for artist in letter.get('artist') or []] return artists - logger.warning('Subsonic does not seem to have any artists in it\'s library.') + logger.warning('Subsonic does not seem to have any rootdirs in its library.') return [] def get_song_by_id(self, song_id): @@ -192,13 +208,50 @@ class SubsonicApi(): return None def get_raw_albums(self, artist_id): - return self.get_raw_dir(artist_id) + try: + response = self.connection.getArtist(artist_id) + except Exception as e: + logger.warning('Connecting to subsonic failed when loading list of albums.') + return [] + if response.get('status') != RESPONSE_OK: + logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + return [] + albums = response.get('artist').get('album') + if albums is not None: + return sorted(albums, key=lambda album: string_nums_nocase_sort_key(album['name'])) + return [] def get_raw_songs(self, album_id): - return self.get_raw_dir(album_id) + try: + response = self.connection.getAlbum(album_id) + except Exception as e: + logger.warning('Connecting to subsonic failed when loading list of songs in album.') + return [] + if response.get('status') != RESPONSE_OK: + logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + return [] + songs = response.get('album').get('song') + if songs is not None: + return songs + return [] - def get_albums_as_refs(self, artist_id): - return [self.raw_album_to_ref(album) for album in self.get_raw_albums(artist_id)] + def get_raw_album_list(self, ltype, size=MAX_LIST_RESULTS): + try: + response = self.connection.getAlbumList2(ltype=ltype, size=size) + except Exception as e: + logger.warning('Connecting to subsonic failed when loading album list.') + return [] + if response.get('status') != RESPONSE_OK: + logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + return [] + albums = response.get('albumList2').get('album') + if albums is not None: + return albums + return [] + + def get_albums_as_refs(self, artist_id=None): + albums = (self.get_raw_album_list('alphabeticalByName') if artist_id is None else self.get_raw_albums(artist_id)) + return [self.raw_album_to_ref(album) for album in albums] def get_albums_as_albums(self, artist_id): return [self.raw_album_to_album(album) for album in self.get_raw_albums(artist_id)] @@ -212,6 +265,12 @@ class SubsonicApi(): def get_artists_as_refs(self): return [self.raw_artist_to_ref(artist) for artist in self.get_raw_artists()] + def get_rootdirs_as_refs(self): + return [self.raw_directory_to_ref(rootdir) for rootdir in self.get_raw_rootdirs()] + + def get_diritems_as_refs(self, directory_id): + return [(self.raw_directory_to_ref(diritem) if diritem.get('isDir') else self.raw_song_to_ref(diritem)) for diritem in self.get_raw_dir(directory_id)] + def get_artists_as_artists(self): return [self.raw_artist_to_artist(artist) for artist in self.get_raw_artists()] @@ -273,6 +332,13 @@ class SubsonicApi(): name=album.get('artist'), uri=uri.get_artist_uri(album.get('artistId')))]) + def raw_directory_to_ref(self, directory): + if directory is None: + return None + return Ref.directory( + name=directory.get('title') or directory.get('name'), + uri=uri.get_directory_uri(directory.get('id'))) + def raw_artist_to_ref(self, artist): if artist is None: return None diff --git a/mopidy_subidy/uri.py b/mopidy_subidy/uri.py index 2ff792f..613f958 100644 --- a/mopidy_subidy/uri.py +++ b/mopidy_subidy/uri.py @@ -4,12 +4,11 @@ SONG = 'song' ARTIST = 'artist' PLAYLIST = 'playlist' ALBUM = 'album' +DIRECTORY = 'directory' +VDIR = 'vdir' PREFIX = 'subidy' -ROOT = 'root' SEARCH = 'search' -ROOT_URI = '%s:%s' % (PREFIX, ROOT) - regex = re.compile(r'(\w+?):(\w+?)(?::|$)(.+?)?$') def is_type_result_valid(result): @@ -45,6 +44,18 @@ def get_album_id(uri): return None return result.group(3) +def get_directory_id(uri): + result = regex.match(uri) + if not is_id_result_valid(result, DIRECTORY): + return None + return result.group(3) + +def get_vdir_id(uri): + result = regex.match(uri) + if not is_id_result_valid(result, VDIR): + return None + return result.group(3) + def get_type(uri): result = regex.match(uri) if not is_type_result_valid(result): @@ -63,6 +74,12 @@ def get_album_uri(id): def get_song_uri(id): return get_type_uri(SONG, id) +def get_directory_uri(id): + return get_type_uri(DIRECTORY, id) + +def get_vdir_uri(id): + return get_type_uri(VDIR, id) + def get_playlist_uri(id): return get_type_uri(PLAYLIST, id)