From d8cbb503a579b6be2f79cbc0d0ec11fe6424a06a Mon Sep 17 00:00:00 2001 From: Frederick Gnodtke Date: Sun, 18 Sep 2016 18:19:08 +0200 Subject: [PATCH] Added full browsing, playlist and search support. --- mopidy_subidy/backend.py | 2 +- mopidy_subidy/library.py | 36 +++++++-- mopidy_subidy/playlists.py | 20 +++-- mopidy_subidy/subsonic_api.py | 135 ++++++++++++++++++++-------------- mopidy_subidy/uri.py | 10 +++ setup.py | 4 +- 6 files changed, 137 insertions(+), 70 deletions(-) diff --git a/mopidy_subidy/backend.py b/mopidy_subidy/backend.py index 7f39d98..7b791aa 100644 --- a/mopidy_subidy/backend.py +++ b/mopidy_subidy/backend.py @@ -12,5 +12,5 @@ class SubidyBackend(pykka.ThreadingActor, backend.Backend): password=subidy_config['password']) self.library = library.SubidyLibraryProvider(backend=self) self.playback = playback.SubidyPlaybackProvider(audio=audio, backend=self) - #self.playlists = playlists.SubidyPlaylistsProvider(backend=self) + self.playlists = playlists.SubidyPlaylistsProvider(backend=self) self.uri_schemes = ['subidy'] diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 4abbf17..b5943a9 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -1,8 +1,12 @@ from mopidy import backend, models +from mopidy.models import Ref, SearchResult from mopidy_subidy import uri +import logging +logger = logging.getLogger(__name__) + class SubidyLibraryProvider(backend.LibraryProvider): - root_directory = models.Ref(uri=uri.ROOT_URI, type=models.Ref.DIRECTORY, name='Subsonic') + root_directory = Ref.directory(uri=uri.ROOT_URI, name='Subsonic') def __init__(self, *args, **kwargs): super(SubidyLibraryProvider, self).__init__(*args, **kwargs) @@ -18,13 +22,13 @@ class SubidyLibraryProvider(backend.LibraryProvider): return self.subsonic_api.get_artists_as_refs() def lookup_song(self, song_id): - return self.subsonic_api.find_song_by_id(song_id) + return self.subsonic_api.get_song_by_id(song_id) def lookup_album(self, album_id): - return self.subsonic_api.find_album_by_id(album_id) + return self.subsonic_api.get_album_by_id(album_id) def lookup_artist(self, artist_id): - return self.subsonic_api.find_artist_by_id(artist_id) + return self.subsonic_api.get_artist_by_id(artist_id) def browse(self, browse_uri): type = uri.get_type(browse_uri) @@ -34,7 +38,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): return self.browse_albums(uri.get_artist_id(browse_uri)) if type == uri.ALBUM: return self.browse_songs(uri.get_album_id(browse_uri)) - + def lookup_one(self, lookup_uri): type = uri.get_type(lookup_uri) if type == uri.ARTIST: @@ -53,5 +57,25 @@ class SubidyLibraryProvider(backend.LibraryProvider): def refresh(self, uri): pass + + def search_uri(self, query): + type = uri.get_type(lookup_uri) + if type == uri.ARTIST: + artist = self.lookup_artist(uri.get_artist_id(lookup_uri)) + if artist is not None: + return SearchResult(artists=[artist]) + elif type == uri.ALBUM: + album = self.lookup_album(uri.get_album_id(lookup_uri)) + if album is not None: + return SearchResult(albums=[album]) + elif type == uri.SONG: + song = self.lookup_song(uri.get_song_id(lookup_uri)) + if song is not None: + return SearchResult(tracks=[song]) + return None + def search(self, query=None, uris=None, exact=False): - pass + if 'uri' in query: + return self.search_uri(query.get('uri')[0]) + if 'any' in query: + return self.subsonic_api.find_as_search_result(query.get('any')[0]) diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index f5adf8a..5cc97f1 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -1,12 +1,18 @@ from mopidy import backend +from mopidy_subidy import uri + +import logging +logger = logging.getLogger(__name__) class SubidyPlaylistsProvider(backend.PlaylistsProvider): def __init__(self, *args, **kwargs): super(SubidyPlaylistsProvider, self).__init__(*args, **kwargs) + self.subsonic_api = self.backend.subsonic_api self.playlists = [] + self.refresh() def as_list(self): - pass + return self.playlists def create(self, name): pass @@ -14,14 +20,16 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): def delete(self, uri): pass - def get_items(self, uri): - pass + def get_items(self, items_uri): + #logger.info('ITEMS %s: %s' % (lookup_uri, self.subsonic_api.get_playlist_songs_as_refs(uri.get_playlist_id(items_uri)))) + return self.subsonic_api.get_playlist_songs_as_refs(uri.get_playlist_id(items_uri)) - def lookup(self, uri): - pass + def lookup(self, lookup_uri): + #logger.info('LOOKUP PLAYLIST %s: %s' % (lookup_uri, self.subsonic_api.get_playlist_as_playlist(uri.get_playlist_id(lookup_uri)))) + return self.subsonic_api.get_playlist_as_playlist(uri.get_playlist_id(lookup_uri)) def refresh(self): - pass + self.playlists = self.subsonic_api.get_playlists_as_refs() def save(self, playlist): pass diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index e3fe8ba..9102127 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -2,7 +2,7 @@ from urlparse import urlparse import libsonic import logging import itertools -from mopidy.models import Track, Album, Artist, Playlist, Ref +from mopidy.models import Track, Album, Artist, Playlist, Ref, SearchResult from mopidy_subidy import uri logger = logging.getLogger(__name__) @@ -41,32 +41,24 @@ class SubsonicApi(): template = '%s/stream.view?id=%s&u=%s&p=%s&c=mopidy&v=1.14' return template % (self.url, song_id, self.username, self.password) - def find_artists_by_name(self, artist_name): - response = self.connection.search3(artist_name, MAX_SEARCH_RESULTS, 0, 0, 0, 0, 0) + def find_raw(self, query, exclude_artists=False, exclude_albums=False, exclude_songs=False): + response = self.connection.search2( + query.encode('utf-8'), + MAX_SEARCH_RESULTS if not exclude_artists else 0, 0, + MAX_SEARCH_RESULTS if not exclude_albums else 0, 0, + MAX_SEARCH_RESULTS if not exclude_songs else 0, 0) if response.get('status') != RESPONSE_OK: return None - artists = response.get('searchResult3').get('artist') - if artists is not None: - return [self.raw_artist_to_artist(artist) for artist in artists] - return None + return response.get('searchResult2') - def find_tracks_by_name(self, track_name): - response = self.connection.search3(track_name, 0, 0, 0, 0, MAX_SEARCH_RESULTS, 0) - if response.get('status') != RESPONSE_OK: - return None - tracks = response.get('searchResult3').get('song') - if tracks is not None: - return [self.raw_song_to_track(track) for track in tracks] - return None + def find_as_search_result(self, query, exclude_artists=False, exclude_albums=False, exclude_songs=False): + result = self.find_raw(query) + return SearchResult( + uri=uri.get_search_uri(query), + artists=[self.raw_artist_to_artist(artist) for artist in result.get('artist') or []], + 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 find_albums_by_name(self, album_name): - response = self.connection.search3(album_name, 0, 0, MAX_SEARCH_RESULTS, 0, 0, 0) - if response.get('status') != RESPONSE_OK: - return None - albums = response.get('searchResult3').get('album') - if albums is not None: - return [self.raw_album_to_album(album) for album in albums] - return None def get_raw_artists(self): response = self.connection.getIndexes() @@ -74,27 +66,40 @@ class SubsonicApi(): return None letters = response.get('indexes').get('index') if letters is not None: - return [artist for letter in letters for artist in letter.get('artist')] + artists = [artist for letter in letters for artist in letter.get('artist')] + return artists return None - def find_song_by_id(self, song_id): + def get_song_by_id(self, song_id): response = self.connection.getSong(song_id) if response.get('status') != RESPONSE_OK: return None return self.raw_song_to_track(response.get('song')) if response.get('song') is not None else None - def find_album_by_id(self, album_id): + def get_album_by_id(self, album_id): response = self.connection.getAlbum(album_id) if response.get('status') != RESPONSE_OK: return None return self.raw_album_to_album(response.get('album')) if response.get('album') is not None else None - def find_artist_by_id(self, artist_id): + def get_artist_by_id(self, artist_id): response = self.connection.getArtist(artist_id) if response.get('status') != RESPONSE_OK: return None return self.raw_artist_to_artist(response.get('artist')) if response.get('artist') is not None else None + def get_raw_playlists(self): + response = self.connection.getPlaylists() + if response.get('status') != RESPONSE_OK: + return None + return response.get('playlists').get('playlist') + + def get_raw_playlist(self, playlist_id): + response = self.connection.getPlaylist(playlist_id) + if response.get('status') != RESPONSE_OK: + return None + return response.get('playlist') + def get_raw_dir(self, parent_id): response = self.connection.getMusicDirectory(parent_id) if response.get('status') != RESPONSE_OK: @@ -111,34 +116,42 @@ class SubsonicApi(): return self.get_raw_dir(album_id) def get_albums_as_refs(self, artist_id): - return sorted([self.raw_album_to_ref(album) for album in self.get_raw_albums(artist_id)], key=ref_sort_key) + return [self.raw_album_to_ref(album) for album in self.get_raw_albums(artist_id)] def get_albums_as_albums(self, artist_id): - return sorted([self.raw_album_to_album(album) for album in self.get_raw_albums(artist_id)], key=ref_sort_key) + return [self.raw_album_to_album(album) for album in self.get_raw_albums(artist_id)] def get_songs_as_refs(self, album_id): - return sorted([self.raw_song_to_ref(song) for song in self.get_raw_songs(album_id)], key=ref_sort_key) + return [self.raw_song_to_ref(song) for song in self.get_raw_songs(album_id)] def get_songs_as_tracks(self, album_id): - return sorted([self.raw_song_to_track(song) for song in self.get_raw_songs(album_id)], key=ref_sort_key) + return [self.raw_song_to_track(song) for song in self.get_raw_songs(album_id)] def get_artists_as_refs(self): - return sorted([self.raw_artist_to_ref(artist) for artist in self.get_raw_artists()], key=ref_sort_key) + return [self.raw_artist_to_ref(artist) for artist in self.get_raw_artists()] def get_artists_as_artists(self): - return sorted([self.raw_artist_to_artist(artist) for artist in self.get_raw_artists()], key=lambda artist:artist.name) + return [self.raw_artist_to_artist(artist) for artist in self.get_raw_artists()] + + def get_playlists_as_refs(self): + return [self.raw_playlist_to_ref(playlist) for playlist in self.get_raw_playlists()] + + def get_playlists_as_playlists(self): + return [self.raw_playlist_to_playlist(playlist) for playlist in self.get_raw_playlists()] + + def get_playlist_as_playlist(self, playlist_id): + return self.raw_playlist_to_playlist(self.get_raw_playlist(playlist_id)) + + def get_playlist_as_songs_as_refs(self, playlist_id): + playlist = self.get_raw_playlist(playlist_id) + return [self.raw_song_to_ref(song) for song in playlist.get('entry')] def raw_song_to_ref(self, song): - return Ref( + return Ref.track( name=song.get('title') or UNKNOWN_SONG, - uri=uri.get_song_uri(song.get('id')), - type=Ref.TRACK) + uri=uri.get_song_uri(song.get('id'))) def raw_song_to_track(self, song): - album_name = song.get('album') - album = self.find_albums_by_name(album_name)[0] if album_name is not None else None - artist_name = song.get('artist') - artist = self.find_artists_by_name(artist_name)[0] if artist_name is not None else None return Track( name=song.get('title') or UNKNOWN_SONG, uri=uri.get_song_uri(song.get('id')), @@ -148,32 +161,44 @@ class SubsonicApi(): genre=song.get('genre'), length=int(song.get('duration')) * 1000 if song.get('duration') else None, disc_no=int(song.get('discNumber')) if song.get('discNumber') else None, - artists=[artist] if artist is not None else None, - album=album - ) + artists=[Artist( + name=song.get('artist'), + uri=uri.get_artist_uri(song.get('artistId')))], + album=Album( + name=song.get('album'), + uri=uri.get_album_uri('albumId'))) def raw_album_to_ref(self, album): - return Ref( + return Ref.album( name=album.get('title') or album.get('name') or UNKNOWN_ALBUM, - uri=uri.get_album_uri(album.get('id')), - type=Ref.ALBUM) + uri=uri.get_album_uri(album.get('id'))) def raw_album_to_album(self, album): - artist_name = album.get('artist') - artist = self.find_artists_by_name(artist_name)[0] if artist_name is not None else None return Album( name=album.get('title') or album.get('name') or UNKNOWN_ALBUM, uri=uri.get_album_uri(album.get('id')), - artists=[artist] - ) + artists=[Artist( + name=album.get('artist'), + uri=uri.get_artist_uri(album.get('artistId')))]) def raw_artist_to_ref(self, artist): - return Ref( + return Ref.artist( name=artist.get('name') or UNKNOWN_ARTIST, - uri=uri.get_artist_uri(artist.get('id')), - type=Ref.ARTIST) + uri=uri.get_artist_uri(artist.get('id'))) def raw_artist_to_artist(self, artist): return Artist( name=artist.get('name') or UNKNOWN_ARTIST, - uri=uri.get_artist_uri(artist.get('id')) - ) + uri=uri.get_artist_uri(artist.get('id'))) + + def raw_playlist_to_playlist(self, playlist): + entries = playlist.get('entry') + tracks = [self.raw_song_to_track(song) for song in entries] if entries is not None else None + return Playlist( + uri=uri.get_playlist_uri(playlist.get('id')), + name=playlist.get('name'), + tracks=tracks) + + def raw_playlist_to_ref(self, playlist): + return Ref.playlist( + uri=uri.get_playlist_uri(playlist.get('id')), + name=playlist.get('name')) diff --git a/mopidy_subidy/uri.py b/mopidy_subidy/uri.py index 4b5a169..2ff792f 100644 --- a/mopidy_subidy/uri.py +++ b/mopidy_subidy/uri.py @@ -6,6 +6,7 @@ PLAYLIST = 'playlist' ALBUM = 'album' PREFIX = 'subidy' ROOT = 'root' +SEARCH = 'search' ROOT_URI = '%s:%s' % (PREFIX, ROOT) @@ -17,6 +18,9 @@ def is_type_result_valid(result): def is_id_result_valid(result, type): return is_type_result_valid(result) and result.group(1) == PREFIX and result.group(2) == type +def is_uri(uri): + return regex.match(uri) is not None + def get_song_id(uri): result = regex.match(uri) if not is_id_result_valid(result, SONG): @@ -58,3 +62,9 @@ def get_album_uri(id): def get_song_uri(id): return get_type_uri(SONG, id) + +def get_playlist_uri(id): + return get_type_uri(PLAYLIST, id) + +def get_search_uri(query): + return get_type_uri(SEARCH, query) diff --git a/setup.py b/setup.py index f7c71f7..882faf2 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import re -from setuptools import setup, find_packages +from setuptools import setup, get_packages def get_version(filename): @@ -18,7 +18,7 @@ setup( author_email='fgnodtke@cronosx.de', description='Improved Subsonic extension for Mopidy', long_description=open('README.md').read(), - packages=find_packages(exclude=['tests', 'tests.*']), + packages=get_packages(exclude=['tests', 'tests.*']), zip_safe=False, include_package_data=True, install_requires=[