Added full browsing, playlist and search support.

This commit is contained in:
Frederick Gnodtke 2016-09-18 18:19:08 +02:00
parent a73edeae8e
commit d8cbb503a5
6 changed files with 137 additions and 70 deletions

View file

@ -12,5 +12,5 @@ class SubidyBackend(pykka.ThreadingActor, backend.Backend):
password=subidy_config['password']) password=subidy_config['password'])
self.library = library.SubidyLibraryProvider(backend=self) self.library = library.SubidyLibraryProvider(backend=self)
self.playback = playback.SubidyPlaybackProvider(audio=audio, 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'] self.uri_schemes = ['subidy']

View file

@ -1,8 +1,12 @@
from mopidy import backend, models from mopidy import backend, models
from mopidy.models import Ref, SearchResult
from mopidy_subidy import uri from mopidy_subidy import uri
import logging
logger = logging.getLogger(__name__)
class SubidyLibraryProvider(backend.LibraryProvider): 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): def __init__(self, *args, **kwargs):
super(SubidyLibraryProvider, self).__init__(*args, **kwargs) super(SubidyLibraryProvider, self).__init__(*args, **kwargs)
@ -18,13 +22,13 @@ class SubidyLibraryProvider(backend.LibraryProvider):
return self.subsonic_api.get_artists_as_refs() return self.subsonic_api.get_artists_as_refs()
def lookup_song(self, song_id): 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): 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): 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): def browse(self, browse_uri):
type = uri.get_type(browse_uri) type = uri.get_type(browse_uri)
@ -53,5 +57,25 @@ class SubidyLibraryProvider(backend.LibraryProvider):
def refresh(self, uri): def refresh(self, uri):
pass 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): 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])

View file

@ -1,12 +1,18 @@
from mopidy import backend from mopidy import backend
from mopidy_subidy import uri
import logging
logger = logging.getLogger(__name__)
class SubidyPlaylistsProvider(backend.PlaylistsProvider): class SubidyPlaylistsProvider(backend.PlaylistsProvider):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SubidyPlaylistsProvider, self).__init__(*args, **kwargs) super(SubidyPlaylistsProvider, self).__init__(*args, **kwargs)
self.subsonic_api = self.backend.subsonic_api
self.playlists = [] self.playlists = []
self.refresh()
def as_list(self): def as_list(self):
pass return self.playlists
def create(self, name): def create(self, name):
pass pass
@ -14,14 +20,16 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider):
def delete(self, uri): def delete(self, uri):
pass pass
def get_items(self, uri): def get_items(self, items_uri):
pass #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): def lookup(self, lookup_uri):
pass #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): def refresh(self):
pass self.playlists = self.subsonic_api.get_playlists_as_refs()
def save(self, playlist): def save(self, playlist):
pass pass

View file

@ -2,7 +2,7 @@ from urlparse import urlparse
import libsonic import libsonic
import logging import logging
import itertools 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 from mopidy_subidy import uri
logger = logging.getLogger(__name__) 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' 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) return template % (self.url, song_id, self.username, self.password)
def find_artists_by_name(self, artist_name): def find_raw(self, query, exclude_artists=False, exclude_albums=False, exclude_songs=False):
response = self.connection.search3(artist_name, MAX_SEARCH_RESULTS, 0, 0, 0, 0, 0) 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: if response.get('status') != RESPONSE_OK:
return None return None
artists = response.get('searchResult3').get('artist') return response.get('searchResult2')
if artists is not None:
return [self.raw_artist_to_artist(artist) for artist in artists]
return None
def find_tracks_by_name(self, track_name): def find_as_search_result(self, query, exclude_artists=False, exclude_albums=False, exclude_songs=False):
response = self.connection.search3(track_name, 0, 0, 0, 0, MAX_SEARCH_RESULTS, 0) result = self.find_raw(query)
if response.get('status') != RESPONSE_OK: return SearchResult(
return None uri=uri.get_search_uri(query),
tracks = response.get('searchResult3').get('song') artists=[self.raw_artist_to_artist(artist) for artist in result.get('artist') or []],
if tracks is not None: albums=[self.raw_album_to_album(album) for album in result.get('album') or []],
return [self.raw_song_to_track(track) for track in tracks] tracks=[self.raw_song_to_track(song) for song in result.get('song') or []])
return None
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): def get_raw_artists(self):
response = self.connection.getIndexes() response = self.connection.getIndexes()
@ -74,27 +66,40 @@ class SubsonicApi():
return None return None
letters = response.get('indexes').get('index') letters = response.get('indexes').get('index')
if letters is not None: 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 return None
def find_song_by_id(self, song_id): def get_song_by_id(self, song_id):
response = self.connection.getSong(song_id) response = self.connection.getSong(song_id)
if response.get('status') != RESPONSE_OK: if response.get('status') != RESPONSE_OK:
return None return None
return self.raw_song_to_track(response.get('song')) if response.get('song') is not None else 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) response = self.connection.getAlbum(album_id)
if response.get('status') != RESPONSE_OK: if response.get('status') != RESPONSE_OK:
return None return None
return self.raw_album_to_album(response.get('album')) if response.get('album') is not None else 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) response = self.connection.getArtist(artist_id)
if response.get('status') != RESPONSE_OK: if response.get('status') != RESPONSE_OK:
return None return None
return self.raw_artist_to_artist(response.get('artist')) if response.get('artist') is not None else 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): def get_raw_dir(self, parent_id):
response = self.connection.getMusicDirectory(parent_id) response = self.connection.getMusicDirectory(parent_id)
if response.get('status') != RESPONSE_OK: if response.get('status') != RESPONSE_OK:
@ -111,34 +116,42 @@ class SubsonicApi():
return self.get_raw_dir(album_id) return self.get_raw_dir(album_id)
def get_albums_as_refs(self, artist_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): 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): 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): 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): 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): 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): def raw_song_to_ref(self, song):
return Ref( return Ref.track(
name=song.get('title') or UNKNOWN_SONG, name=song.get('title') or UNKNOWN_SONG,
uri=uri.get_song_uri(song.get('id')), uri=uri.get_song_uri(song.get('id')))
type=Ref.TRACK)
def raw_song_to_track(self, song): 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( return Track(
name=song.get('title') or UNKNOWN_SONG, name=song.get('title') or UNKNOWN_SONG,
uri=uri.get_song_uri(song.get('id')), uri=uri.get_song_uri(song.get('id')),
@ -148,32 +161,44 @@ class SubsonicApi():
genre=song.get('genre'), genre=song.get('genre'),
length=int(song.get('duration')) * 1000 if song.get('duration') else None, length=int(song.get('duration')) * 1000 if song.get('duration') else None,
disc_no=int(song.get('discNumber')) if song.get('discNumber') else None, disc_no=int(song.get('discNumber')) if song.get('discNumber') else None,
artists=[artist] if artist is not None else None, artists=[Artist(
album=album 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): def raw_album_to_ref(self, album):
return Ref( return Ref.album(
name=album.get('title') or album.get('name') or UNKNOWN_ALBUM, name=album.get('title') or album.get('name') or UNKNOWN_ALBUM,
uri=uri.get_album_uri(album.get('id')), uri=uri.get_album_uri(album.get('id')))
type=Ref.ALBUM)
def raw_album_to_album(self, album): 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( return Album(
name=album.get('title') or album.get('name') or UNKNOWN_ALBUM, name=album.get('title') or album.get('name') or UNKNOWN_ALBUM,
uri=uri.get_album_uri(album.get('id')), 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): def raw_artist_to_ref(self, artist):
return Ref( return Ref.artist(
name=artist.get('name') or UNKNOWN_ARTIST, name=artist.get('name') or UNKNOWN_ARTIST,
uri=uri.get_artist_uri(artist.get('id')), uri=uri.get_artist_uri(artist.get('id')))
type=Ref.ARTIST)
def raw_artist_to_artist(self, artist): def raw_artist_to_artist(self, artist):
return Artist( return Artist(
name=artist.get('name') or UNKNOWN_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'))

View file

@ -6,6 +6,7 @@ PLAYLIST = 'playlist'
ALBUM = 'album' ALBUM = 'album'
PREFIX = 'subidy' PREFIX = 'subidy'
ROOT = 'root' ROOT = 'root'
SEARCH = 'search'
ROOT_URI = '%s:%s' % (PREFIX, ROOT) ROOT_URI = '%s:%s' % (PREFIX, ROOT)
@ -17,6 +18,9 @@ def is_type_result_valid(result):
def is_id_result_valid(result, type): def is_id_result_valid(result, type):
return is_type_result_valid(result) and result.group(1) == PREFIX and result.group(2) == 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): def get_song_id(uri):
result = regex.match(uri) result = regex.match(uri)
if not is_id_result_valid(result, SONG): if not is_id_result_valid(result, SONG):
@ -58,3 +62,9 @@ def get_album_uri(id):
def get_song_uri(id): def get_song_uri(id):
return get_type_uri(SONG, 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)

View file

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re import re
from setuptools import setup, find_packages from setuptools import setup, get_packages
def get_version(filename): def get_version(filename):
@ -18,7 +18,7 @@ setup(
author_email='fgnodtke@cronosx.de', author_email='fgnodtke@cronosx.de',
description='Improved Subsonic extension for Mopidy', description='Improved Subsonic extension for Mopidy',
long_description=open('README.md').read(), long_description=open('README.md').read(),
packages=find_packages(exclude=['tests', 'tests.*']), packages=get_packages(exclude=['tests', 'tests.*']),
zip_safe=False, zip_safe=False,
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[