From fd9987ec35ce033b8aa3b1f54a01d6f687234dd1 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 19:58:12 +0200 Subject: [PATCH] Implemented images for albums --- maloja/apis/native_v1.py | 4 +- maloja/images.py | 98 ++++++++++++++++++++++---------- maloja/server.py | 4 +- maloja/thirdparty/__init__.py | 33 ++++++++++- maloja/thirdparty/audiodb.py | 8 ++- maloja/thirdparty/deezer.py | 11 +++- maloja/thirdparty/lastfm.py | 9 ++- maloja/thirdparty/musicbrainz.py | 2 + maloja/thirdparty/spotify.py | 4 +- 9 files changed, 133 insertions(+), 40 deletions(-) diff --git a/maloja/apis/native_v1.py b/maloja/apis/native_v1.py index cd031a2..f7e36a7 100644 --- a/maloja/apis/native_v1.py +++ b/maloja/apis/native_v1.py @@ -519,7 +519,7 @@ def post_scrobble( @api.post("addpicture") @authenticated_function(alternate=api_key_correct,api=True) @catch_exceptions -def add_picture(b64,artist:Multi=[],title=None): +def add_picture(b64,artist:Multi=[],title=None,albumtitle=None): """Uploads a new image for an artist or track. param string b64: Base 64 representation of the image @@ -531,8 +531,10 @@ def add_picture(b64,artist:Multi=[],title=None): for a in artist: keys.append("artist",a) if title is not None: keys.append("title",title) + elif albumtitle is not None: keys.append("albumtitle",albumtitle) k_filter, _, _, _, _ = uri_to_internal(keys) if "track" in k_filter: k_filter = k_filter["track"] + elif "album" in k_filter: k_filter = k_filter["album"] url = images.set_image(b64,**k_filter) return { diff --git a/maloja/images.py b/maloja/images.py index 103b0f1..f92e91f 100644 --- a/maloja/images.py +++ b/maloja/images.py @@ -39,6 +39,13 @@ DB['tracks'] = sql.Table( sql.Column('expire',sql.Integer), sql.Column('raw',sql.String) ) +DB['albums'] = sql.Table( + 'albums', meta, + sql.Column('id',sql.Integer,primary_key=True), + sql.Column('url',sql.String), + sql.Column('expire',sql.Integer), + sql.Column('raw',sql.String) +) meta.create_all(engine) @@ -137,7 +144,7 @@ def resolve_track_image(track_id): # local image if malojaconfig["USE_LOCAL_IMAGES"]: - images = local_files(artists=track['artists'],title=track['title']) + images = local_files(track=track) if len(images) != 0: result = random.choice(images) result = urllib.parse.quote(result) @@ -181,31 +188,56 @@ def resolve_artist_image(artist_id): return result +def resolve_album_image(album_id): + + with resolve_semaphore: + # check cache + result = get_image_from_cache(album_id,'albums') + if result is not None: + return result + + album = database.sqldb.get_album(album_id) + + # local image + if malojaconfig["USE_LOCAL_IMAGES"]: + images = local_files(album=album) + if len(images) != 0: + result = random.choice(images) + result = urllib.parse.quote(result) + result = {'type':'url','value':result} + set_image_in_cache(album_id,'tracks',result['value']) + return result + + # third party + result = thirdparty.get_image_album_all((album['artists'],album['albumtitle'])) + result = {'type':'url','value':result} + set_image_in_cache(album_id,'albums',result['value']) + + return result + + # removes emojis and weird shit from names def clean(name): return "".join(c for c in name if c.isalnum() or c in []).strip() -def get_all_possible_filenames(artist=None,artists=None,title=None): - # check if we're dealing with a track or artist, then clean up names - # (only remove non-alphanumeric, allow korean and stuff) - - if title is not None and artists is not None: - track = True - title, artists = clean(title), [clean(a) for a in artists] - elif artist is not None: - track = False +# new and improved +def get_all_possible_filenames(artist=None,track=None,album=None): + if track: + title, artists = clean(track['title']), [clean(a) for a in track['artists']] + superfolder = "tracks/" + elif album: + title, artists = clean(album['albumtitle']), [clean(a) for a in album['artists']] + superfolder = "albums/" + elif artist: artist = clean(artist) - else: return [] - - - superfolder = "tracks/" if track else "artists/" + superfolder = "artists/" + else: + return [] filenames = [] - if track: - #unsafeartists = [artist.translate(None,"-_./\\") for artist in artists] + if track or album: safeartists = [re.sub("[^a-zA-Z0-9]","",artist) for artist in artists] - #unsafetitle = title.translate(None,"-_./\\") safetitle = re.sub("[^a-zA-Z0-9]","",title) if len(artists) < 4: @@ -215,7 +247,6 @@ def get_all_possible_filenames(artist=None,artists=None,title=None): unsafeperms = [sorted(artists)] safeperms = [sorted(safeartists)] - for unsafeartistlist in unsafeperms: filename = "-".join(unsafeartistlist) + "_" + title if filename != "": @@ -246,10 +277,11 @@ def get_all_possible_filenames(artist=None,artists=None,title=None): return [superfolder + name for name in filenames] -def local_files(artist=None,artists=None,title=None): + +def local_files(artist=None,album=None,track=None): - filenames = get_all_possible_filenames(artist,artists,title) + filenames = get_all_possible_filenames(artist=artist,album=album,track=track) images = [] @@ -276,13 +308,18 @@ class MalformedB64(Exception): pass def set_image(b64,**keys): - track = "title" in keys - if track: - entity = {'artists':keys['artists'],'title':keys['title']} - id = database.sqldb.get_track_id(entity) - else: - entity = keys['artist'] - id = database.sqldb.get_artist_id(entity) + if "title" in keys: + entity = {"track":keys} + id = database.sqldb.get_track_id(entity['track']) + dbtable = "tracks" + elif "albumtitle" in keys: + entity = {"album":keys} + id = database.sqldb.get_album_id(entity['album']) + dbtable = "albums" + elif "artist" in keys: + entity = keys + id = database.sqldb.get_artist_id(entity['artist']) + dbtable = "artists" log("Trying to set image, b64 string: " + str(b64[:30] + "..."),module="debug") @@ -293,13 +330,13 @@ def set_image(b64,**keys): type,b64 = match.groups() b64 = base64.b64decode(b64) filename = "webupload" + str(int(datetime.datetime.now().timestamp())) + "." + type - for folder in get_all_possible_filenames(**keys): + for folder in get_all_possible_filenames(**entity): if os.path.exists(data_dir['images'](folder)): with open(data_dir['images'](folder,filename),"wb") as f: f.write(b64) break else: - folder = get_all_possible_filenames(**keys)[0] + folder = get_all_possible_filenames(**entity)[0] os.makedirs(data_dir['images'](folder)) with open(data_dir['images'](folder,filename),"wb") as f: f.write(b64) @@ -308,7 +345,6 @@ def set_image(b64,**keys): log("Saved image as " + data_dir['images'](folder,filename),module="debug") # set as current picture in rotation - if track: set_image_in_cache(id,'tracks',os.path.join("/images",folder,filename)) - else: set_image_in_cache(id,'artists',os.path.join("/images",folder,filename)) + set_image_in_cache(id,dbtable,os.path.join("/images",folder,filename)) return os.path.join("/images",folder,filename) diff --git a/maloja/server.py b/maloja/server.py index ce3595c..7e3815e 100644 --- a/maloja/server.py +++ b/maloja/server.py @@ -19,7 +19,7 @@ from doreah import auth # rest of the project from . import database from .database.jinjaview import JinjaDBConnection -from .images import resolve_track_image, resolve_artist_image +from .images import resolve_track_image, resolve_artist_image, resolve_album_image from .malojauri import uri_to_internal, remove_identical from .pkg_global.conf import malojaconfig, data_dir from .jinjaenv.context import jinja_environment @@ -124,6 +124,8 @@ def dynamic_image(): result = resolve_track_image(keys['id']) elif keys['type'] == 'artist': result = resolve_artist_image(keys['id']) + elif keys['type'] == 'album': + result = resolve_album_image(keys['id']) if result is None or result['value'] in [None,'']: return "" diff --git a/maloja/thirdparty/__init__.py b/maloja/thirdparty/__init__.py index 135d792..25cd657 100644 --- a/maloja/thirdparty/__init__.py +++ b/maloja/thirdparty/__init__.py @@ -63,7 +63,18 @@ def get_image_artist_all(artist): log("Could not get artist image for " + str(artist) + " from " + service.name) except Exception as e: log("Error getting artist image from " + service.name + ": " + repr(e)) - +def get_image_album_all(album): + with thirdpartylock: + for service in services["metadata"]: + try: + res = service.get_image_album(album) + if res is not None: + log("Got album image for " + str(album) + " from " + service.name) + return res + else: + log("Could not get album image for " + str(album) + " from " + service.name) + except Exception as e: + log("Error getting album image from " + service.name + ": " + repr(e)) class GenericInterface: @@ -217,6 +228,23 @@ class MetadataInterface(GenericInterface,abstract=True): if imgurl is not None: imgurl = self.postprocess_url(imgurl) return imgurl + def get_image_album(self,album): + artists, title = album + artiststring = urllib.parse.quote(", ".join(artists)) + titlestring = urllib.parse.quote(title) + response = urllib.request.urlopen( + self.metadata["albumurl"].format(artist=artiststring,title=titlestring,**self.settings) + ) + + responsedata = response.read() + if self.metadata["response_type"] == "json": + data = json.loads(responsedata) + imgurl = self.metadata_parse_response_album(data) + else: + imgurl = None + if imgurl is not None: imgurl = self.postprocess_url(imgurl) + return imgurl + # default function to parse response by descending down nodes # override if more complicated def metadata_parse_response_artist(self,data): @@ -225,6 +253,9 @@ class MetadataInterface(GenericInterface,abstract=True): def metadata_parse_response_track(self,data): return self._parse_response("response_parse_tree_track", data) + def metadata_parse_response_album(self,data): + return self._parse_response("response_parse_tree_album", data) + def _parse_response(self, resp, data): res = data for node in self.metadata[resp]: diff --git a/maloja/thirdparty/audiodb.py b/maloja/thirdparty/audiodb.py index 66d2b84..9a4d84a 100644 --- a/maloja/thirdparty/audiodb.py +++ b/maloja/thirdparty/audiodb.py @@ -9,13 +9,17 @@ class AudioDB(MetadataInterface): } metadata = { - #"trackurl": "https://theaudiodb.com/api/v1/json/{api_key}/searchtrack.php?s={artist}&t={title}", + #"trackurl": "https://theaudiodb.com/api/v1/json/{api_key}/searchtrack.php?s={artist}&t={title}", #patreon "artisturl": "https://www.theaudiodb.com/api/v1/json/{api_key}/search.php?s={artist}", + #"albumurl": "https://www.theaudiodb.com/api/v1/json/{api_key}/searchalbum.php?s={artist}&a={title}", #patreon "response_type":"json", #"response_parse_tree_track": ["tracks",0,"astrArtistThumb"], "response_parse_tree_artist": ["artists",0,"strArtistThumb"], "required_settings": ["api_key"], } - def get_image_track(self,artist): + def get_image_track(self,track): + return None + + def get_image_album(self,album): return None diff --git a/maloja/thirdparty/deezer.py b/maloja/thirdparty/deezer.py index 1347c6f..c691899 100644 --- a/maloja/thirdparty/deezer.py +++ b/maloja/thirdparty/deezer.py @@ -8,10 +8,17 @@ class Deezer(MetadataInterface): } metadata = { - "trackurl": "https://api.deezer.com/search?q={artist}%20{title}", + #"trackurl": "https://api.deezer.com/search?q={artist}%20{title}", "artisturl": "https://api.deezer.com/search?q={artist}", + "albumurl": "https://api.deezer.com/search?q={artist}%20{title}", "response_type":"json", - "response_parse_tree_track": ["data",0,"album","cover_medium"], + #"response_parse_tree_track": ["data",0,"album","cover_medium"], "response_parse_tree_artist": ["data",0,"artist","picture_medium"], + "response_parse_tree_album": ["data",0,"album","cover_medium"], "required_settings": [], } + + def get_image_track(self,track): + return None + # we can use the album pic from the track search, + # but should do so via maloja logic diff --git a/maloja/thirdparty/lastfm.py b/maloja/thirdparty/lastfm.py index 80f3c75..e565e5f 100644 --- a/maloja/thirdparty/lastfm.py +++ b/maloja/thirdparty/lastfm.py @@ -22,15 +22,22 @@ class LastFM(MetadataInterface, ProxyScrobbleInterface): "activated_setting": "SCROBBLE_LASTFM" } metadata = { + #"artisturl": "https://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artist}&api_key={apikey}&format=json" "trackurl": "https://ws.audioscrobbler.com/2.0/?method=track.getinfo&track={title}&artist={artist}&api_key={apikey}&format=json", + "albumurl": "https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={apikey}&artist={artist}&album={title}&format=json", "response_type":"json", "response_parse_tree_track": ["track","album","image",-1,"#text"], + # technically just the album artwork, but we use it for now + #"response_parse_tree_artist": ["artist","image",-1,"#text"], + "response_parse_tree_album": ["album","image",-1,"#text"], "required_settings": ["apikey"], } def get_image_artist(self,artist): return None - # lastfm doesn't provide artist images + # lastfm still provides that endpoint with data, + # but doesn't provide actual images + def proxyscrobble_parse_response(self,data): return data.attrib.get("status") == "ok" and data.find("scrobbles").attrib.get("ignored") == "0" diff --git a/maloja/thirdparty/musicbrainz.py b/maloja/thirdparty/musicbrainz.py index 78e033a..f16229b 100644 --- a/maloja/thirdparty/musicbrainz.py +++ b/maloja/thirdparty/musicbrainz.py @@ -26,6 +26,8 @@ class MusicBrainz(MetadataInterface): return None # not supported + def get_image_album(self,album): + return None def get_image_track(self,track): self.lock.acquire() diff --git a/maloja/thirdparty/spotify.py b/maloja/thirdparty/spotify.py index 8d50284..2665865 100644 --- a/maloja/thirdparty/spotify.py +++ b/maloja/thirdparty/spotify.py @@ -15,9 +15,11 @@ class Spotify(MetadataInterface): metadata = { "trackurl": "https://api.spotify.com/v1/search?q=artist:{artist}%20track:{title}&type=track&access_token={token}", + "albumurl": "https://api.spotify.com/v1/search?q=artist:{artist}%album:{title}&type=album&access_token={token}", "artisturl": "https://api.spotify.com/v1/search?q=artist:{artist}&type=artist&access_token={token}", "response_type":"json", - "response_parse_tree_track": ["tracks","items",0,"album","images",0,"url"], + "response_parse_tree_track": ["tracks","items",0,"album","images",0,"url"], # use album art + "response_parse_tree_album": ["albums","items",0,"images",0,"url"], "response_parse_tree_artist": ["artists","items",0,"images",0,"url"], "required_settings": ["apiid","secret"], }