mirror of
https://github.com/krateng/maloja.git
synced 2025-04-13 07:27:12 +03:00
Merge branch 'feature-albums' into next_minor_version
This commit is contained in:
commit
7eb2ae11aa
@ -2,6 +2,7 @@ minor_release_name: "Momo"
|
||||
3.2.0:
|
||||
notes:
|
||||
- "[Architecture] Switched to linuxserver.io container base image"
|
||||
- "[Feature] Added basic support for albums"
|
||||
- "[Performance] Improved image rendering"
|
||||
- "[Bugfix] Fixed configuration of time format"
|
||||
- "[Bugfix] Fixed search on manual scrobble page"
|
||||
|
@ -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": ""
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
@ -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
|
||||
|
||||
|
||||
@ -515,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
|
||||
@ -527,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 {
|
||||
|
@ -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 = (malojaconfig["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'])
|
||||
@ -130,8 +132,11 @@ 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'])
|
||||
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())
|
||||
|
||||
|
||||
# processed info to internal scrobble dict
|
||||
scrobbledict = {
|
||||
"time":scrobbleinfo.get('scrobble_time'),
|
||||
@ -139,7 +144,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'),
|
||||
"albumtitle":scrobbleinfo.get('album_title'),
|
||||
"artists":scrobbleinfo.get('album_artists')
|
||||
},
|
||||
"length":scrobbleinfo.get('track_length')
|
||||
@ -148,7 +153,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_artists']
|
||||
},
|
||||
"rawscrobble":rawscrobble
|
||||
}
|
||||
@ -216,6 +221,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']]
|
||||
@ -229,6 +236,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
|
||||
@ -259,10 +268,21 @@ 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
|
||||
|
||||
@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):
|
||||
|
||||
@ -299,6 +319,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})
|
||||
@ -336,6 +364,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):
|
||||
|
||||
@ -417,6 +460,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"
|
||||
|
@ -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
|
||||
|
@ -21,44 +21,72 @@ 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, {}),
|
||||
("album_id", sql.Integer, sql.ForeignKey('albums.id'), {})
|
||||
],
|
||||
'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 +166,7 @@ def connection_provider(func):
|
||||
# "artists":list,
|
||||
# "title":string,
|
||||
# "album":{
|
||||
# "name":string,
|
||||
# "albumtitle":string,
|
||||
# "artists":list
|
||||
# },
|
||||
# "length":None
|
||||
@ -185,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
|
||||
@ -207,18 +236,31 @@ 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.get(row.id),
|
||||
"albumtitle":row.albtitle,
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def album_db_to_dict(row,dbconn=None):
|
||||
return albums_db_to_dict([row],dbconn=dbconn)[0]
|
||||
|
||||
|
||||
|
||||
|
||||
### 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
|
||||
}
|
||||
@ -236,6 +278,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('albumtitle'),
|
||||
"albtitle_normalized":normalize_name(info.get('albumtitle'))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -244,17 +292,17 @@ def artist_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
|
||||
]
|
||||
|
||||
@ -285,11 +333,34 @@ def delete_scrobble(scrobble_id,dbconn=None):
|
||||
return True
|
||||
|
||||
|
||||
@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(
|
||||
*conditions
|
||||
).values(
|
||||
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))
|
||||
@ -310,18 +381,19 @@ 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]
|
||||
#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 trackdict.get('album'):
|
||||
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
|
||||
|
||||
|
||||
op = DB['tracks'].insert().values(
|
||||
**track_dict_to_db(trackdict,dbconn=dbconn)
|
||||
)
|
||||
@ -335,6 +407,9 @@ def get_track_id(trackdict,create_new=True,dbconn=None):
|
||||
)
|
||||
result = dbconn.execute(op)
|
||||
#print("Created",trackdict['title'],track_id)
|
||||
|
||||
if trackdict.get('album'):
|
||||
add_track_to_album(track_id,get_album_id(trackdict['album'],dbconn=dbconn),dbconn=dbconn)
|
||||
return track_id
|
||||
|
||||
@cached_wrapper
|
||||
@ -364,6 +439,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['albumtitle'])
|
||||
artist_ids = [get_artist_id(a,dbconn=dbconn) for a in albumdict.get('artists') or []]
|
||||
artist_ids = list(set(artist_ids))
|
||||
|
||||
|
||||
|
||||
|
||||
op = DB['albums'].select(
|
||||
# DB['albums'].c.id
|
||||
).where(
|
||||
DB['albums'].c.albtitle_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.album_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 album_id
|
||||
|
||||
|
||||
|
||||
|
||||
### Edit existing
|
||||
|
||||
|
||||
@ -544,6 +672,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):
|
||||
@ -687,6 +838,112 @@ 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
|
||||
# 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):
|
||||
@ -717,6 +974,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
|
||||
@ -734,6 +1020,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
|
||||
@ -769,6 +1068,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
|
||||
@ -835,6 +1150,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
|
||||
|
103
maloja/images.py
103
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)
|
||||
|
||||
@ -115,6 +122,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)
|
||||
@ -132,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)
|
||||
@ -176,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.get('artists') or []]
|
||||
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:
|
||||
@ -210,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 != "":
|
||||
@ -241,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 = []
|
||||
|
||||
@ -271,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")
|
||||
|
||||
@ -288,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)
|
||||
@ -303,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)
|
||||
|
@ -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"),"albumtitle":keys.get("albumtitle") or keys.get("title")}}
|
||||
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"].get("artists") or []:
|
||||
urikeys.append("artist",a)
|
||||
urikeys.append("albumtitle",keys["album"]["albumtitle"])
|
||||
|
||||
#time
|
||||
if "timerange" in keys:
|
||||
|
@ -177,6 +177,7 @@ malojaconfig = Configuration(
|
||||
|
||||
},
|
||||
"Database":{
|
||||
"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"),
|
||||
@ -191,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),
|
||||
|
@ -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 ""
|
||||
|
33
maloja/thirdparty/__init__.py
vendored
33
maloja/thirdparty/__init__.py
vendored
@ -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]:
|
||||
|
8
maloja/thirdparty/audiodb.py
vendored
8
maloja/thirdparty/audiodb.py
vendored
@ -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
|
||||
|
11
maloja/thirdparty/deezer.py
vendored
11
maloja/thirdparty/deezer.py
vendored
@ -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
|
||||
|
9
maloja/thirdparty/lastfm.py
vendored
9
maloja/thirdparty/lastfm.py
vendored
@ -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"
|
||||
|
2
maloja/thirdparty/musicbrainz.py
vendored
2
maloja/thirdparty/musicbrainz.py
vendored
@ -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()
|
||||
|
4
maloja/thirdparty/spotify.py
vendored
4
maloja/thirdparty/spotify.py
vendored
@ -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"],
|
||||
}
|
||||
|
163
maloja/web/jinja/album.jinja
Normal file
163
maloja/web/jinja/album.jinja
Normal file
@ -0,0 +1,163 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - {{ info.album.albumtitle }}{% endblock %}
|
||||
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/rangeselect.js"></script>
|
||||
<script src="/edit.js"></script>
|
||||
{% 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' %}
|
||||
<script>showValidMergeIcons();</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
const entity_id = {{ info.id }};
|
||||
const entity_type = 'album';
|
||||
const entity_name = {{ album.albumtitle | tojson }};
|
||||
</script>
|
||||
|
||||
|
||||
{% import 'partials/awards_album.jinja' as awards %}
|
||||
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
{% if adminmode %}
|
||||
<div
|
||||
class="changeable-image" data-uploader="b64=>upload('{{ encodedalbum }}',b64)"
|
||||
style="background-image:url('{{ images.get_album_image(album) }}');"
|
||||
title="Drag & Drop to upload new image"
|
||||
></div>
|
||||
{% else %}
|
||||
<div style="background-image:url('{{ images.get_album_image(album) }}');">
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text">
|
||||
<span>{{ links.links(album.artists) }}</span><br/>
|
||||
<h1 id="main_entity_name" class="headerwithextra">{{ info.album.albumtitle | e }}</h1>
|
||||
{# awards.certs(album) #}
|
||||
<span class="rank"><a href="/charts_albums?max=100">#{{ info.position }}</a></span>
|
||||
<br/>
|
||||
|
||||
<p class="stats">
|
||||
<a href="{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}">{{ info['scrobbles'] }} Scrobbles</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{ awards.medals(info) }}
|
||||
{{ awards.topweeks(info) }}
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_tracks",filterkeys) }}'>Top Tracks</a></h2>
|
||||
|
||||
|
||||
{% with amountkeys={"perpage":15,"page":0} %}
|
||||
{% include 'partials/charts_tracks.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
<table class="twopart">
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<h2 class="headerwithextra"><a href='{{ mlj_uri.create_uri("/pulse",filterkeys) }}'>Pulse</a></h2>
|
||||
<br/>
|
||||
{% for r in xranges %}
|
||||
<span
|
||||
onclick="showRangeManual('pulse','{{ r.identifier }}')"
|
||||
class="stat_selector_pulse selector_pulse_{{ r.identifier }}"
|
||||
style="{{ 'opacity:0.5;' if initialrange==r.identifier else '' }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{% if not loop.last %}|{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<br/><br/>
|
||||
|
||||
{% for r in xranges %}
|
||||
|
||||
<span
|
||||
class="stat_module_pulse pulse_{{ r.identifier }}"
|
||||
style="{{ 'display:none;' if initialrange!=r.identifier else '' }}"
|
||||
>
|
||||
|
||||
{% with limitkeys={"since":r.firstrange},delimitkeys={'step':r.identifier,'trail':1} %}
|
||||
{% include 'partials/pulse.jinja' %}
|
||||
{% endwith %}
|
||||
</span>
|
||||
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<!-- We use the same classes / function calls here because we want it to switch together with pulse -->
|
||||
<h2 class="headerwithextra"><a href='{{ mlj_uri.create_uri("/performance",filterkeys) }}'>Performance</a></h2>
|
||||
<br/>
|
||||
{% for r in xranges %}
|
||||
<span
|
||||
onclick="showRangeManual('pulse','{{ r.identifier }}')"
|
||||
class="stat_selector_pulse selector_pulse_{{ r.identifier }}"
|
||||
style="{{ 'opacity:0.5;' if initialrange==r.identifier else '' }}">
|
||||
{{ r.localisation }}
|
||||
</span>
|
||||
{% if not loop.last %}|{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<br/><br/>
|
||||
|
||||
{% for r in xranges %}
|
||||
|
||||
<span
|
||||
class="stat_module_pulse pulse_{{ r.identifier }}"
|
||||
style="{{ 'display:none;' if initialrange!=r.identifier else '' }}"
|
||||
>
|
||||
|
||||
{% with limitkeys={"since":r.firstrange},delimitkeys={'step':r.identifier,'trail':1} %}
|
||||
{% include 'partials/performance.jinja' %}
|
||||
{% endwith %}
|
||||
</span>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}'>Last Scrobbles</a></h2>
|
||||
|
||||
{% with amountkeys = {"perpage":15,"page":0} %}
|
||||
{% include 'partials/scrobbles.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
{% endblock %}
|
@ -90,8 +90,14 @@
|
||||
</table>
|
||||
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_tracks",filterkeys) }}'>Top Tracks</a></h2>
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_albums",filterkeys) }}'>Top Albums</a></h2>
|
||||
|
||||
{% with amountkeys={"perpage":15,"page":0} %}
|
||||
{% include 'partials/charts_albums.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_tracks",filterkeys) }}'>Top Tracks</a></h2>
|
||||
|
||||
{% with amountkeys={"perpage":15,"page":0} %}
|
||||
{% include 'partials/charts_tracks.jinja' %}
|
||||
|
52
maloja/web/jinja/charts_albums.jinja
Normal file
52
maloja/web/jinja/charts_albums.jinja
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - Album Charts{% endblock %}
|
||||
|
||||
{% import 'snippets/links.jinja' as links %}
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/datechange.js" async></script>
|
||||
{% 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 %}
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('{{ img }}')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Album Charts</h1><a href="/top_albums"><span>View #1 Albums</span></a><br/>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
<br/><br/>
|
||||
{% with delimitkeys = {} %}
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if settings['CHARTS_DISPLAY_TILES'] %}
|
||||
{% include 'partials/charts_albums_tiles.jinja' %}
|
||||
<br/><br/>
|
||||
{% endif %}
|
||||
|
||||
{% with compare=true %}
|
||||
{% include 'partials/charts_albums.jinja' %}
|
||||
{% endwith %}
|
||||
|
||||
{% import 'snippets/pagination.jinja' as pagination %}
|
||||
{{ pagination.pagination(filterkeys,limitkeys,delimitkeys,amountkeys,pages) }}
|
||||
|
||||
{% endblock %}
|
@ -1,6 +1,8 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - Artist Charts{% endblock %}
|
||||
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/datechange.js" async></script>
|
||||
{% endblock %}
|
||||
@ -25,7 +27,7 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Artist Charts</h1><a href="/top_artists"><span>View #1 Artists</span></a><br/>
|
||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
<br/><br/>
|
||||
{% with delimitkeys = {} %}
|
||||
{% include 'snippets/timeselection.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 %}
|
||||
<script src="/datechange.js" async></script>
|
||||
@ -26,8 +27,7 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Track Charts</h1><a href="/top_tracks"><span>View #1 Tracks</span></a><br/>
|
||||
{% if filterkeys.get('artist') is not none %}by {{ links.link(filterkeys.get('artist')) }}{% endif %}
|
||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
<br/><br/>
|
||||
{% with delimitkeys = {} %}
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
|
42
maloja/web/jinja/partials/awards_album.jinja
Normal file
42
maloja/web/jinja/partials/awards_album.jinja
Normal file
@ -0,0 +1,42 @@
|
||||
{% macro medals(info) %}
|
||||
|
||||
<!-- MEDALS -->
|
||||
{% for year in info.medals.gold -%}
|
||||
<a title="Best Album in {{ year }}" class="hidelink medal shiny gold" href='/charts_albums?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
{% for year in info.medals.silver -%}
|
||||
<a title="Second best Album in {{ year }}" class="hidelink medal shiny silver" href='/charts_albums?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
{% for year in info.medals.bronze -%}
|
||||
<a title="Third best Album in {{ year }}" class="hidelink medal shiny bronze" href='/charts_albums?max=50&in={{ year }}'>
|
||||
<span>{{ year }}</span>
|
||||
</a>
|
||||
{%- endfor %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% macro topweeks(info) %}
|
||||
|
||||
{% set encodedtrack = mlj_uri.uriencode({'album':info.album}) %}
|
||||
|
||||
<!-- TOPWEEKS -->
|
||||
<span>
|
||||
{% if info.topweeks > 0 %}
|
||||
<a title="{{ info.topweeks }} weeks on #1" href="/performance?{{ encodedalbum }}&step=week">
|
||||
<img class="star" src="/media/star.png" />{{ info.topweeks }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{%- endmacro %}
|
56
maloja/web/jinja/partials/charts_albums.jinja
Normal file
56
maloja/web/jinja/partials/charts_albums.jinja
Normal file
@ -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 or [])+"||"+t.album.albumtitle:t.rank}) %}{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for t in charts %}
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
|
||||
{% set firstindex = amountkeys.page * amountkeys.perpage %}
|
||||
{% set lastindex = firstindex + amountkeys.perpage %}
|
||||
|
||||
{% set maxbar = charts[0]['scrobbles'] if charts != [] else 0 %}
|
||||
<table class='list'>
|
||||
{% for e in charts %}
|
||||
{% if loop.index0 >= firstindex and loop.index0 < lastindex %}
|
||||
<tr>
|
||||
<!-- Rank -->
|
||||
<td class="rank">{%if loop.changed(e.scrobbles) %}#{{ e.rank }}{% endif %}</td>
|
||||
<!-- Rank change -->
|
||||
{% if compare %}
|
||||
{% if e.last_rank is undefined %}<td class='rankup' title='New'>🆕</td>
|
||||
{% elif e.last_rank < e.rank %}<td class='rankdown' title='Down from #{{ e.last_rank }}'>↘</td>
|
||||
{% elif e.last_rank > e.rank %}<td class='rankup' title='Up from #{{ e.last_rank }}'>↗</td>
|
||||
{% elif e.last_rank == e.rank %}<td class='ranksame' title='Unchanged'>➡</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- artist -->
|
||||
{{ entityrow.row(e['album']) }}
|
||||
|
||||
<!-- scrobbles -->
|
||||
<td class="amount">{{ links.link_scrobbles([{'album':e.album,'timerange':limitkeys.timerange}],amount=e['scrobbles']) }}</td>
|
||||
<td class="bar">{{ links.link_scrobbles([{'album':e.album,'timerange':limitkeys.timerange}],percent=e['scrobbles']*100/maxbar) }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
45
maloja/web/jinja/partials/charts_albums_tiles.jinja
Normal file
45
maloja/web/jinja/partials/charts_albums_tiles.jinja
Normal file
@ -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) %}
|
||||
|
||||
|
||||
|
||||
<table class="tiles_top"><tr>
|
||||
{% for segment in range(3) %}
|
||||
{% if charts_14[0] is none and loop.first %}
|
||||
{% include 'icons/nodata.jinja' %}
|
||||
{% else %}
|
||||
<td>
|
||||
{% set segmentsize = segment+1 %}
|
||||
<table class="tiles_{{ segmentsize }}x{{ segmentsize }} tiles_sub">
|
||||
{% for row in range(segmentsize) -%}
|
||||
<tr>
|
||||
{% for col in range(segmentsize) %}
|
||||
{% set entry = charts_cycler.next() %}
|
||||
{% if entry is not none %}
|
||||
{% set album = entry.album %}
|
||||
{% set rank = entry.rank %}
|
||||
<td>
|
||||
<a href="{{ links.url(album) }}">
|
||||
<div class="lazy" data-bg="{{ images.get_album_image(album) }}"'>
|
||||
<span class='stats'>#{{ rank }}</span> <span>{{ album.title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
{% else -%}
|
||||
<td></td>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</table>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr></table>
|
30
maloja/web/jinja/partials/top_albums.jinja
Normal file
30
maloja/web/jinja/partials/top_albums.jinja
Normal file
@ -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 %}
|
||||
|
||||
<table class="list">
|
||||
{% for e in ranges %}
|
||||
|
||||
{% set thisrange = e.range %}
|
||||
{% set album = e.album %}
|
||||
<tr>
|
||||
<td>{{ thisrange.desc() }}</td>
|
||||
|
||||
{% if album is none %}
|
||||
<td><div></div></td>
|
||||
<td class='stats'>n/a</td>
|
||||
<td class='amount'>0</td>
|
||||
<td class='bar'></td>
|
||||
{% else %}
|
||||
{{ entityrow.row(album) }}
|
||||
<td class='amount'>{{ links.link_scrobbles([{'album':album,'timerange':thisrange}],amount=e.scrobbles) }}</td>
|
||||
<td class='bar'> {{ links.link_scrobbles([{'album':album,'timerange':thisrange}],percent=e.scrobbles*100/maxbar) }}</td>
|
||||
{% endif %}
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
@ -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 @@
|
||||
<td class='track'>
|
||||
<span class='artist_in_trackcolumn'>{{ links.links(entity.artists) }}</span> – {{ links.link(entity) }}
|
||||
</td>
|
||||
{% elif entity is mapping and 'albumtitle' in entity %}
|
||||
<td class='album'>
|
||||
<span class='artist_in_trackcolumn'>{{ links.links(entity.artists) }}</span> – {{ links.link(entity) }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class='artist'>{{ links.link(entity) }}
|
||||
{% if counting != [] %}
|
||||
|
@ -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) }}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% macro link(entity) -%}
|
||||
{% if entity is mapping and 'artists' in entity %}
|
||||
{% set name = entity.title %}
|
||||
{% if entity is mapping and 'title' in entity or 'albumtitle' in entity %}
|
||||
{% set name = entity.title or entity.albumtitle %}
|
||||
{% else %}
|
||||
{% set name = entity %}
|
||||
{% endif %}
|
||||
@ -9,15 +9,21 @@
|
||||
{%- 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 %}
|
||||
|
||||
|
||||
|
||||
{% 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 +49,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 %}
|
||||
|
31
maloja/web/jinja/top_albums.jinja
Normal file
31
maloja/web/jinja/top_albums.jinja
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - #1 Albums{% endblock %}
|
||||
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
<!-- find representative -->
|
||||
|
||||
{% 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 %}
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('{{ img }}')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>#1 Albums</h1><br/>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
|
||||
<br/><br/>
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% include 'partials/top_albums.jinja' %}
|
||||
|
||||
{% endblock %}
|
@ -1,6 +1,7 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - #1 Artists{% endblock %}
|
||||
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
<!-- find representative -->
|
||||
|
||||
@ -17,7 +18,7 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>#1 Artists</h1><br/>
|
||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
|
||||
<br/><br/>
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - #1 Tracks{% endblock %}
|
||||
|
||||
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||
|
||||
<!-- find representative -->
|
||||
|
||||
@ -17,7 +18,7 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>#1 Tracks</h1><br/>
|
||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
||||
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||
|
||||
<br/><br/>
|
||||
{% include 'snippets/timeselection.jinja' %}
|
||||
|
@ -64,6 +64,9 @@
|
||||
{{ awards.certs(track) }}
|
||||
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
|
||||
<br/>
|
||||
{% if info.track.album %}
|
||||
from {{ links.link(info.track.album) }}<br/>
|
||||
{% endif %}
|
||||
|
||||
<p class="stats">
|
||||
{% if adminmode %}<button type="button" onclick="scrobble('{{ encodedtrack }}')">Scrobble now</button>{% endif %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user