From: marcelveldt Date: Thu, 31 Oct 2019 01:20:24 +0000 (+0100) Subject: fixes for the refactored frontend X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=3c01aae072ec1f8109049762ce14f65ca8564b89;p=music-assistant-server.git fixes for the refactored frontend --- diff --git a/music_assistant.code-workspace b/music_assistant.code-workspace index 362d7c25..1f11899d 100644 --- a/music_assistant.code-workspace +++ b/music_assistant.code-workspace @@ -2,6 +2,9 @@ "folders": [ { "path": "." + }, + { + "path": "/Users/marcelvanderveldt/Workdir/test" } ] } \ No newline at end of file diff --git a/music_assistant/database.py b/music_assistant/database.py index ebff6bed..15782a50 100755 --- a/music_assistant/database.py +++ b/music_assistant/database.py @@ -274,7 +274,7 @@ class Database(): artist.external_ids = await self.__get_external_ids(artist.item_id, MediaType.Artist, db) artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist, db) artist.tags = await self.__get_tags(artist.item_id, MediaType.Artist, db) - artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist, db, filter_key='image') + artist.metadata = await self.__get_metadata(artist.item_id, MediaType.Artist, db) artists.append(artist) if should_close_db: await db.close() @@ -354,8 +354,6 @@ class Database(): album.metadata = await self.__get_metadata(album.item_id, MediaType.Album, db) album.tags = await self.__get_tags(album.item_id, MediaType.Album, db) album.labels = await self.__get_album_labels(album.item_id, db) - else: - album.metadata = await self.__get_metadata(album.item_id, MediaType.Album, db, filter_key='image') albums.append(album) if should_close_db: await db.close() @@ -512,6 +510,12 @@ class Database(): sql_query = ' WHERE artist_id = %d' % artist_id return await self.albums(sql_query, limit=limit, offset=offset, orderby=orderby, fulldata=False) + async def album_tracks(self, album_id:int, limit=100000, offset=0, orderby='disc_number,track_number') -> List[Track]: + ''' get album tracks for the given album ''' + sql_query = """SELECT * FROM tracks + WHERE album_id=%s""" % album_id + return await self.tracks(sql_query, orderby=orderby, limit=limit, offset=offset, fulldata=False) + async def playlist_tracks(self, playlist_id:int, limit=100000, offset=0, orderby='position') -> List[Track]: ''' get playlist tracks for the given playlist_id ''' sql_query = """SELECT *, playlist_tracks.position FROM tracks diff --git a/music_assistant/models/musicprovider.py b/music_assistant/models/musicprovider.py index 546a6626..e3484913 100755 --- a/music_assistant/models/musicprovider.py +++ b/music_assistant/models/musicprovider.py @@ -221,11 +221,12 @@ class MusicProvider(): ''' return top tracks for an artist ''' items = [] for prov_track in await self.get_artist_toptracks(prov_item_id): - db_id = await self.mass.db.get_database_id(self.prov_id, prov_track.item_id, MediaType.Track) - if db_id: - items.append( await self.mass.db.track(db_id) ) - else: - items.append(prov_track) + if prov_track: + db_id = await self.mass.db.get_database_id(self.prov_id, prov_track.item_id, MediaType.Track) + if db_id: + items.append( await self.mass.db.track(db_id) ) + else: + items.append(prov_track) return items async def artist_albums(self, prov_item_id) -> List[Track]: @@ -245,10 +246,11 @@ class MusicProvider(): searchstr = "%s - %s" %(searchartist.name, searchtrack.name) search_results = await self.search(searchstr, [MediaType.Track], limit=5) for item in search_results["tracks"]: - if item and item.name == searchtrack.name and item.version == searchtrack.version and item.album.name == searchtrack.album.name: + if (item and item.name == searchtrack.name and + item.version == searchtrack.version): # double safety check - artist must match exactly ! for artist in item.artists: - if artist.name == searchartist.name: + if artist.name.lower() == searchartist.name.lower(): # just load this item in the database, it will be matched automagically ;-) return await self.artist(artist.item_id, lazy=False) @@ -257,7 +259,9 @@ class MusicProvider(): searchstr = "%s - %s %s" %(searchalbum.artist.name, searchalbum.name, searchalbum.version) search_results = await self.search(searchstr, [MediaType.Album], limit=5) for item in search_results["albums"]: - if item and item.name == searchalbum.name and item.version == searchalbum.version and item.artist.name == searchalbum.artist.name: + if (item and item.name == searchalbum.name and + item.version == searchalbum.version and + item.artist.name == searchalbum.artist.name): # just load this item in the database, it will be matched automagically ;-) await self.album(item.item_id, lazy=False) @@ -267,7 +271,10 @@ class MusicProvider(): searchartists = [item.name for item in searchtrack.artists] search_results = await self.search(searchstr, [MediaType.Track], limit=5) for item in search_results["tracks"]: - if item and item.name == searchtrack.name and item.version == searchtrack.version and item.album.name == searchtrack.album.name: + if (item and item.name == searchtrack.name and + item.version == searchtrack.version and + item.album and item.album.name == searchtrack.album.name and + item.album.version == searchtrack.album.version): # double safety check - artist must match exactly ! for artist in item.artists: if artist.name in searchartists: diff --git a/music_assistant/music_manager.py b/music_assistant/music_manager.py index 0dab61b6..53058666 100755 --- a/music_assistant/music_manager.py +++ b/music_assistant/music_manager.py @@ -6,6 +6,9 @@ from typing import List import toolz import operator import os +import base64 +from PIL import Image +import aiohttp from .utils import run_periodic, LOGGER, load_provider_modules from .models.media_types import MediaType, Track, Artist, Album, Playlist, Radio @@ -151,15 +154,18 @@ class MusicManager(): async def album_tracks(self, album_id, provider='database') -> List[Track]: ''' get the album tracks for given album ''' - items = [] album = await self.album(album_id, provider) - for prov_mapping in album.provider_ids: - prov_id = prov_mapping['provider'] - prov_item_id = prov_mapping['item_id'] - prov_obj = self.providers[prov_id] - items += await prov_obj.album_tracks(prov_item_id) - items = list(toolz.unique(items, key=operator.attrgetter('item_id'))) - items = sorted(items, key=operator.attrgetter('disc_number'), reverse=False) + if provider == 'database' and album.in_library: + # library albums are synced + items = await self.mass.db.album_tracks(album_id) + if items: + return items + # collect the tracks from the first provider + for prov in album.provider_ids: + prov_obj = self.providers[prov['provider']] + items = await prov_obj.album_tracks(album_id) + if items: + break items = sorted(items, key=operator.attrgetter('track_number'), reverse=False) return items @@ -339,8 +345,8 @@ class MusicManager(): db_item = await music_provider.album(item.item_id, lazy=False) cur_db_ids.append(db_item.item_id) # precache album tracks... - for album_track in await music_provider.get_album_tracks(item.item_id): - await music_provider.track(album_track.item_id) + for album_track in await music_provider.album_tracks(item.item_id): + await self.track(album_track.item_id, album_track.provider) if not db_item.item_id in prev_db_ids: await self.mass.db.add_to_library(db_item.item_id, MediaType.Album, prov_id) # process deletions @@ -390,6 +396,7 @@ class MusicManager(): async def sync_playlist_tracks(self, db_playlist_id, prov_id, prov_playlist_id): ''' sync library playlists tracks for given provider''' + playlist = await self.mass.db.playlist(db_playlist_id) music_provider = self.providers[prov_id] prev_items = await self.playlist_tracks(db_playlist_id) prev_db_ids = [item.item_id for item in prev_items] @@ -407,12 +414,14 @@ class MusicManager(): # always add/update because position could be changed # note: we ignore duplicate tracks in the same playlist await self.mass.db.add_playlist_track(db_playlist_id, db_item.item_id, pos) + else: + LOGGER.warning("SKIP duplicate track in playlist %s: %s" %(playlist.name, db_item.name)) pos += 1 # process playlist track deletions for db_id in prev_db_ids: if db_id not in cur_db_ids: await self.mass.db.remove_playlist_track(db_playlist_id, db_id) - LOGGER.info("Finished syncing Playlist %s tracks for provider %s" % (prov_playlist_id, prov_id)) + LOGGER.info("Finished syncing Playlist %s tracks for provider %s" % (playlist.name, prov_id)) async def sync_radios(self, prov_id): ''' sync library radios for given provider''' @@ -433,3 +442,50 @@ class MusicManager(): if db_id not in cur_db_ids: await self.mass.db.remove_from_library(db_id, MediaType.Radio, prov_id) LOGGER.info("Finished syncing Radios for provider %s" % prov_id) + + async def get_image_path(self, item_id, media_type:MediaType, provider, size=50, key='image'): + ''' get path to (resized) thumb image for given media item ''' + cache_folder = os.path.join(self.mass.datapath, '.thumbs') + cache_id = f'{item_id}{media_type}{provider}{key}' + cache_id = base64.b64encode(cache_id.encode('utf-8')).decode('utf-8') + cache_file_org = os.path.join(cache_folder, f'{cache_id}0.png') + cache_file_sized = os.path.join(cache_folder, f'{cache_id}{size}.png') + if os.path.isfile(cache_file_sized): + # return file from cache + return cache_file_sized + # no file in cache so we should get it + img_url = '' + item = await self.item(item_id, media_type, provider) + if item and item.metadata.get(key): + img_url = item.metadata[key] + elif media_type == MediaType.Track: + # try album image instead for tracks + return await self.get_image_path( + item.album.item_id, MediaType.Album, item.album.provider, size, key) + elif media_type == MediaType.Album: + # try artist image instead for albums + return await self.get_image_path( + item.artist.item_id, MediaType.Artist, item.artist.provider, size, key) + if not img_url: + return None + # fetch image and store in cache + os.makedirs(cache_folder, exist_ok=True) + # download base image + async with aiohttp.ClientSession() as session: + async with session.get(img_url, verify_ssl=False) as response: + assert response.status == 200 + img_data = await response.read() + with open(cache_file_org, 'wb') as f: + f.write(img_data) + if not size: + # return base image + return cache_file_org + # save resized image + basewidth = size + img = Image.open(cache_file_org) + wpercent = (basewidth/float(img.size[0])) + hsize = int((float(img.size[1])*float(wpercent))) + img = img.resize((basewidth,hsize), Image.ANTIALIAS) + img.save(cache_file_sized) + # return file from cache + return cache_file_sized diff --git a/music_assistant/musicproviders/qobuz.py b/music_assistant/musicproviders/qobuz.py index 576c3772..c1213ae1 100644 --- a/music_assistant/musicproviders/qobuz.py +++ b/music_assistant/musicproviders/qobuz.py @@ -166,6 +166,8 @@ class QobuzProvider(MusicProvider): track = await self.__parse_track(track_obj) if track: tracks.append(track) + else: + LOGGER.warning("Unavailable track found in album %s: %s" %(prov_album_id, track_obj['title'])) return tracks async def get_playlist_tracks(self, prov_playlist_id, limit=100, offset=0) -> List[Track]: @@ -179,7 +181,9 @@ class QobuzProvider(MusicProvider): playlist_track = await self.__parse_track(track_obj) if playlist_track: tracks.append(playlist_track) - # TODO: should we look for an alternative track version if the original is marked unavailable ? + else: + LOGGER.warning("Unavailable track found in playlist %s: %s" %(playlist_obj['name'], track_obj['title'])) + # TODO: should we look for an alternative track version if the original is marked unavailable ? return tracks async def get_artist_albums(self, prov_artist_id, limit=100, offset=0) -> List[Album]: @@ -197,14 +201,16 @@ class QobuzProvider(MusicProvider): async def get_artist_toptracks(self, prov_artist_id) -> List[Track]: ''' get a list of most popular tracks for the given artist ''' # artist toptracks not supported on Qobuz, so use search instead + # assuming qobuz returns results sorted by popularity items = [] artist = await self.get_artist(prov_artist_id) - params = {"query": artist.name, "limit": 10, "type": "tracks" } + params = {"query": artist.name, "limit": 25, "type": "tracks" } searchresult = await self.__get_data("catalog/search", params) for item in searchresult["tracks"]["items"]: if "performer" in item and str(item["performer"]["id"]) == str(prov_artist_id): track = await self.__parse_track(item) - items.append(track) + if track: + items.append(track) return items async def add_library(self, prov_item_id, media_type:MediaType): @@ -336,9 +342,22 @@ class QobuzProvider(MusicProvider): return None album.item_id = album_obj['id'] album.provider = self.prov_id + if album_obj['maximum_sampling_rate'] > 192: + quality = TrackQuality.FLAC_LOSSLESS_HI_RES_4 + elif album_obj['maximum_sampling_rate'] > 96: + quality = TrackQuality.FLAC_LOSSLESS_HI_RES_3 + elif album_obj['maximum_sampling_rate'] > 48: + quality = TrackQuality.FLAC_LOSSLESS_HI_RES_2 + elif album_obj['maximum_bit_depth'] > 16: + quality = TrackQuality.FLAC_LOSSLESS_HI_RES_1 + elif album_obj.get('format_id',0) == 5: + quality = TrackQuality.LOSSY_AAC + else: + quality = TrackQuality.FLAC_LOSSLESS album.provider_ids.append({ "provider": self.prov_id, "item_id": album_obj['id'], + "quality": quality, "details": "%skHz %sbit" %(album_obj['maximum_sampling_rate'], album_obj['maximum_bit_depth']) }) album.name, album.version = parse_track_title(album_obj['title']) diff --git a/music_assistant/musicproviders/spotify.py b/music_assistant/musicproviders/spotify.py index d51ab00a..d2464d63 100644 --- a/music_assistant/musicproviders/spotify.py +++ b/music_assistant/musicproviders/spotify.py @@ -162,7 +162,7 @@ class SpotifyProvider(MusicProvider): async def get_playlist_tracks(self, prov_playlist_id, limit=50, offset=0) -> List[Track]: ''' get playlist tracks for given playlist id ''' - playlist_obj = await self.__get_data("playlists/%s?fields=snapshot_id" % prov_playlist_id, ignore_cache=True) + playlist_obj = await self.__get_data("playlists/%s?fields=snapshot_id,name" % prov_playlist_id, ignore_cache=True) cache_checksum = playlist_obj["snapshot_id"] track_objs = await self.__get_all_items("playlists/%s/tracks" % prov_playlist_id, limit=limit, offset=offset, cache_checksum=cache_checksum) tracks = [] @@ -170,6 +170,8 @@ class SpotifyProvider(MusicProvider): playlist_track = await self.__parse_track(track_obj) if playlist_track: tracks.append(playlist_track) + else: + LOGGER.warning("Unavailable track found in playlist %s: %s" %(playlist_obj['name'], track_obj['track']['name'])) return tracks async def get_artist_albums(self, prov_artist_id) -> List[Album]: @@ -342,7 +344,8 @@ class SpotifyProvider(MusicProvider): album.metadata['explicit'] = str(album_obj['explicit']).lower() album.provider_ids.append({ "provider": self.prov_id, - "item_id": album_obj['id'] + "item_id": album_obj['id'], + "quality": TrackQuality.LOSSY_OGG }) return album diff --git a/music_assistant/web.py b/music_assistant/web.py index b4153fd1..121c5f07 100755 --- a/music_assistant/web.py +++ b/music_assistant/web.py @@ -57,16 +57,15 @@ class Web(): LOGGER.warning( "SSL certificate key file not found: %s" % config['ssl_key']) self.https_port = config['https_port'] self._enable_ssl = enable_ssl - async def setup(self): ''' perform async setup ''' - app = web.Application() + app = web.Application(middlewares=[self.handle_cors]) app.add_routes([web.get('/jsonrpc.js', self.json_rpc)]) app.add_routes([web.post('/jsonrpc.js', self.json_rpc)]) app.add_routes([web.get('/ws', self.websocket_handler)]) - app.add_routes([web.get('/stream/{player_id}', self.mass.http_streamer.stream)]) - app.add_routes([web.get('/stream/{player_id}/{queue_item_id}', self.mass.http_streamer.stream)]) + app.add_routes([web.get('/stream/{player_id}', self.mass.http_streamer.stream, allow_head=False)]) + app.add_routes([web.get('/stream/{player_id}/{queue_item_id}', self.mass.http_streamer.stream, allow_head=False)]) app.add_routes([web.get('/api/search', self.search)]) app.add_routes([web.get('/api/config', self.get_config)]) app.add_routes([web.post('/api/config/{key}/{subkey}', self.save_config)]) @@ -82,6 +81,7 @@ class Web(): app.add_routes([web.get('/api/artists/{artist_id}/toptracks', self.artist_toptracks)]) app.add_routes([web.get('/api/artists/{artist_id}/albums', self.artist_albums)]) app.add_routes([web.get('/api/albums/{album_id}/tracks', self.album_tracks)]) + app.add_routes([web.get('/api/{media_type}/{media_id}/image', self.get_image)]) app.add_routes([web.get('/api/{media_type}/{media_id}', self.get_item)]) app.add_routes([web.get('/api/{media_type}', self.get_items)]) app.add_routes([web.get('/', self.index)]) @@ -99,6 +99,13 @@ class Web(): await https_site.start() LOGGER.info("Started HTTPS webserver on port %s" % self.config['https_port']) + @web.middleware + async def handle_cors(self, request, handler): + ''' append CORS header to our API ''' + response = await handler(request) + response.headers['Access-Control-Allow-Origin'] = '*' + return response + async def get_items(self, request): ''' get multiple library items''' media_type_str = request.match_info.get('media_type') @@ -128,17 +135,32 @@ class Web(): result = await self.mass.music.item(media_id, media_type, provider, lazy=lazy) return web.json_response(result, dumps=json_serializer) + async def get_image(self, request): + ''' get item image ''' + media_type_str = request.match_info.get('media_type') + media_type = media_type_from_string(media_type_str) + media_id = request.match_info.get('media_id') + # optional params + provider = request.rel_url.query.get('provider', 'database') + size = int(request.rel_url.query.get('size', 0)) + type_key = request.rel_url.query.get('type', 'image') + img_file = await self.mass.music.get_image_path(media_id, media_type, provider, size, type_key) + if not img_file or not os.path.isfile(img_file): + return web.Response(status=404) + headers = {'Cache-Control': 'max-age=86400, public', 'Pragma': 'public'} + return web.FileResponse(img_file, headers=headers) + async def artist_toptracks(self, request): ''' get top tracks for given artist ''' artist_id = request.match_info.get('artist_id') - provider = request.rel_url.query.get('provider') + provider = request.rel_url.query.get('provider', 'database') result = await self.mass.music.artist_toptracks(artist_id, provider) return web.json_response(result, dumps=json_serializer) async def artist_albums(self, request): ''' get (all) albums for given artist ''' artist_id = request.match_info.get('artist_id') - provider = request.rel_url.query.get('provider') + provider = request.rel_url.query.get('provider', 'database') result = await self.mass.music.artist_albums(artist_id, provider) return web.json_response(result, dumps=json_serializer) @@ -147,14 +169,14 @@ class Web(): playlist_id = request.match_info.get('playlist_id') limit = int(request.query.get('limit', 50)) offset = int(request.query.get('offset', 0)) - provider = request.rel_url.query.get('provider') + provider = request.rel_url.query.get('provider', 'database') result = await self.mass.music.playlist_tracks(playlist_id, provider, offset=offset, limit=limit) return web.json_response(result, dumps=json_serializer) async def album_tracks(self, request): ''' get album tracks from provider''' album_id = request.match_info.get('album_id') - provider = request.rel_url.query.get('provider') + provider = request.rel_url.query.get('provider','database') result = await self.mass.music.album_tracks(album_id, provider) return web.json_response(result, dumps=json_serializer) @@ -332,6 +354,8 @@ class Web(): cmds = params[1] cmd_str = " ".join(cmds) player = await self.mass.players.get_player(player_id) + if not player: + return web.Response(status=404) if cmd_str == 'play': await player.play() elif cmd_str == 'pause': diff --git a/requirements.txt b/requirements.txt index cfe85c5e..7e7384c2 100755 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,5 @@ aiohttp pyloudnorm SoundFile aiorun -soco \ No newline at end of file +soco +pillow \ No newline at end of file