"folders": [
{
"path": "."
+ },
+ {
+ "path": "/Users/marcelvanderveldt/Workdir/test"
}
]
}
\ No newline at end of file
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()
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()
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
''' 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]:
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)
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)
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:
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
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
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
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]
# 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'''
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
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]:
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]:
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):
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'])
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 = []
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]:
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
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)])
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)])
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')
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)
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)
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':
pyloudnorm
SoundFile
aiorun
-soco
\ No newline at end of file
+soco
+pillow
\ No newline at end of file