From 8f3df9881c36109c26a6120db07568c8594be8d4 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 14:52:54 +0200 Subject: [PATCH 01/18] Added album support to database --- maloja/database/sqldb.py | 152 +++++++++++++++++++++++++++++++++------ 1 file changed, 132 insertions(+), 20 deletions(-) diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index 61f0b38..f22c27c 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -21,44 +21,71 @@ DBTABLES = { # name - type - foreign key - kwargs 'scrobbles':{ 'columns':[ - ("timestamp", sql.Integer, {'primary_key':True}), - ("rawscrobble", sql.String, {}), - ("origin", sql.String, {}), - ("duration", sql.Integer, {}), - ("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}), - ("extra", sql.String, {}) + ("timestamp", sql.Integer, {'primary_key':True}), + ("rawscrobble", sql.String, {}), + ("origin", sql.String, {}), + ("duration", sql.Integer, {}), + ("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}), + ("extra", sql.String, {}) ], 'extraargs':(),'extrakwargs':{} }, 'tracks':{ 'columns':[ - ("id", sql.Integer, {'primary_key':True}), - ("title", sql.String, {}), - ("title_normalized",sql.String, {}), - ("length", sql.Integer, {}) + ("id", sql.Integer, {'primary_key':True}), + ("title", sql.String, {}), + ("title_normalized", sql.String, {}), + ("length", sql.Integer, {}) ], 'extraargs':(),'extrakwargs':{'sqlite_autoincrement':True} }, 'artists':{ 'columns':[ - ("id", sql.Integer, {'primary_key':True}), - ("name", sql.String, {}), - ("name_normalized", sql.String, {}) + ("id", sql.Integer, {'primary_key':True}), + ("name", sql.String, {}), + ("name_normalized", sql.String, {}) + ], + 'extraargs':(),'extrakwargs':{'sqlite_autoincrement':True} + }, + 'albums':{ + 'columns':[ + ("id", sql.Integer, {'primary_key':True}), + ("albtitle", sql.String, {}), + ("albtitle_normalized", sql.String, {}) + #("albumartist", sql.String, {}) + # when an album has no artists, always use 'Various Artists' ], 'extraargs':(),'extrakwargs':{'sqlite_autoincrement':True} }, 'trackartists':{ 'columns':[ - ("id", sql.Integer, {'primary_key':True}), - ("artist_id", sql.Integer, sql.ForeignKey('artists.id'), {}), - ("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}) + ("id", sql.Integer, {'primary_key':True}), + ("artist_id", sql.Integer, sql.ForeignKey('artists.id'), {}), + ("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}) ], 'extraargs':(sql.UniqueConstraint('artist_id', 'track_id'),),'extrakwargs':{} }, + 'albumartists':{ + 'columns':[ + ("id", sql.Integer, {'primary_key':True}), + ("artist_id", sql.Integer, sql.ForeignKey('artists.id'), {}), + ("album_id", sql.Integer, sql.ForeignKey('albums.id'), {}) + ], + 'extraargs':(sql.UniqueConstraint('artist_id', 'album_id'),),'extrakwargs':{} + }, + 'albumtracks':{ + # tracks can be in multiple albums + 'columns':[ + ("id", sql.Integer, {'primary_key':True}), + ("album_id", sql.Integer, sql.ForeignKey('albums.id'), {}), + ("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}) + ], + 'extraargs':(sql.UniqueConstraint('album_id', 'track_id'),),'extrakwargs':{} + }, 'associated_artists':{ 'columns':[ - ("source_artist", sql.Integer, sql.ForeignKey('artists.id'), {}), - ("target_artist", sql.Integer, sql.ForeignKey('artists.id'), {}) + ("source_artist", sql.Integer, sql.ForeignKey('artists.id'), {}), + ("target_artist", sql.Integer, sql.ForeignKey('artists.id'), {}) ], 'extraargs':(sql.UniqueConstraint('source_artist', 'target_artist'),),'extrakwargs':{} } @@ -138,7 +165,7 @@ def connection_provider(func): # "artists":list, # "title":string, # "album":{ -# "name":string, +# "title":string, # "artists":list # }, # "length":None @@ -207,6 +234,19 @@ def artists_db_to_dict(rows,dbconn=None): def artist_db_to_dict(row,dbconn=None): return artists_db_to_dict([row],dbconn=dbconn)[0] +def albums_db_to_dict(rows,dbconn=None): + artists = get_artists_of_albums(set(row.id for row in rows),dbconn=dbconn) + return [ + { + "artists":artists[row.id], + "title":row.albtitle, + } + for row in rows + ] + +def album_db_to_dict(row,dbconn=None): + return albums_db_to_dict([row],dbconn=dbconn) + @@ -236,6 +276,12 @@ def artist_dict_to_db(info,dbconn=None): "name_normalized":normalize_name(info) } +def album_dict_to_db(info,dbconn=None): + return { + "albtitle":info.get('title'), + "albtitle_normalized":normalize_name(info.get('title')) + } + @@ -310,7 +356,7 @@ def get_track_id(trackdict,create_new=True,dbconn=None): op = DB['trackartists'].select( # DB['trackartists'].c.artist_id ).where( - DB['trackartists'].c.track_id==row[0] + DB['trackartists'].c.track_id==row.id ) result = dbconn.execute(op).all() match_artist_ids = [r.artist_id for r in result] @@ -364,6 +410,59 @@ def get_artist_id(artistname,create_new=True,dbconn=None): return result.inserted_primary_key[0] +@cached_wrapper +@connection_provider +def get_album_id(albumdict,create_new=True,dbconn=None): + ntitle = normalize_name(albumdict['title']) + artist_ids = [get_artist_id(a,dbconn=dbconn) for a in albumdict['artists']] + artist_ids = list(set(artist_ids)) + + + + + op = DB['albums'].select( +# DB['albums'].c.id + ).where( + DB['albums'].c.title_normalized==ntitle + ) + result = dbconn.execute(op).all() + for row in result: + # check if the artists are the same + foundtrackartists = [] + + op = DB['albumartists'].select( +# DB['albumartists'].c.artist_id + ).where( + DB['albumartists'].c.track_id==row.id + ) + result = dbconn.execute(op).all() + match_artist_ids = [r.artist_id for r in result] + #print("required artists",artist_ids,"this match",match_artist_ids) + if set(artist_ids) == set(match_artist_ids): + #print("ID for",albumdict['title'],"was",row[0]) + return row.id + + if not create_new: return None + + + op = DB['albums'].insert().values( + **album_dict_to_db(albumdict,dbconn=dbconn) + ) + result = dbconn.execute(op) + album_id = result.inserted_primary_key[0] + + for artist_id in artist_ids: + op = DB['albumartists'].insert().values( + album_id=album_id, + artist_id=artist_id + ) + result = dbconn.execute(op) + #print("Created",trackdict['title'],track_id) + return track_id + + + + ### Edit existing @@ -734,6 +833,19 @@ def get_artists_of_tracks(track_ids,dbconn=None): artists.setdefault(row.track_id,[]).append(artist_db_to_dict(row,dbconn=dbconn)) return artists +@cached_wrapper_individual +@connection_provider +def get_artists_of_albums(album_ids,dbconn=None): + op = sql.join(DB['albumartists'],DB['artists']).select().where( + DB['albumartists'].c.album_id.in_(album_ids) + ) + result = dbconn.execute(op).all() + + artists = {} + for row in result: + artists.setdefault(row.album_id,[]).append(artist_db_to_dict(row,dbconn=dbconn)) + return artists + @cached_wrapper_individual @connection_provider From 7f62021d57755cd24ddf13debee40d2a5056bd9e Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 15:37:01 +0200 Subject: [PATCH 02/18] Added functions for albums --- maloja/database/__init__.py | 4 ++-- maloja/database/sqldb.py | 45 ++++++++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index 473cb7b..c6178f0 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -139,7 +139,7 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None): "artists":scrobbleinfo.get('track_artists'), "title":scrobbleinfo.get('track_title'), "album":{ - "name":scrobbleinfo.get('album_name'), + "title":scrobbleinfo.get('album_title') or scrobbleinfo.get('album_name'), "artists":scrobbleinfo.get('album_artists') }, "length":scrobbleinfo.get('track_length') @@ -148,7 +148,7 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None): "origin":f"client:{client}" if client else "generic", "extra":{ k:scrobbleinfo[k] for k in scrobbleinfo if k not in - ['scrobble_time','track_artists','track_title','track_length','scrobble_duration','album_name','album_artists'] + ['scrobble_time','track_artists','track_title','track_length','scrobble_duration','album_title','album_name','album_artists'] }, "rawscrobble":rawscrobble } diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index f22c27c..9a5b17f 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -35,7 +35,8 @@ DBTABLES = { ("id", sql.Integer, {'primary_key':True}), ("title", sql.String, {}), ("title_normalized", sql.String, {}), - ("length", sql.Integer, {}) + ("length", sql.Integer, {}), + ("album_id", sql.Integer, sql.ForeignKey('albums.id'), {}) ], 'extraargs':(),'extrakwargs':{'sqlite_autoincrement':True} }, @@ -73,15 +74,15 @@ DBTABLES = { ], 'extraargs':(sql.UniqueConstraint('artist_id', 'album_id'),),'extrakwargs':{} }, - 'albumtracks':{ - # tracks can be in multiple albums - 'columns':[ - ("id", sql.Integer, {'primary_key':True}), - ("album_id", sql.Integer, sql.ForeignKey('albums.id'), {}), - ("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}) - ], - 'extraargs':(sql.UniqueConstraint('album_id', 'track_id'),),'extrakwargs':{} - }, +# 'albumtracks':{ +# # tracks can be in multiple albums +# 'columns':[ +# ("id", sql.Integer, {'primary_key':True}), +# ("album_id", sql.Integer, sql.ForeignKey('albums.id'), {}), +# ("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}) +# ], +# 'extraargs':(sql.UniqueConstraint('album_id', 'track_id'),),'extrakwargs':{} +# }, 'associated_artists':{ 'columns':[ ("source_artist", sql.Integer, sql.ForeignKey('artists.id'), {}), @@ -331,11 +332,27 @@ def delete_scrobble(scrobble_id,dbconn=None): return True +@connection_provider +def add_track_to_album(track_id,album_id,replace=False,dbconn=None): + + op = DB['tracks'].update().where( + DB['tracks'].c.id == track_id, + (DB['tracks'].c.album_id == None) or replace + # update if we want replacement or if there is no album yet + ).values( + DB['tracks'].c.album_id = album_id + ) + + result = dbconn.execute(op) + return True + + + ### these will 'get' the ID of an entity, creating it if necessary @cached_wrapper @connection_provider -def get_track_id(trackdict,create_new=True,dbconn=None): +def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): ntitle = normalize_name(trackdict['title']) artist_ids = [get_artist_id(a,dbconn=dbconn) for a in trackdict['artists']] artist_ids = list(set(artist_ids)) @@ -363,11 +380,11 @@ def get_track_id(trackdict,create_new=True,dbconn=None): #print("required artists",artist_ids,"this match",match_artist_ids) if set(artist_ids) == set(match_artist_ids): #print("ID for",trackdict['title'],"was",row[0]) + add_track_to_album(row.id,get_album_id(trackdict['album']),replace=update_album) return row.id if not create_new: return None - op = DB['tracks'].insert().values( **track_dict_to_db(trackdict,dbconn=dbconn) ) @@ -381,6 +398,8 @@ def get_track_id(trackdict,create_new=True,dbconn=None): ) result = dbconn.execute(op) #print("Created",trackdict['title'],track_id) + + add_track_to_album(track_id,get_album_id(trackdict['album'])) return track_id @cached_wrapper @@ -458,7 +477,7 @@ def get_album_id(albumdict,create_new=True,dbconn=None): ) result = dbconn.execute(op) #print("Created",trackdict['title'],track_id) - return track_id + return album_id From 3a4f145f41cd1c00a694f2d8b83c8e0b33b25d3d Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 16:04:50 +0200 Subject: [PATCH 03/18] Added album support for scrobbling --- maloja/apis/listenbrainz.py | 2 +- maloja/apis/native_v1.py | 2 +- maloja/database/__init__.py | 2 ++ maloja/database/sqldb.py | 25 +++++++++++++++++-------- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/maloja/apis/listenbrainz.py b/maloja/apis/listenbrainz.py index 234048c..cb15b89 100644 --- a/maloja/apis/listenbrainz.py +++ b/maloja/apis/listenbrainz.py @@ -75,7 +75,7 @@ class Listenbrainz(APIHandler): self.scrobble({ 'track_artists':[artiststr], 'track_title':titlestr, - 'album_name':albumstr, + 'album_title':albumstr, 'scrobble_time':timestamp, 'track_length': additional.get("duration"), **extrafields diff --git a/maloja/apis/native_v1.py b/maloja/apis/native_v1.py index 41fa636..4e9c49f 100644 --- a/maloja/apis/native_v1.py +++ b/maloja/apis/native_v1.py @@ -470,7 +470,7 @@ def post_scrobble( rawscrobble = { 'track_artists':(artist or []) + artists, 'track_title':title, - 'album_name':album, + 'album_title':album, 'album_artists':albumartists, 'scrobble_duration':duration, 'track_length':length, diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index c6178f0..54b76c9 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -130,8 +130,10 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None): scrobbleinfo = {**rawscrobble} if fix: scrobbleinfo['track_artists'],scrobbleinfo['track_title'] = cla.fullclean(scrobbleinfo['track_artists'],scrobbleinfo['track_title']) + scrobbleinfo['album_artists'] = cla.parseArtists(scrobbleinfo['album_artists']) scrobbleinfo['scrobble_time'] = scrobbleinfo.get('scrobble_time') or int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp()) + # processed info to internal scrobble dict scrobbledict = { "time":scrobbleinfo.get('scrobble_time'), diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index 9a5b17f..3ec27f2 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -335,12 +335,19 @@ def delete_scrobble(scrobble_id,dbconn=None): @connection_provider def add_track_to_album(track_id,album_id,replace=False,dbconn=None): + conditions = [ + DB['tracks'].c.id == track_id + ] + if not replace: + # if we dont want replacement, just update if there is no album yet + conditions.append( + DB['tracks'].c.album_id == None + ) + op = DB['tracks'].update().where( - DB['tracks'].c.id == track_id, - (DB['tracks'].c.album_id == None) or replace - # update if we want replacement or if there is no album yet + *conditions ).values( - DB['tracks'].c.album_id = album_id + album_id=album_id ) result = dbconn.execute(op) @@ -380,7 +387,8 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): #print("required artists",artist_ids,"this match",match_artist_ids) if set(artist_ids) == set(match_artist_ids): #print("ID for",trackdict['title'],"was",row[0]) - add_track_to_album(row.id,get_album_id(trackdict['album']),replace=update_album) + if 'album' in trackdict: + add_track_to_album(row.id,get_album_id(trackdict['album']),replace=update_album) return row.id if not create_new: return None @@ -399,7 +407,8 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): result = dbconn.execute(op) #print("Created",trackdict['title'],track_id) - add_track_to_album(track_id,get_album_id(trackdict['album'])) + if 'album' in trackdict: + add_track_to_album(track_id,get_album_id(trackdict['album'])) return track_id @cached_wrapper @@ -442,7 +451,7 @@ def get_album_id(albumdict,create_new=True,dbconn=None): op = DB['albums'].select( # DB['albums'].c.id ).where( - DB['albums'].c.title_normalized==ntitle + DB['albums'].c.albtitle_normalized==ntitle ) result = dbconn.execute(op).all() for row in result: @@ -452,7 +461,7 @@ def get_album_id(albumdict,create_new=True,dbconn=None): op = DB['albumartists'].select( # DB['albumartists'].c.artist_id ).where( - DB['albumartists'].c.track_id==row.id + DB['albumartists'].c.album_id==row.id ) result = dbconn.execute(op).all() match_artist_ids = [r.artist_id for r in result] From 657bb7e6d7d41cd825aacb7ec9b9941e5966f62d Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 16:14:29 +0200 Subject: [PATCH 04/18] Added setting for album information update --- maloja/database/__init__.py | 4 +++- maloja/database/sqldb.py | 12 ++++++------ maloja/pkg_global/conf.py | 1 + 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index 54b76c9..e8acca5 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -93,8 +93,10 @@ def incoming_scrobble(rawscrobble,fix=True,client=None,api=None,dbconn=None): log(f"Incoming scrobble [Client: {client} | API: {api}]: {rawscrobble}") scrobbledict = rawscrobble_to_scrobbledict(rawscrobble, fix, client) + albumupdate = (malojaconf["ALBUM_INFORMATION_TRUST"] == 'last') - sqldb.add_scrobble(scrobbledict,dbconn=dbconn) + + sqldb.add_scrobble(scrobbledict,update_album=albumupdate,dbconn=dbconn) proxy_scrobble_all(scrobbledict['track']['artists'],scrobbledict['track']['title'],scrobbledict['time']) dbcache.invalidate_caches(scrobbledict['time']) diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index 3ec27f2..4b33dbe 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -254,12 +254,12 @@ def album_db_to_dict(row,dbconn=None): ### DICT -> DB # These should return None when no data is in the dict so they can be used for update statements -def scrobble_dict_to_db(info,dbconn=None): +def scrobble_dict_to_db(info,update_album=False,dbconn=None): return { "timestamp":info.get('time'), "origin":info.get('origin'), "duration":info.get('duration'), - "track_id":get_track_id(info.get('track'),dbconn=dbconn), + "track_id":get_track_id(info.get('track'),update_album=update_album,dbconn=dbconn), "extra":json.dumps(info.get('extra')) if info.get('extra') else None, "rawscrobble":json.dumps(info.get('rawscrobble')) if info.get('rawscrobble') else None } @@ -291,17 +291,17 @@ def album_dict_to_db(info,dbconn=None): @connection_provider -def add_scrobble(scrobbledict,dbconn=None): - add_scrobbles([scrobbledict],dbconn=dbconn) +def add_scrobble(scrobbledict,update_album=False,dbconn=None): + add_scrobbles([scrobbledict],update_album=update_album,dbconn=dbconn) @connection_provider -def add_scrobbles(scrobbleslist,dbconn=None): +def add_scrobbles(scrobbleslist,update_album=False,dbconn=None): with SCROBBLE_LOCK: ops = [ DB['scrobbles'].insert().values( - **scrobble_dict_to_db(s,dbconn=dbconn) + **scrobble_dict_to_db(s,update_album=update_album,dbconn=dbconn) ) for s in scrobbleslist ] diff --git a/maloja/pkg_global/conf.py b/maloja/pkg_global/conf.py index ea6a8c0..f11d569 100644 --- a/maloja/pkg_global/conf.py +++ b/maloja/pkg_global/conf.py @@ -177,6 +177,7 @@ malojaconfig = Configuration( }, "Database":{ + "album_information_trust":(tp.Choice({'first':"First",'last':"Last"}),"Album Information Authority","first", "Whether to trust the first album information that is sent with a track or update every time a different album is sent"), "invalid_artists":(tp.Set(tp.String()), "Invalid Artists", ["[Unknown Artist]","Unknown Artist","Spotify"], "Artists that should be discarded immediately"), "remove_from_title":(tp.Set(tp.String()), "Remove from Title", ["(Original Mix)","(Radio Edit)","(Album Version)","(Explicit Version)","(Bonus Track)"], "Phrases that should be removed from song titles"), "delimiters_feat":(tp.Set(tp.String()), "Featuring Delimiters", ["ft.","ft","feat.","feat","featuring"], "Delimiters used for extra artists, even when in the title field"), From 4620ed1407450beac876adce8d384bb835aecfba Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 16:37:01 +0200 Subject: [PATCH 05/18] Added album support to URI handler --- maloja/malojauri.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/maloja/malojauri.py b/maloja/malojauri.py index 6b1af6d..b18ca9f 100644 --- a/maloja/malojauri.py +++ b/maloja/malojauri.py @@ -4,7 +4,7 @@ import urllib import math # this also sets defaults! -def uri_to_internal(keys,forceTrack=False,forceArtist=False,api=False): +def uri_to_internal(keys,forceTrack=False,forceArtist=False,forceAlbum=False,api=False): # output: # 1 keys that define the filtered object like artist or track @@ -12,12 +12,23 @@ def uri_to_internal(keys,forceTrack=False,forceArtist=False,api=False): # 3 keys that define interal time ranges # 4 keys that define amount limits + type = None + if forceTrack: type = "track" + if forceArtist: type = "artist" + if forceAlbum: type = "album" + + if not type and "title" in keys: type = "track" + if not type and "albumtitle" in keys: type = "album" + if not type and "artist" in keys: type = "artist" + # 1 - if "title" in keys and not forceArtist: + if type == "track": filterkeys = {"track":{"artists":keys.getall("artist"),"title":keys.get("title")}} - elif "artist" in keys and not forceTrack: + elif type == "artist": filterkeys = {"artist":keys.get("artist")} if "associated" in keys: filterkeys["associated"] = True + elif type == "album": + filterkeys = {"album":{"artists":keys.getall("artist"),"title":keys.get("title") or keys.get("albumtitle")}} else: filterkeys = {} @@ -84,6 +95,10 @@ def internal_to_uri(keys): for a in keys["track"]["artists"]: urikeys.append("artist",a) urikeys.append("title",keys["track"]["title"]) + elif "album" in keys: + for a in keys["album"]["artists"]: + urikeys.append("artist",a) + urikeys.append("albumtitle",keys["album"]["title"]) #time if "timerange" in keys: From e7b1cb469d615cedcae766cfa9122c3b0e86cddd Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 17:22:16 +0200 Subject: [PATCH 06/18] Implemented several functions for albums --- maloja/database/__init__.py | 41 +++++++++++++++++++++++++- maloja/database/cached.py | 22 ++++++++++++-- maloja/database/sqldb.py | 57 ++++++++++++++++++++++++++++++++++++- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index e8acca5..6a73247 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -93,7 +93,7 @@ def incoming_scrobble(rawscrobble,fix=True,client=None,api=None,dbconn=None): log(f"Incoming scrobble [Client: {client} | API: {api}]: {rawscrobble}") scrobbledict = rawscrobble_to_scrobbledict(rawscrobble, fix, client) - albumupdate = (malojaconf["ALBUM_INFORMATION_TRUST"] == 'last') + albumupdate = (malojaconfig["ALBUM_INFORMATION_TRUST"] == 'last') sqldb.add_scrobble(scrobbledict,update_album=albumupdate,dbconn=dbconn) @@ -267,6 +267,15 @@ def get_charts_tracks(dbconn=None,**keys): result = sqldb.count_scrobbles_by_track(since=since,to=to,dbconn=dbconn) return result +@waitfordb +def get_charts_albums(dbconn=None,**keys): + (since,to) = keys.get('timerange').timestamps() + if 'artist' in keys: + result = sqldb.count_scrobbles_by_album_of_artist(since=since,to=to,artist=keys['artist'],dbconn=dbconn) + else: + result = sqldb.count_scrobbles_by_album(since=since,to=to,dbconn=dbconn) + return result + @waitfordb def get_pulse(dbconn=None,**keys): @@ -421,6 +430,36 @@ def track_info(dbconn=None,**keys): } +@waitfordb +def album_info(dbconn=None,**keys): + + album = keys.get('album') + if album is None: raise exceptions.MissingEntityParameter() + + album_id = sqldb.get_album_id(album,dbconn=dbconn) + album = sqldb.get_album(album_id,dbconn=dbconn) + + alltimecharts = get_charts_albums(timerange=alltime(),dbconn=dbconn) + + #scrobbles = get_scrobbles_num(track=track,timerange=alltime()) + + c = [e for e in alltimecharts if e["album"] == album][0] + scrobbles = c["scrobbles"] + position = c["rank"] + + return { + "album":album, + "scrobbles":scrobbles, + "position":position, + "medals":{ + "gold": [year for year in cached.medals_albums if album_id in cached.medals_albums[year]['gold']], + "silver": [year for year in cached.medals_albums if album_id in cached.medals_albums[year]['silver']], + "bronze": [year for year in cached.medals_albums if album_id in cached.medals_albums[year]['bronze']], + }, + "topweeks":len([e for e in cached.weekly_topalbums if e == album_id]), + "id":album_id + } + def get_predefined_rulesets(dbconn=None): validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" diff --git a/maloja/database/cached.py b/maloja/database/cached.py index ea39a29..d1c0a26 100644 --- a/maloja/database/cached.py +++ b/maloja/database/cached.py @@ -14,16 +14,21 @@ medals_artists = { medals_tracks = { # year: {'gold':[],'silver':[],'bronze':[]} } +medals_albums = { + # year: {'gold':[],'silver':[],'bronze':[]} +} weekly_topartists = [] weekly_toptracks = [] +weekly_topalbums = [] @runyearly def update_medals(): - global medals_artists, medals_tracks + global medals_artists, medals_tracks, medals_albums medals_artists.clear() medals_tracks.clear() + medals_albums.clear() with sqldb.engine.begin() as conn: for year in mjt.ranges(step="year"): @@ -31,11 +36,14 @@ def update_medals(): charts_artists = sqldb.count_scrobbles_by_artist(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=conn) charts_tracks = sqldb.count_scrobbles_by_track(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=conn) + charts_albums = sqldb.count_scrobbles_by_album(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=conn) entry_artists = {'gold':[],'silver':[],'bronze':[]} entry_tracks = {'gold':[],'silver':[],'bronze':[]} + entry_albums = {'gold':[],'silver':[],'bronze':[]} medals_artists[year.desc()] = entry_artists medals_tracks[year.desc()] = entry_tracks + medals_albums[year.desc()] = entry_albums for entry in charts_artists: if entry['rank'] == 1: entry_artists['gold'].append(entry['artist_id']) @@ -47,6 +55,11 @@ def update_medals(): elif entry['rank'] == 2: entry_tracks['silver'].append(entry['track_id']) elif entry['rank'] == 3: entry_tracks['bronze'].append(entry['track_id']) else: break + for entry in charts_albums: + if entry['rank'] == 1: entry_albums['gold'].append(entry['album_id']) + elif entry['rank'] == 2: entry_albums['silver'].append(entry['album_id']) + elif entry['rank'] == 3: entry_albums['bronze'].append(entry['album_id']) + else: break @@ -54,9 +67,10 @@ def update_medals(): @rundaily def update_weekly(): - global weekly_topartists, weekly_toptracks + global weekly_topartists, weekly_toptracks, weekly_topalbums weekly_topartists.clear() weekly_toptracks.clear() + weekly_topalbums.clear() with sqldb.engine.begin() as conn: for week in mjt.ranges(step="week"): @@ -65,6 +79,7 @@ def update_weekly(): charts_artists = sqldb.count_scrobbles_by_artist(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=conn) charts_tracks = sqldb.count_scrobbles_by_track(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=conn) + charts_albums = sqldb.count_scrobbles_by_album(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=conn) for entry in charts_artists: if entry['rank'] == 1: weekly_topartists.append(entry['artist_id']) @@ -72,3 +87,6 @@ def update_weekly(): for entry in charts_tracks: if entry['rank'] == 1: weekly_toptracks.append(entry['track_id']) else: break + for entry in charts_albums: + if entry['rank'] == 1: weekly_topalbums.append(entry['album_id']) + else: break diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index 4b33dbe..a86bc32 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -246,7 +246,7 @@ def albums_db_to_dict(rows,dbconn=None): ] def album_db_to_dict(row,dbconn=None): - return albums_db_to_dict([row],dbconn=dbconn) + return albums_db_to_dict([row],dbconn=dbconn)[0] @@ -814,6 +814,35 @@ def count_scrobbles_by_track(since,to,resolve_ids=True,dbconn=None): result = rank(result,key='scrobbles') return result +@cached_wrapper +@connection_provider +def count_scrobbles_by_album(since,to,resolve_ids=True,dbconn=None): + + jointable = sql.join( + DB['scrobbles'], + DB['tracks'], + DB['scrobbles'].c.track_id == DB['tracks'].c.id + ) + + op = sql.select( + sql.func.count(sql.func.distinct(DB['scrobbles'].c.timestamp)).label('count'), + DB['tracks'].c.album_id + ).select_from(jointable).where( + DB['scrobbles'].c.timestamp<=to, + DB['scrobbles'].c.timestamp>=since, + DB['tracks'].c.album_id != None + ).group_by(DB['tracks'].c.album_id).order_by(sql.desc('count')) + result = dbconn.execute(op).all() + + if resolve_ids: + counts = [row.count for row in result] + albums = get_albums_map([row.album_id for row in result],dbconn=dbconn) + result = [{'scrobbles':row.count,'album':albums[row.album_id]} for row in result] + else: + result = [{'scrobbles':row.count,'album_id':row.album_id} for row in result] + result = rank(result,key='scrobbles') + return result + @cached_wrapper @connection_provider def count_scrobbles_by_track_of_artist(since,to,artist,dbconn=None): @@ -909,6 +938,22 @@ def get_artists_map(artist_ids,dbconn=None): return artists +@cached_wrapper_individual +@connection_provider +def get_albums_map(album_ids,dbconn=None): + op = DB['albums'].select().where( + DB['albums'].c.id.in_(album_ids) + ) + result = dbconn.execute(op).all() + + albums = {} + result = list(result) + # this will get a list of albumdicts in the correct order of our rows + albumdicts = albums_db_to_dict(result,dbconn=dbconn) + for row,albumdict in zip(result,albumdicts): + albums[row.id] = albumdict + return albums + ### associations @cached_wrapper @@ -975,6 +1020,16 @@ def get_artist(id,dbconn=None): artistinfo = result[0] return artist_db_to_dict(artistinfo,dbconn=dbconn) +@cached_wrapper +@connection_provider +def get_album(id,dbconn=None): + op = DB['albums'].select().where( + DB['albums'].c.id==id + ) + result = dbconn.execute(op).all() + + albuminfo = result[0] + return album_db_to_dict(albuminfo,dbconn=dbconn) @cached_wrapper @connection_provider From 6d55d605353548db95de03f3c158b269fbab4fd6 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 17:36:13 +0200 Subject: [PATCH 07/18] Implemented more album functions --- maloja/database/__init__.py | 10 ++++++++++ maloja/database/sqldb.py | 27 +++++++++++++++++++++++++-- maloja/images.py | 5 +++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index 6a73247..45211ed 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -220,6 +220,8 @@ def get_scrobbles(dbconn=None,**keys): result = sqldb.get_scrobbles_of_artist(artist=keys['artist'],since=since,to=to,dbconn=dbconn) elif 'track' in keys: result = sqldb.get_scrobbles_of_track(track=keys['track'],since=since,to=to,dbconn=dbconn) + elif 'album' in keys: + result = sqldb.get_scrobbles_of_album(album=keys['album'],since=since,to=to,dbconn=dbconn) else: result = sqldb.get_scrobbles(since=since,to=to,dbconn=dbconn) #return result[keys['page']*keys['perpage']:(keys['page']+1)*keys['perpage']] @@ -312,6 +314,14 @@ def get_performance(dbconn=None,**keys): if c["artist"] == artist: rank = c["rank"] break + elif "album" in keys: + album = sqldb.get_album(sqldb.get_album_id(keys['album'],dbconn=dbconn),dbconn=dbconn) + charts = get_charts_albums(timerange=rng,dbconn=dbconn) + rank = None + for c in charts: + if c["album"] == album: + rank = c["rank"] + break else: raise exceptions.MissingEntityParameter() results.append({"range":rng,"rank":rank}) diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index a86bc32..36ccc1e 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -388,7 +388,7 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): if set(artist_ids) == set(match_artist_ids): #print("ID for",trackdict['title'],"was",row[0]) if 'album' in trackdict: - add_track_to_album(row.id,get_album_id(trackdict['album']),replace=update_album) + add_track_to_album(row.id,get_album_id(trackdict['album']),replace=update_album,dbconn=dbconn) return row.id if not create_new: return None @@ -408,7 +408,7 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): #print("Created",trackdict['title'],track_id) if 'album' in trackdict: - add_track_to_album(track_id,get_album_id(trackdict['album'])) + add_track_to_album(track_id,get_album_id(trackdict['album']),dbconn=dbconn) return track_id @cached_wrapper @@ -671,6 +671,29 @@ def get_scrobbles_of_track(track,since=None,to=None,resolve_references=True,dbco #result = [scrobble_db_to_dict(row) for row in result] return result +@cached_wrapper +@connection_provider +def get_scrobbles_of_album(album,since=None,to=None,resolve_references=True,dbconn=None): + + if since is None: since=0 + if to is None: to=now() + + album_id = get_album_id(album,dbconn=dbconn) + + jointable = sql.join(DB['scrobbles'],DB['tracks'],DB['scrobbles'].c.track_id == DB['tracks'].c.id) + + op = jointable.select().where( + DB['scrobbles'].c.timestamp<=to, + DB['scrobbles'].c.timestamp>=since, + DB['tracks'].c.album_id==album_id + ).order_by(sql.asc('timestamp')) + result = dbconn.execute(op).all() + + if resolve_references: + result = scrobbles_db_to_dict(result) + #result = [scrobble_db_to_dict(row) for row in result] + return result + @cached_wrapper @connection_provider def get_scrobbles(since=None,to=None,resolve_references=True,dbconn=None): diff --git a/maloja/images.py b/maloja/images.py index f1c569a..103b0f1 100644 --- a/maloja/images.py +++ b/maloja/images.py @@ -115,6 +115,11 @@ def get_artist_image(artist=None,artist_id=None): return f"/image?type=artist&id={artist_id}" +def get_album_image(album=None,album_id=None): + if album_id is None: + album_id = database.sqldb.get_album_id(album) + + return f"/image?type=album&id={album_id}" resolve_semaphore = BoundedSemaphore(8) From 1086dfee25b02e0d09e8e3116d728530ff932d1b Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 18:16:15 +0200 Subject: [PATCH 08/18] Implemented and changed more album stuff --- maloja/database/__init__.py | 17 +++++++++++++++++ maloja/database/sqldb.py | 10 +++++----- maloja/malojauri.py | 4 ++-- maloja/pkg_global/conf.py | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index 45211ed..fb9f48b 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -235,6 +235,8 @@ def get_scrobbles_num(dbconn=None,**keys): result = len(sqldb.get_scrobbles_of_artist(artist=keys['artist'],since=since,to=to,resolve_references=False,dbconn=dbconn)) elif 'track' in keys: result = len(sqldb.get_scrobbles_of_track(track=keys['track'],since=since,to=to,resolve_references=False,dbconn=dbconn)) + elif 'album' in keys: + result = len(sqldb.get_scrobbles_of_album(album=keys['album'],since=since,to=to,resolve_references=False,dbconn=dbconn)) else: result = sqldb.get_scrobbles_num(since=since,to=to,dbconn=dbconn) return result @@ -359,6 +361,21 @@ def get_top_tracks(dbconn=None,**keys): return results +@waitfordb +def get_top_albums(dbconn=None,**keys): + + rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]}) + results = [] + + for rng in rngs: + try: + res = get_charts_albums(timerange=rng,dbconn=dbconn)[0] + results.append({"range":rng,"album":res["album"],"scrobbles":res["scrobbles"]}) + except Exception: + results.append({"range":rng,"album":None,"scrobbles":0}) + + return results + @waitfordb def artist_info(dbconn=None,**keys): diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index 36ccc1e..01e2975 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -166,7 +166,7 @@ def connection_provider(func): # "artists":list, # "title":string, # "album":{ -# "title":string, +# "albumtitle":string, # "artists":list # }, # "length":None @@ -240,7 +240,7 @@ def albums_db_to_dict(rows,dbconn=None): return [ { "artists":artists[row.id], - "title":row.albtitle, + "albumtitle":row.albtitle, } for row in rows ] @@ -279,8 +279,8 @@ def artist_dict_to_db(info,dbconn=None): def album_dict_to_db(info,dbconn=None): return { - "albtitle":info.get('title'), - "albtitle_normalized":normalize_name(info.get('title')) + "albtitle":info.get('albumtitle'), + "albtitle_normalized":normalize_name(info.get('albumtitle')) } @@ -441,7 +441,7 @@ def get_artist_id(artistname,create_new=True,dbconn=None): @cached_wrapper @connection_provider def get_album_id(albumdict,create_new=True,dbconn=None): - ntitle = normalize_name(albumdict['title']) + ntitle = normalize_name(albumdict['albumtitle']) artist_ids = [get_artist_id(a,dbconn=dbconn) for a in albumdict['artists']] artist_ids = list(set(artist_ids)) diff --git a/maloja/malojauri.py b/maloja/malojauri.py index b18ca9f..e2675cc 100644 --- a/maloja/malojauri.py +++ b/maloja/malojauri.py @@ -28,7 +28,7 @@ def uri_to_internal(keys,forceTrack=False,forceArtist=False,forceAlbum=False,api filterkeys = {"artist":keys.get("artist")} if "associated" in keys: filterkeys["associated"] = True elif type == "album": - filterkeys = {"album":{"artists":keys.getall("artist"),"title":keys.get("title") or keys.get("albumtitle")}} + filterkeys = {"album":{"artists":keys.getall("artist"),"albumtitle":keys.get("title") or keys.get("albumtitle")}} else: filterkeys = {} @@ -98,7 +98,7 @@ def internal_to_uri(keys): elif "album" in keys: for a in keys["album"]["artists"]: urikeys.append("artist",a) - urikeys.append("albumtitle",keys["album"]["title"]) + urikeys.append("albumtitle",keys["album"]["albumtitle"]) #time if "timerange" in keys: diff --git a/maloja/pkg_global/conf.py b/maloja/pkg_global/conf.py index f11d569..164cfa3 100644 --- a/maloja/pkg_global/conf.py +++ b/maloja/pkg_global/conf.py @@ -177,7 +177,7 @@ malojaconfig = Configuration( }, "Database":{ - "album_information_trust":(tp.Choice({'first':"First",'last':"Last"}),"Album Information Authority","first", "Whether to trust the first album information that is sent with a track or update every time a different album is sent"), + "album_information_trust":(tp.Choice({'first':"First",'last':"Last",'majority':"Majority"}), "Album Information Authority","first", "Whether to trust the first album information that is sent with a track or update every time a different album is sent"), "invalid_artists":(tp.Set(tp.String()), "Invalid Artists", ["[Unknown Artist]","Unknown Artist","Spotify"], "Artists that should be discarded immediately"), "remove_from_title":(tp.Set(tp.String()), "Remove from Title", ["(Original Mix)","(Radio Edit)","(Album Version)","(Explicit Version)","(Bonus Track)"], "Phrases that should be removed from song titles"), "delimiters_feat":(tp.Set(tp.String()), "Featuring Delimiters", ["ft.","ft","feat.","feat","featuring"], "Delimiters used for extra artists, even when in the title field"), From 1a43aa302d63ec3b158bbfbae3cfae924363a5d8 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 18:32:45 +0200 Subject: [PATCH 09/18] Updated API tests --- dev/testing/Maloja.postman_collection.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dev/testing/Maloja.postman_collection.json b/dev/testing/Maloja.postman_collection.json index 7ca23a4..852bd92 100644 --- a/dev/testing/Maloja.postman_collection.json +++ b/dev/testing/Maloja.postman_collection.json @@ -189,7 +189,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.title1}}\"\n}" + "raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.title1}}\",\n \"album\": \"{{data.album}}\",\n \"albumartists\":[\n \"{{data.artist1}}\",\n \"{{data.artist3}}\"\n ]\n}" }, "url": { "raw": "{{url}}/apis/mlj_1/newscrobble", @@ -219,7 +219,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"key\": \"{{api_key}}\",\n \"artists\": [\"{{data.artist1}}\",\"{{data.artist2}}\"],\n \"title\": \"{{data.title1}}\"\n}" + "raw": "{\n \"key\": \"{{api_key}}\",\n \"artists\": [\"{{data.artist1}}\",\"{{data.artist2}}\"],\n \"title\": \"{{data.title1}}\",\n \"album\": \"{{data.album}}\",\n \"albumartists\":[\n \"{{data.artist1}}\",\n \"{{data.artist3}}\"\n ]\n}" }, "url": { "raw": "{{url}}/apis/mlj_1/newscrobble", @@ -867,6 +867,11 @@ "key": "data.title3", "value": "One in a Million" }, + { + "key": "data.album", + "value": "The Epic Collection", + "type": "default" + }, { "key": "data.timestamp1", "value": "" From 69b456dc73e1108e374091cddd07337a7390463e Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 18:41:49 +0200 Subject: [PATCH 10/18] Scrobbling fixes --- maloja/apis/native_v1.py | 10 +++++++--- maloja/database/__init__.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/maloja/apis/native_v1.py b/maloja/apis/native_v1.py index 4e9c49f..cd031a2 100644 --- a/maloja/apis/native_v1.py +++ b/maloja/apis/native_v1.py @@ -494,19 +494,23 @@ def post_scrobble( 'artists':result['track']['artists'], 'title':result['track']['title'] }, - 'desc':f"Scrobbled {result['track']['title']} by {', '.join(result['track']['artists'])}" + 'desc':f"Scrobbled {result['track']['title']} by {', '.join(result['track']['artists'])}", + 'warnings':[] } if extra_kwargs: - responsedict['warnings'] = [ + responsedict['warnings'] += [ {'type':'invalid_keyword_ignored','value':k, 'desc':"This key was not recognized by the server and has been discarded."} for k in extra_kwargs ] if artist and artists: - responsedict['warnings'] = [ + responsedict['warnings'] += [ {'type':'mixed_schema','value':['artist','artists'], 'desc':"These two fields are meant as alternative methods to submit information. Use of both is discouraged, but works at the moment."} ] + + if len(responsedict['warnings']) == 0: del responsedict['warnings'] + return responsedict diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index fb9f48b..9204367 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -143,7 +143,7 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None): "artists":scrobbleinfo.get('track_artists'), "title":scrobbleinfo.get('track_title'), "album":{ - "title":scrobbleinfo.get('album_title') or scrobbleinfo.get('album_name'), + "albumtitle":scrobbleinfo.get('album_title'), "artists":scrobbleinfo.get('album_artists') }, "length":scrobbleinfo.get('track_length') @@ -152,7 +152,7 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None): "origin":f"client:{client}" if client else "generic", "extra":{ k:scrobbleinfo[k] for k in scrobbleinfo if k not in - ['scrobble_time','track_artists','track_title','track_length','scrobble_duration','album_title','album_name','album_artists'] + ['scrobble_time','track_artists','track_title','track_length','scrobble_duration','album_title','album_artists'] }, "rawscrobble":rawscrobble } From fd9987ec35ce033b8aa3b1f54a01d6f687234dd1 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 19:58:12 +0200 Subject: [PATCH 11/18] 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"], } From 99cb8f4c647018d0a27ce80438bd140b0280e9c3 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 19:58:25 +0200 Subject: [PATCH 12/18] Fixed DB locking --- maloja/database/sqldb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index 01e2975..918fa43 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -388,7 +388,7 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): if set(artist_ids) == set(match_artist_ids): #print("ID for",trackdict['title'],"was",row[0]) if 'album' in trackdict: - add_track_to_album(row.id,get_album_id(trackdict['album']),replace=update_album,dbconn=dbconn) + add_track_to_album(row.id,get_album_id(trackdict['album'],dbconn=dbconn),replace=update_album,dbconn=dbconn) return row.id if not create_new: return None @@ -408,7 +408,7 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): #print("Created",trackdict['title'],track_id) if 'album' in trackdict: - add_track_to_album(track_id,get_album_id(trackdict['album']),dbconn=dbconn) + add_track_to_album(track_id,get_album_id(trackdict['album'],dbconn=dbconn),dbconn=dbconn) return track_id @cached_wrapper From 27a2bc705a6fa78dbd52b5f1e2258074c48710b4 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 20:26:46 +0200 Subject: [PATCH 13/18] Added web pages for albums --- maloja/web/jinja/album.jinja | 154 ++++++++++++++++++ maloja/web/jinja/charts_albums.jinja | 52 ++++++ maloja/web/jinja/partials/awards_album.jinja | 42 +++++ maloja/web/jinja/partials/charts_albums.jinja | 56 +++++++ .../jinja/partials/charts_albums_tiles.jinja | 45 +++++ maloja/web/jinja/partials/top_albums.jinja | 30 ++++ maloja/web/jinja/snippets/entityrow.jinja | 8 +- .../jinja/snippets/filterdescription.jinja | 3 + maloja/web/jinja/snippets/links.jinja | 8 +- maloja/web/jinja/top_albums.jinja | 30 ++++ 10 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 maloja/web/jinja/album.jinja create mode 100644 maloja/web/jinja/charts_albums.jinja create mode 100644 maloja/web/jinja/partials/awards_album.jinja create mode 100644 maloja/web/jinja/partials/charts_albums.jinja create mode 100644 maloja/web/jinja/partials/charts_albums_tiles.jinja create mode 100644 maloja/web/jinja/partials/top_albums.jinja create mode 100644 maloja/web/jinja/top_albums.jinja diff --git a/maloja/web/jinja/album.jinja b/maloja/web/jinja/album.jinja new file mode 100644 index 0000000..4a6f855 --- /dev/null +++ b/maloja/web/jinja/album.jinja @@ -0,0 +1,154 @@ +{% extends "abstracts/base.jinja" %} +{% block title %}Maloja - {{ info.album.albumtitle }}{% endblock %} + +{% import 'snippets/links.jinja' as links %} + +{% block scripts %} + + +{% endblock %} + +{% set album = filterkeys.album %} +{% set info = dbc.album_info({'album':album}) %} + +{% set initialrange ='month' %} + + +{% set encodedalbum = mlj_uri.uriencode({'album':album}) %} + + +{% block icon_bar %} + {% if adminmode %} + {% include 'icons/edit.jinja' %} + {% include 'icons/merge.jinja' %} + {% include 'icons/merge_mark.jinja' %} + {% include 'icons/merge_cancel.jinja' %} + + {% endif %} +{% endblock %} + +{% block content %} + + + + +{% import 'partials/awards_album.jinja' as awards %} + + + + + + + +
+ {% if adminmode %} +
+ {% else %} +
+
+ {% endif %} +
+ {{ links.links(album.artists) }}
+

{{ info.album.albumtitle | e }}

+ {# awards.certs(album) #} + #{{ info.position }} +
+ +

+ {{ info['scrobbles'] }} Scrobbles +

+ + + + + + {{ awards.medals(info) }} + {{ awards.topweeks(info) }} + + +
+ + + + + + + + +
+

Pulse

+
+ {% for r in xranges %} + + {{ r.localisation }} + + {% if not loop.last %}|{% endif %} + {% endfor %} + +

+ + {% for r in xranges %} + + + + {% with limitkeys={"since":r.firstrange},delimitkeys={'step':r.identifier,'trail':1} %} + {% include 'partials/pulse.jinja' %} + {% endwith %} + + + {% endfor %} +
+ +

Performance

+
+ {% for r in xranges %} + + {{ r.localisation }} + + {% if not loop.last %}|{% endif %} + {% endfor %} + +

+ + {% for r in xranges %} + + + + {% with limitkeys={"since":r.firstrange},delimitkeys={'step':r.identifier,'trail':1} %} + {% include 'partials/performance.jinja' %} + {% endwith %} + + + {% endfor %} + +
+ + +

Last Scrobbles

+ +{% with amountkeys = {"perpage":15,"page":0} %} +{% include 'partials/scrobbles.jinja' %} +{% endwith %} + + +{% endblock %} diff --git a/maloja/web/jinja/charts_albums.jinja b/maloja/web/jinja/charts_albums.jinja new file mode 100644 index 0000000..7d2667a --- /dev/null +++ b/maloja/web/jinja/charts_albums.jinja @@ -0,0 +1,52 @@ +{% extends "abstracts/base.jinja" %} +{% block title %}Maloja - Album Charts{% endblock %} + +{% import 'snippets/links.jinja' as links %} + +{% block scripts %} + +{% endblock %} + +{% set charts = dbc.get_charts_albums(filterkeys,limitkeys) %} +{% set pages = math.ceil(charts.__len__() / amountkeys.perpage) %} +{% if charts[0] is defined %} + {% set topalbum = charts[0].album %} + {% set img = images.get_album_image(topalbum) %} +{% else %} + {% set img = "/favicon.png" %} +{% endif %} + + +{% block content %} + + + + + + +
+
+
+

Album Charts

View #1 Albums
+ {% if filterkeys.get('artist') is not none %}by {{ links.link(filterkeys.get('artist')) }}{% endif %} + {{ limitkeys.timerange.desc(prefix=True) }} +

+ {% with delimitkeys = {} %} + {% include 'snippets/timeselection.jinja' %} + {% endwith %} + +
+ +{% if settings['CHARTS_DISPLAY_TILES'] %} + {% include 'partials/charts_albums_tiles.jinja' %} +

+{% endif %} + +{% with compare=true %} +{% include 'partials/charts_albums.jinja' %} +{% endwith %} + +{% import 'snippets/pagination.jinja' as pagination %} +{{ pagination.pagination(filterkeys,limitkeys,delimitkeys,amountkeys,pages) }} + +{% endblock %} diff --git a/maloja/web/jinja/partials/awards_album.jinja b/maloja/web/jinja/partials/awards_album.jinja new file mode 100644 index 0000000..5ded047 --- /dev/null +++ b/maloja/web/jinja/partials/awards_album.jinja @@ -0,0 +1,42 @@ +{% macro medals(info) %} + + +{% for year in info.medals.gold -%} + + {{ year }} + +{%- endfor %} +{% for year in info.medals.silver -%} + + {{ year }} + +{%- endfor %} +{% for year in info.medals.bronze -%} + + {{ year }} + +{%- endfor %} + +{%- endmacro %} + + + + + + + + +{% macro topweeks(info) %} + +{% set encodedtrack = mlj_uri.uriencode({'album':info.album}) %} + + + + {% if info.topweeks > 0 %} + + {{ info.topweeks }} + + {% endif %} + + +{%- endmacro %} diff --git a/maloja/web/jinja/partials/charts_albums.jinja b/maloja/web/jinja/partials/charts_albums.jinja new file mode 100644 index 0000000..b6bcc0f --- /dev/null +++ b/maloja/web/jinja/partials/charts_albums.jinja @@ -0,0 +1,56 @@ +{% import 'snippets/links.jinja' as links %} +{% import 'snippets/entityrow.jinja' as entityrow %} + +{% if charts is undefined %} + {% set charts = dbc.get_charts_albums(filterkeys,limitkeys) %} +{% endif %} +{% if compare %} + {% if compare is true %} + {% set compare = limitkeys.timerange.next(step=-1) %} + {% if compare is none %}{% set compare = False %}{% endif %} + {% endif %} + {% if compare %} + {% set prevalbums = dbc.get_charts_albums(filterkeys,{'timerange':compare}) %} + + {% set lastranks = {} %} + {% for t in prevalbums %} + {% if lastranks.update({"|".join(t.album.artists)+"||"+t.album.albumtitle:t.rank}) %}{% endif %} + {% endfor %} + + {% for t in charts %} + {% if "|".join(t.album.artists)+"||"+t.album.albumtitle in lastranks %} + {% if t.update({'last_rank':lastranks["|".join(t.album.artists)+"||"+t.album.albumtitle]}) %}{% endif %} + {% endif %} + {% endfor %} + {% endif %} +{% endif %} + +{% set firstindex = amountkeys.page * amountkeys.perpage %} +{% set lastindex = firstindex + amountkeys.perpage %} + +{% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %} + + {% for e in charts %} + {% if loop.index0 >= firstindex and loop.index0 < lastindex %} + + + + + {% if compare %} + {% if e.last_rank is undefined %} + {% elif e.last_rank < e.rank %} + {% elif e.last_rank > e.rank %} + {% elif e.last_rank == e.rank %} + {% endif %} + {% endif %} + + + {{ entityrow.row(e['album']) }} + + + + + + {% endif %} + {% endfor %} +
{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}🆕↘↗➡{{ links.link_scrobbles([{'album':e.album,'timerange':limitkeys.timerange}],amount=e['scrobbles']) }}{{ links.link_scrobbles([{'album':e.album,'timerange':limitkeys.timerange}],percent=e['scrobbles']*100/maxbar) }}
diff --git a/maloja/web/jinja/partials/charts_albums_tiles.jinja b/maloja/web/jinja/partials/charts_albums_tiles.jinja new file mode 100644 index 0000000..56e3c4e --- /dev/null +++ b/maloja/web/jinja/partials/charts_albums_tiles.jinja @@ -0,0 +1,45 @@ +{% import 'snippets/links.jinja' as links %} + + +{% if charts is undefined %} + {% set charts = dbc.get_charts_albums(limitkeys) %} +{% endif %} + +{% set charts_14 = charts | fixlength(14) %} +{% set charts_cycler = cycler(*charts_14) %} + + + + +{% for segment in range(3) %} + {% if charts_14[0] is none and loop.first %} + {% include 'icons/nodata.jinja' %} + {% else %} + + {% endif %} +{% endfor %} +
+ {% set segmentsize = segment+1 %} + + {% for row in range(segmentsize) -%} + + {% for col in range(segmentsize) %} + {% set entry = charts_cycler.next() %} + {% if entry is not none %} + {% set album = entry.album %} + {% set rank = entry.rank %} + + {% else -%} + + {%- endif -%} + {%- endfor -%} + + {%- endfor -%} +
+ +
+ #{{ rank }} {{ album.title }} +
+
+
+
diff --git a/maloja/web/jinja/partials/top_albums.jinja b/maloja/web/jinja/partials/top_albums.jinja new file mode 100644 index 0000000..cdedd6a --- /dev/null +++ b/maloja/web/jinja/partials/top_albums.jinja @@ -0,0 +1,30 @@ +{% import 'snippets/links.jinja' as links %} +{% import 'snippets/entityrow.jinja' as entityrow %} + +{% set ranges = dbc.get_top_albums(filterkeys,limitkeys,delimitkeys) %} + +{% set maxbar = ranges|map(attribute="scrobbles")|max|default(1) %} +{% if maxbar < 1 %}{% set maxbar = 1 %}{% endif %} + + + {% for e in ranges %} + + {% set thisrange = e.range %} + {% set album = e.album %} + + + + {% if album is none %} + + + + + {% else %} + {{ entityrow.row(album) }} + + + {% endif %} + + + {% endfor %} +
{{ thisrange.desc() }}
n/a0{{ links.link_scrobbles([{'album':album,'timerange':thisrange}],amount=e.scrobbles) }} {{ links.link_scrobbles([{'album':album,'timerange':thisrange}],percent=e.scrobbles*100/maxbar) }}
diff --git a/maloja/web/jinja/snippets/entityrow.jinja b/maloja/web/jinja/snippets/entityrow.jinja index 7601fea..2721f0c 100644 --- a/maloja/web/jinja/snippets/entityrow.jinja +++ b/maloja/web/jinja/snippets/entityrow.jinja @@ -2,8 +2,10 @@ {% import 'snippets/links.jinja' as links %} -{% if entity is mapping and 'artists' in entity %} +{% if entity is mapping and 'title' in entity %} {% set img = images.get_track_image(entity) %} +{% elif entity is mapping and 'albumtitle' in entity %} + {% set img = images.get_album_image(entity) %} {% else %} {% set img = images.get_artist_image(entity) %} {% endif %} @@ -20,6 +22,10 @@ {{ links.links(entity.artists) }} – {{ links.link(entity) }} +{% elif entity is mapping and 'albumtitle' in entity %} + + {{ links.links(entity.artists) }} – {{ links.link(entity) }} + {% else %} {{ links.link(entity) }} {% if counting != [] %} diff --git a/maloja/web/jinja/snippets/filterdescription.jinja b/maloja/web/jinja/snippets/filterdescription.jinja index 820d501..4a5135f 100644 --- a/maloja/web/jinja/snippets/filterdescription.jinja +++ b/maloja/web/jinja/snippets/filterdescription.jinja @@ -7,6 +7,9 @@ {% elif filterkeys.get('track') is not none %} of {{ links.link(filterkeys.get('track')) }} by {{ links.links(filterkeys["track"]["artists"]) }} + {% elif filterkeys.get('album') is not none %} + of {{ links.link(filterkeys.get('album')) }} + by {{ links.links(filterkeys["album"]["artists"]) }} {% endif %} {{ limitkeys.timerange.desc(prefix=True) }} diff --git a/maloja/web/jinja/snippets/links.jinja b/maloja/web/jinja/snippets/links.jinja index e53a39f..e49b2c3 100644 --- a/maloja/web/jinja/snippets/links.jinja +++ b/maloja/web/jinja/snippets/links.jinja @@ -1,6 +1,6 @@ {% macro link(entity) -%} {% if entity is mapping and 'artists' in entity %} - {% set name = entity.title %} + {% set name = entity.title or entity.albumtitle %} {% else %} {% set name = entity %} {% endif %} @@ -17,7 +17,9 @@ {% macro url(entity) %} - {% if entity is mapping and 'artists' in entity -%} + {% if entity is mapping and 'albumtitle' in entity -%} + {{ mlj_uri.create_uri("/album",{'album':entity}) }} + {% elif entity is mapping and 'artists' in entity -%} {{ mlj_uri.create_uri("/track",{'track':entity}) }} {%- else -%} {{ mlj_uri.create_uri("/artist",{'artist':entity}) }} @@ -43,6 +45,8 @@ {% if 'track' in filterkeys %} {% set url = mlj_uri.create_uri("/charts_tracks",{'timerange':timerange}) %} + {% elif 'album' in filterkeys %} + {% set url = mlj_uri.create_uri("/charts_albums",{'timerange':timerange}) %} {% elif 'artist' in filterkeys %} {% set url = mlj_uri.create_uri("/charts_artists",{'timerange':timerange}) %} {% endif %} diff --git a/maloja/web/jinja/top_albums.jinja b/maloja/web/jinja/top_albums.jinja new file mode 100644 index 0000000..c7a0782 --- /dev/null +++ b/maloja/web/jinja/top_albums.jinja @@ -0,0 +1,30 @@ +{% extends "abstracts/base.jinja" %} +{% block title %}Maloja - #1 Albums{% endblock %} + + + + +{% set entries = dbc.get_top_albums(filterkeys,limitkeys,delimitkeys) %} +{% set repr = entries | find_representative('album','scrobbles') %} +{% set img = "/favicon.png" if repr is none else images.get_album_image(repr.album) %} + + +{% block content %} + + + + + +
+
+
+

#1 Albums


+ {{ limitkeys.timerange.desc(prefix=True) }} + +

+ {% include 'snippets/timeselection.jinja' %} +
+ + {% include 'partials/top_albums.jinja' %} + +{% endblock %} From 4a0bd4b97ebf6a4d76f484b83ed603f3345ee930 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 20:58:50 +0200 Subject: [PATCH 14/18] More album functionality --- maloja/database/__init__.py | 2 ++ maloja/database/sqldb.py | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index 9204367..5fbd5ed 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -267,6 +267,8 @@ def get_charts_tracks(dbconn=None,**keys): (since,to) = keys.get('timerange').timestamps() if 'artist' in keys: result = sqldb.count_scrobbles_by_track_of_artist(since=since,to=to,artist=keys['artist'],dbconn=dbconn) + elif 'album' in keys: + result = sqldb.count_scrobbles_by_track_of_album(since=since,to=to,album=keys['album'],dbconn=dbconn) else: result = sqldb.count_scrobbles_by_track(since=since,to=to,dbconn=dbconn) return result diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index 918fa43..633fc52 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -213,11 +213,12 @@ def scrobble_db_to_dict(row,dbconn=None): def tracks_db_to_dict(rows,dbconn=None): artists = get_artists_of_tracks(set(row.id for row in rows),dbconn=dbconn) + albums = get_albums_map(set(row.album_id for row in rows),dbconn=dbconn) return [ { "artists":artists[row.id], "title":row.title, - #"album": + "album":albums.get(row.album_id), "length":row.length } for row in rows @@ -387,7 +388,7 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): #print("required artists",artist_ids,"this match",match_artist_ids) if set(artist_ids) == set(match_artist_ids): #print("ID for",trackdict['title'],"was",row[0]) - if 'album' in trackdict: + if trackdict.get('album'): add_track_to_album(row.id,get_album_id(trackdict['album'],dbconn=dbconn),replace=update_album,dbconn=dbconn) return row.id @@ -407,7 +408,7 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None): result = dbconn.execute(op) #print("Created",trackdict['title'],track_id) - if 'album' in trackdict: + if trackdict.get('album'): add_track_to_album(track_id,get_album_id(trackdict['album'],dbconn=dbconn),dbconn=dbconn) return track_id @@ -896,6 +897,35 @@ def count_scrobbles_by_track_of_artist(since,to,artist,dbconn=None): return result +@cached_wrapper +@connection_provider +def count_scrobbles_by_track_of_album(since,to,album,dbconn=None): + + album_id = get_album_id(album,dbconn=dbconn) + + jointable = sql.join( + DB['scrobbles'], + DB['tracks'], + DB['scrobbles'].c.track_id == DB['tracks'].c.id + ) + + op = sql.select( + sql.func.count(sql.func.distinct(DB['scrobbles'].c.timestamp)).label('count'), + DB['scrobbles'].c.track_id + ).select_from(jointable).filter( + DB['scrobbles'].c.timestamp<=to, + DB['scrobbles'].c.timestamp>=since, + DB['tracks'].c.album_id==album_id + ).group_by(DB['scrobbles'].c.track_id).order_by(sql.desc('count')) + result = dbconn.execute(op).all() + + + counts = [row.count for row in result] + tracks = get_tracks_map([row.track_id for row in result],dbconn=dbconn) + result = [{'scrobbles':row.count,'track':tracks[row.track_id]} for row in result] + result = rank(result,key='scrobbles') + return result + ### functions that get mappings for several entities -> rows From add79916042c963309d76b6909b146a61b4376f8 Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 21:00:39 +0200 Subject: [PATCH 15/18] Unified filter desciptions --- maloja/web/jinja/charts_albums.jinja | 4 ++-- maloja/web/jinja/charts_artists.jinja | 4 +++- maloja/web/jinja/charts_tracks.jinja | 4 ++-- maloja/web/jinja/snippets/links.jinja | 2 +- maloja/web/jinja/top_albums.jinja | 3 ++- maloja/web/jinja/top_artists.jinja | 3 ++- maloja/web/jinja/top_tracks.jinja | 3 ++- 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/maloja/web/jinja/charts_albums.jinja b/maloja/web/jinja/charts_albums.jinja index 7d2667a..c442df2 100644 --- a/maloja/web/jinja/charts_albums.jinja +++ b/maloja/web/jinja/charts_albums.jinja @@ -2,6 +2,7 @@ {% block title %}Maloja - Album Charts{% endblock %} {% import 'snippets/links.jinja' as links %} +{% import 'snippets/filterdescription.jinja' as filterdesc %} {% block scripts %} @@ -26,8 +27,7 @@

Album Charts

View #1 Albums
- {% if filterkeys.get('artist') is not none %}by {{ links.link(filterkeys.get('artist')) }}{% endif %} - {{ limitkeys.timerange.desc(prefix=True) }} + {{ filterdesc.desc(filterkeys,limitkeys) }}

{% with delimitkeys = {} %} {% include 'snippets/timeselection.jinja' %} diff --git a/maloja/web/jinja/charts_artists.jinja b/maloja/web/jinja/charts_artists.jinja index 87f51ff..627f38c 100644 --- a/maloja/web/jinja/charts_artists.jinja +++ b/maloja/web/jinja/charts_artists.jinja @@ -1,6 +1,8 @@ {% extends "abstracts/base.jinja" %} {% block title %}Maloja - Artist Charts{% endblock %} +{% import 'snippets/filterdescription.jinja' as filterdesc %} + {% block scripts %} {% endblock %} @@ -25,7 +27,7 @@

Artist Charts

View #1 Artists
- {{ limitkeys.timerange.desc(prefix=True) }} + {{ filterdesc.desc(filterkeys,limitkeys) }}

{% with delimitkeys = {} %} {% include 'snippets/timeselection.jinja' %} diff --git a/maloja/web/jinja/charts_tracks.jinja b/maloja/web/jinja/charts_tracks.jinja index 17b2f20..d265c3a 100644 --- a/maloja/web/jinja/charts_tracks.jinja +++ b/maloja/web/jinja/charts_tracks.jinja @@ -2,6 +2,7 @@ {% block title %}Maloja - Track Charts{% endblock %} {% import 'snippets/links.jinja' as links %} +{% import 'snippets/filterdescription.jinja' as filterdesc %} {% block scripts %} @@ -26,8 +27,7 @@

Track Charts

View #1 Tracks
- {% if filterkeys.get('artist') is not none %}by {{ links.link(filterkeys.get('artist')) }}{% endif %} - {{ limitkeys.timerange.desc(prefix=True) }} + {{ filterdesc.desc(filterkeys,limitkeys) }}

{% with delimitkeys = {} %} {% include 'snippets/timeselection.jinja' %} diff --git a/maloja/web/jinja/snippets/links.jinja b/maloja/web/jinja/snippets/links.jinja index e49b2c3..280ed75 100644 --- a/maloja/web/jinja/snippets/links.jinja +++ b/maloja/web/jinja/snippets/links.jinja @@ -1,5 +1,5 @@ {% macro link(entity) -%} - {% if entity is mapping and 'artists' in entity %} + {% if entity is mapping and 'title' in entity or 'albumtitle' in entity %} {% set name = entity.title or entity.albumtitle %} {% else %} {% set name = entity %} diff --git a/maloja/web/jinja/top_albums.jinja b/maloja/web/jinja/top_albums.jinja index c7a0782..df1a416 100644 --- a/maloja/web/jinja/top_albums.jinja +++ b/maloja/web/jinja/top_albums.jinja @@ -1,6 +1,7 @@ {% extends "abstracts/base.jinja" %} {% block title %}Maloja - #1 Albums{% endblock %} +{% import 'snippets/filterdescription.jinja' as filterdesc %} @@ -17,7 +18,7 @@

#1 Albums


- {{ limitkeys.timerange.desc(prefix=True) }} + {{ filterdesc.desc(filterkeys,limitkeys) }}

{% include 'snippets/timeselection.jinja' %} diff --git a/maloja/web/jinja/top_artists.jinja b/maloja/web/jinja/top_artists.jinja index 175d213..ce4bf24 100644 --- a/maloja/web/jinja/top_artists.jinja +++ b/maloja/web/jinja/top_artists.jinja @@ -1,6 +1,7 @@ {% extends "abstracts/base.jinja" %} {% block title %}Maloja - #1 Artists{% endblock %} +{% import 'snippets/filterdescription.jinja' as filterdesc %} @@ -17,7 +18,7 @@

#1 Artists


- {{ limitkeys.timerange.desc(prefix=True) }} + {{ filterdesc.desc(filterkeys,limitkeys) }}

{% include 'snippets/timeselection.jinja' %} diff --git a/maloja/web/jinja/top_tracks.jinja b/maloja/web/jinja/top_tracks.jinja index 42ae78d..0f98dd3 100644 --- a/maloja/web/jinja/top_tracks.jinja +++ b/maloja/web/jinja/top_tracks.jinja @@ -1,6 +1,7 @@ {% extends "abstracts/base.jinja" %} {% block title %}Maloja - #1 Tracks{% endblock %} +{% import 'snippets/filterdescription.jinja' as filterdesc %} @@ -17,7 +18,7 @@

#1 Tracks


- {{ limitkeys.timerange.desc(prefix=True) }} + {{ filterdesc.desc(filterkeys,limitkeys) }}

{% include 'snippets/timeselection.jinja' %} From 4d1f810e9213fde98da035d10588a9ecbfa2a9de Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 21:48:44 +0200 Subject: [PATCH 16/18] Improved support for artistless albums --- maloja/database/__init__.py | 3 ++- maloja/database/sqldb.py | 4 ++-- maloja/images.py | 2 +- maloja/malojauri.py | 4 ++-- maloja/pkg_global/conf.py | 1 + maloja/web/jinja/partials/charts_albums.jinja | 6 +++--- maloja/web/jinja/snippets/links.jinja | 10 +++++++--- 7 files changed, 18 insertions(+), 12 deletions(-) diff --git a/maloja/database/__init__.py b/maloja/database/__init__.py index 5fbd5ed..7cb3c6b 100644 --- a/maloja/database/__init__.py +++ b/maloja/database/__init__.py @@ -132,7 +132,8 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None): scrobbleinfo = {**rawscrobble} if fix: scrobbleinfo['track_artists'],scrobbleinfo['track_title'] = cla.fullclean(scrobbleinfo['track_artists'],scrobbleinfo['track_title']) - scrobbleinfo['album_artists'] = cla.parseArtists(scrobbleinfo['album_artists']) + if scrobbleinfo.get('album_artists'): + scrobbleinfo['album_artists'] = cla.parseArtists(scrobbleinfo['album_artists']) scrobbleinfo['scrobble_time'] = scrobbleinfo.get('scrobble_time') or int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp()) diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index 633fc52..b4fd2c2 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -240,7 +240,7 @@ def albums_db_to_dict(rows,dbconn=None): artists = get_artists_of_albums(set(row.id for row in rows),dbconn=dbconn) return [ { - "artists":artists[row.id], + "artists":artists.get(row.id), "albumtitle":row.albtitle, } for row in rows @@ -443,7 +443,7 @@ def get_artist_id(artistname,create_new=True,dbconn=None): @connection_provider def get_album_id(albumdict,create_new=True,dbconn=None): ntitle = normalize_name(albumdict['albumtitle']) - artist_ids = [get_artist_id(a,dbconn=dbconn) for a in albumdict['artists']] + artist_ids = [get_artist_id(a,dbconn=dbconn) for a in albumdict.get('artists') or []] artist_ids = list(set(artist_ids)) diff --git a/maloja/images.py b/maloja/images.py index f92e91f..4c2fa22 100644 --- a/maloja/images.py +++ b/maloja/images.py @@ -226,7 +226,7 @@ def get_all_possible_filenames(artist=None,track=None,album=None): 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']] + title, artists = clean(album['albumtitle']), [clean(a) for a in album.get('artists') or []] superfolder = "albums/" elif artist: artist = clean(artist) diff --git a/maloja/malojauri.py b/maloja/malojauri.py index e2675cc..350a7e4 100644 --- a/maloja/malojauri.py +++ b/maloja/malojauri.py @@ -28,7 +28,7 @@ def uri_to_internal(keys,forceTrack=False,forceArtist=False,forceAlbum=False,api filterkeys = {"artist":keys.get("artist")} if "associated" in keys: filterkeys["associated"] = True elif type == "album": - filterkeys = {"album":{"artists":keys.getall("artist"),"albumtitle":keys.get("title") or keys.get("albumtitle")}} + filterkeys = {"album":{"artists":keys.getall("artist"),"albumtitle":keys.get("albumtitle") or keys.get("title")}} else: filterkeys = {} @@ -96,7 +96,7 @@ def internal_to_uri(keys): urikeys.append("artist",a) urikeys.append("title",keys["track"]["title"]) elif "album" in keys: - for a in keys["album"]["artists"]: + for a in keys["album"].get("artists") or []: urikeys.append("artist",a) urikeys.append("albumtitle",keys["album"]["albumtitle"]) diff --git a/maloja/pkg_global/conf.py b/maloja/pkg_global/conf.py index 164cfa3..7224970 100644 --- a/maloja/pkg_global/conf.py +++ b/maloja/pkg_global/conf.py @@ -192,6 +192,7 @@ malojaconfig = Configuration( "default_step_pulse":(tp.Choice({'year':'Year','month':"Month",'week':'Week','day':'Day'}), "Default Pulse Step", "month"), "charts_display_tiles":(tp.Boolean(), "Display Chart Tiles", False), "display_art_icons":(tp.Boolean(), "Display Album/Artist Icons", True), + "default_album_artist":(tp.String(), "Default Albumartist", "Various Artists"), "discourage_cpu_heavy_stats":(tp.Boolean(), "Discourage CPU-heavy stats", False, "Prevent visitors from mindlessly clicking on CPU-heavy options. Does not actually disable them for malicious actors!"), "use_local_images":(tp.Boolean(), "Use Local Images", True), #"local_image_rotate":(tp.Integer(), "Local Image Rotate", 3600), diff --git a/maloja/web/jinja/partials/charts_albums.jinja b/maloja/web/jinja/partials/charts_albums.jinja index b6bcc0f..a779e62 100644 --- a/maloja/web/jinja/partials/charts_albums.jinja +++ b/maloja/web/jinja/partials/charts_albums.jinja @@ -14,12 +14,12 @@ {% set lastranks = {} %} {% for t in prevalbums %} - {% if lastranks.update({"|".join(t.album.artists)+"||"+t.album.albumtitle:t.rank}) %}{% endif %} + {% if lastranks.update({"|".join(t.album.artists or [])+"||"+t.album.albumtitle:t.rank}) %}{% endif %} {% endfor %} {% for t in charts %} - {% if "|".join(t.album.artists)+"||"+t.album.albumtitle in lastranks %} - {% if t.update({'last_rank':lastranks["|".join(t.album.artists)+"||"+t.album.albumtitle]}) %}{% endif %} + {% if "|".join(t.album.artists or [])+"||"+t.album.albumtitle in lastranks %} + {% if t.update({'last_rank':lastranks["|".join(t.album.artists or [])+"||"+t.album.albumtitle]}) %}{% endif %} {% endif %} {% endfor %} {% endif %} diff --git a/maloja/web/jinja/snippets/links.jinja b/maloja/web/jinja/snippets/links.jinja index 280ed75..3b5c29d 100644 --- a/maloja/web/jinja/snippets/links.jinja +++ b/maloja/web/jinja/snippets/links.jinja @@ -9,9 +9,13 @@ {%- endmacro %} {% macro links(entities) -%} - {% for entity in entities -%} - {{ link(entity) }}{{ ", " if not loop.last }} - {%- endfor %} + {% if entities is none or entities == [] %} + {{ settings["DEFAULT_ALBUM_ARTIST"] }} + {% else %} + {% for entity in entities -%} + {{ link(entity) }}{{ ", " if not loop.last }} + {%- endfor %} + {% endif %} {%- endmacro %} From dc2a8a54f99134d2530fb4494b4251537e782efc Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 21:49:21 +0200 Subject: [PATCH 17/18] Implemented additional album functions --- maloja/database/sqldb.py | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/maloja/database/sqldb.py b/maloja/database/sqldb.py index b4fd2c2..1c0e0c5 100644 --- a/maloja/database/sqldb.py +++ b/maloja/database/sqldb.py @@ -867,6 +867,83 @@ def count_scrobbles_by_album(since,to,resolve_ids=True,dbconn=None): result = rank(result,key='scrobbles') return result +@cached_wrapper +@connection_provider +# this ranks the albums of that artist, not albums the artist appears on - even scrobbles +# of tracks the artist is not part of! +def count_scrobbles_by_album_of_artist(since,to,artist,resolve_ids=True,dbconn=None): + + artist_id = get_artist_id(artist,dbconn=dbconn) + + jointable = sql.join( + DB['scrobbles'], + DB['tracks'], + DB['scrobbles'].c.track_id == DB['tracks'].c.id + ) + jointable2 = sql.join( + jointable, + DB['albumartists'], + DB['tracks'].c.album_id == DB['albumartists'].c.album_id + ) + + op = sql.select( + sql.func.count(sql.func.distinct(DB['scrobbles'].c.timestamp)).label('count'), + DB['tracks'].c.album_id + ).select_from(jointable2).where( + DB['scrobbles'].c.timestamp<=to, + DB['scrobbles'].c.timestamp>=since, + DB['albumartists'].c.artist_id == artist_id + ).group_by(DB['tracks'].c.album_id).order_by(sql.desc('count')) + result = dbconn.execute(op).all() + + if resolve_ids: + counts = [row.count for row in result] + albums = get_albums_map([row.album_id for row in result],dbconn=dbconn) + result = [{'scrobbles':row.count,'album':albums[row.album_id]} for row in result] + else: + result = [{'scrobbles':row.count,'album_id':row.album_id} for row in result] + result = rank(result,key='scrobbles') + return result + +@cached_wrapper +@connection_provider +# this ranks the tracks of that artist by the album they appear on - even when the album +# is not the artist's +def count_scrobbles_of_artist_by_album(since,to,artist,resolve_ids=True,dbconn=None): + + artist_id = get_artist_id(artist,dbconn=dbconn) + + jointable = sql.join( + DB['scrobbles'], + DB['trackartists'], + DB['scrobbles'].c.track_id == DB['trackartists'].c.track_id + ) + jointable2 = sql.join( + jointable, + DB['tracks'], + DB['scrobbles'].c.track_id == DB['tracks'].c.id + ) + + op = sql.select( + sql.func.count(sql.func.distinct(DB['scrobbles'].c.timestamp)).label('count'), + DB['tracks'].c.album_id + ).select_from(jointable2).where( + DB['scrobbles'].c.timestamp<=to, + DB['scrobbles'].c.timestamp>=since, + DB['trackartists'].c.artist_id == artist_id + ).group_by(DB['tracks'].c.album_id).order_by(sql.desc('count')) + result = dbconn.execute(op).all() + + if resolve_ids: + counts = [row.count for row in result] + albums = get_albums_map([row.album_id for row in result],dbconn=dbconn) + result = [{'scrobbles':row.count,'album':albums[row.album_id]} for row in result] + else: + result = [{'scrobbles':row.count,'album_id':row.album_id} for row in result] + result = rank(result,key='scrobbles') + return result + + @cached_wrapper @connection_provider def count_scrobbles_by_track_of_artist(since,to,artist,dbconn=None): From c7f392a74f42651a402545b71ad21b085300120b Mon Sep 17 00:00:00 2001 From: krateng Date: Tue, 28 Mar 2023 21:50:24 +0200 Subject: [PATCH 18/18] Created some interlinking with the new album pages --- maloja/web/jinja/album.jinja | 9 +++++++++ maloja/web/jinja/artist.jinja | 8 +++++++- maloja/web/jinja/track.jinja | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/maloja/web/jinja/album.jinja b/maloja/web/jinja/album.jinja index 4a6f855..6a16da3 100644 --- a/maloja/web/jinja/album.jinja +++ b/maloja/web/jinja/album.jinja @@ -76,6 +76,15 @@ +

Top Tracks

+ + +{% with amountkeys={"perpage":15,"page":0} %} +{% include 'partials/charts_tracks.jinja' %} +{% endwith %} + +
+ diff --git a/maloja/web/jinja/artist.jinja b/maloja/web/jinja/artist.jinja index acac7b1..6f3b635 100644 --- a/maloja/web/jinja/artist.jinja +++ b/maloja/web/jinja/artist.jinja @@ -90,8 +90,14 @@
-

Top Tracks

+

Top Albums

+{% with amountkeys={"perpage":15,"page":0} %} +{% include 'partials/charts_albums.jinja' %} +{% endwith %} + + +

Top Tracks

{% with amountkeys={"perpage":15,"page":0} %} {% include 'partials/charts_tracks.jinja' %} diff --git a/maloja/web/jinja/track.jinja b/maloja/web/jinja/track.jinja index 29cb404..3fbc909 100644 --- a/maloja/web/jinja/track.jinja +++ b/maloja/web/jinja/track.jinja @@ -64,6 +64,9 @@ {{ awards.certs(track) }} #{{ info.position }}
+ {% if info.track.album %} + from {{ links.link(info.track.album) }}
+ {% endif %}

{% if adminmode %}{% endif %}