mirror of
https://github.com/krateng/maloja.git
synced 2025-04-15 00:10:32 +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:
|
3.2.0:
|
||||||
notes:
|
notes:
|
||||||
- "[Architecture] Switched to linuxserver.io container base image"
|
- "[Architecture] Switched to linuxserver.io container base image"
|
||||||
|
- "[Feature] Added basic support for albums"
|
||||||
- "[Performance] Improved image rendering"
|
- "[Performance] Improved image rendering"
|
||||||
- "[Bugfix] Fixed configuration of time format"
|
- "[Bugfix] Fixed configuration of time format"
|
||||||
- "[Bugfix] Fixed search on manual scrobble page"
|
- "[Bugfix] Fixed search on manual scrobble page"
|
||||||
|
@ -189,7 +189,7 @@
|
|||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"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": {
|
"url": {
|
||||||
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
||||||
@ -219,7 +219,7 @@
|
|||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"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": {
|
"url": {
|
||||||
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
||||||
@ -867,6 +867,11 @@
|
|||||||
"key": "data.title3",
|
"key": "data.title3",
|
||||||
"value": "One in a Million"
|
"value": "One in a Million"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "data.album",
|
||||||
|
"value": "The Epic Collection",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "data.timestamp1",
|
"key": "data.timestamp1",
|
||||||
"value": ""
|
"value": ""
|
||||||
|
@ -75,7 +75,7 @@ class Listenbrainz(APIHandler):
|
|||||||
self.scrobble({
|
self.scrobble({
|
||||||
'track_artists':[artiststr],
|
'track_artists':[artiststr],
|
||||||
'track_title':titlestr,
|
'track_title':titlestr,
|
||||||
'album_name':albumstr,
|
'album_title':albumstr,
|
||||||
'scrobble_time':timestamp,
|
'scrobble_time':timestamp,
|
||||||
'track_length': additional.get("duration"),
|
'track_length': additional.get("duration"),
|
||||||
**extrafields
|
**extrafields
|
||||||
|
@ -470,7 +470,7 @@ def post_scrobble(
|
|||||||
rawscrobble = {
|
rawscrobble = {
|
||||||
'track_artists':(artist or []) + artists,
|
'track_artists':(artist or []) + artists,
|
||||||
'track_title':title,
|
'track_title':title,
|
||||||
'album_name':album,
|
'album_title':album,
|
||||||
'album_artists':albumartists,
|
'album_artists':albumartists,
|
||||||
'scrobble_duration':duration,
|
'scrobble_duration':duration,
|
||||||
'track_length':length,
|
'track_length':length,
|
||||||
@ -494,19 +494,23 @@ def post_scrobble(
|
|||||||
'artists':result['track']['artists'],
|
'artists':result['track']['artists'],
|
||||||
'title':result['track']['title']
|
'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:
|
if extra_kwargs:
|
||||||
responsedict['warnings'] = [
|
responsedict['warnings'] += [
|
||||||
{'type':'invalid_keyword_ignored','value':k,
|
{'type':'invalid_keyword_ignored','value':k,
|
||||||
'desc':"This key was not recognized by the server and has been discarded."}
|
'desc':"This key was not recognized by the server and has been discarded."}
|
||||||
for k in extra_kwargs
|
for k in extra_kwargs
|
||||||
]
|
]
|
||||||
if artist and artists:
|
if artist and artists:
|
||||||
responsedict['warnings'] = [
|
responsedict['warnings'] += [
|
||||||
{'type':'mixed_schema','value':['artist','artists'],
|
{'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."}
|
'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
|
return responsedict
|
||||||
|
|
||||||
|
|
||||||
@ -515,7 +519,7 @@ def post_scrobble(
|
|||||||
@api.post("addpicture")
|
@api.post("addpicture")
|
||||||
@authenticated_function(alternate=api_key_correct,api=True)
|
@authenticated_function(alternate=api_key_correct,api=True)
|
||||||
@catch_exceptions
|
@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.
|
"""Uploads a new image for an artist or track.
|
||||||
|
|
||||||
param string b64: Base 64 representation of the image
|
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:
|
for a in artist:
|
||||||
keys.append("artist",a)
|
keys.append("artist",a)
|
||||||
if title is not None: keys.append("title",title)
|
if title is not None: keys.append("title",title)
|
||||||
|
elif albumtitle is not None: keys.append("albumtitle",albumtitle)
|
||||||
k_filter, _, _, _, _ = uri_to_internal(keys)
|
k_filter, _, _, _, _ = uri_to_internal(keys)
|
||||||
if "track" in k_filter: k_filter = k_filter["track"]
|
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)
|
url = images.set_image(b64,**k_filter)
|
||||||
|
|
||||||
return {
|
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}")
|
log(f"Incoming scrobble [Client: {client} | API: {api}]: {rawscrobble}")
|
||||||
|
|
||||||
scrobbledict = rawscrobble_to_scrobbledict(rawscrobble, fix, client)
|
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'])
|
proxy_scrobble_all(scrobbledict['track']['artists'],scrobbledict['track']['title'],scrobbledict['time'])
|
||||||
|
|
||||||
dbcache.invalidate_caches(scrobbledict['time'])
|
dbcache.invalidate_caches(scrobbledict['time'])
|
||||||
@ -130,8 +132,11 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None):
|
|||||||
scrobbleinfo = {**rawscrobble}
|
scrobbleinfo = {**rawscrobble}
|
||||||
if fix:
|
if fix:
|
||||||
scrobbleinfo['track_artists'],scrobbleinfo['track_title'] = cla.fullclean(scrobbleinfo['track_artists'],scrobbleinfo['track_title'])
|
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())
|
scrobbleinfo['scrobble_time'] = scrobbleinfo.get('scrobble_time') or int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
|
||||||
|
|
||||||
|
|
||||||
# processed info to internal scrobble dict
|
# processed info to internal scrobble dict
|
||||||
scrobbledict = {
|
scrobbledict = {
|
||||||
"time":scrobbleinfo.get('scrobble_time'),
|
"time":scrobbleinfo.get('scrobble_time'),
|
||||||
@ -139,7 +144,7 @@ def rawscrobble_to_scrobbledict(rawscrobble, fix=True, client=None):
|
|||||||
"artists":scrobbleinfo.get('track_artists'),
|
"artists":scrobbleinfo.get('track_artists'),
|
||||||
"title":scrobbleinfo.get('track_title'),
|
"title":scrobbleinfo.get('track_title'),
|
||||||
"album":{
|
"album":{
|
||||||
"name":scrobbleinfo.get('album_name'),
|
"albumtitle":scrobbleinfo.get('album_title'),
|
||||||
"artists":scrobbleinfo.get('album_artists')
|
"artists":scrobbleinfo.get('album_artists')
|
||||||
},
|
},
|
||||||
"length":scrobbleinfo.get('track_length')
|
"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",
|
"origin":f"client:{client}" if client else "generic",
|
||||||
"extra":{
|
"extra":{
|
||||||
k:scrobbleinfo[k] for k in scrobbleinfo if k not in
|
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
|
"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)
|
result = sqldb.get_scrobbles_of_artist(artist=keys['artist'],since=since,to=to,dbconn=dbconn)
|
||||||
elif 'track' in keys:
|
elif 'track' in keys:
|
||||||
result = sqldb.get_scrobbles_of_track(track=keys['track'],since=since,to=to,dbconn=dbconn)
|
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:
|
else:
|
||||||
result = sqldb.get_scrobbles(since=since,to=to,dbconn=dbconn)
|
result = sqldb.get_scrobbles(since=since,to=to,dbconn=dbconn)
|
||||||
#return result[keys['page']*keys['perpage']:(keys['page']+1)*keys['perpage']]
|
#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))
|
result = len(sqldb.get_scrobbles_of_artist(artist=keys['artist'],since=since,to=to,resolve_references=False,dbconn=dbconn))
|
||||||
elif 'track' in keys:
|
elif 'track' in keys:
|
||||||
result = len(sqldb.get_scrobbles_of_track(track=keys['track'],since=since,to=to,resolve_references=False,dbconn=dbconn))
|
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:
|
else:
|
||||||
result = sqldb.get_scrobbles_num(since=since,to=to,dbconn=dbconn)
|
result = sqldb.get_scrobbles_num(since=since,to=to,dbconn=dbconn)
|
||||||
return result
|
return result
|
||||||
@ -259,10 +268,21 @@ def get_charts_tracks(dbconn=None,**keys):
|
|||||||
(since,to) = keys.get('timerange').timestamps()
|
(since,to) = keys.get('timerange').timestamps()
|
||||||
if 'artist' in keys:
|
if 'artist' in keys:
|
||||||
result = sqldb.count_scrobbles_by_track_of_artist(since=since,to=to,artist=keys['artist'],dbconn=dbconn)
|
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:
|
else:
|
||||||
result = sqldb.count_scrobbles_by_track(since=since,to=to,dbconn=dbconn)
|
result = sqldb.count_scrobbles_by_track(since=since,to=to,dbconn=dbconn)
|
||||||
return result
|
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
|
@waitfordb
|
||||||
def get_pulse(dbconn=None,**keys):
|
def get_pulse(dbconn=None,**keys):
|
||||||
|
|
||||||
@ -299,6 +319,14 @@ def get_performance(dbconn=None,**keys):
|
|||||||
if c["artist"] == artist:
|
if c["artist"] == artist:
|
||||||
rank = c["rank"]
|
rank = c["rank"]
|
||||||
break
|
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:
|
else:
|
||||||
raise exceptions.MissingEntityParameter()
|
raise exceptions.MissingEntityParameter()
|
||||||
results.append({"range":rng,"rank":rank})
|
results.append({"range":rng,"rank":rank})
|
||||||
@ -336,6 +364,21 @@ def get_top_tracks(dbconn=None,**keys):
|
|||||||
|
|
||||||
return results
|
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
|
@waitfordb
|
||||||
def artist_info(dbconn=None,**keys):
|
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):
|
def get_predefined_rulesets(dbconn=None):
|
||||||
validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
@ -14,16 +14,21 @@ medals_artists = {
|
|||||||
medals_tracks = {
|
medals_tracks = {
|
||||||
# year: {'gold':[],'silver':[],'bronze':[]}
|
# year: {'gold':[],'silver':[],'bronze':[]}
|
||||||
}
|
}
|
||||||
|
medals_albums = {
|
||||||
|
# year: {'gold':[],'silver':[],'bronze':[]}
|
||||||
|
}
|
||||||
|
|
||||||
weekly_topartists = []
|
weekly_topartists = []
|
||||||
weekly_toptracks = []
|
weekly_toptracks = []
|
||||||
|
weekly_topalbums = []
|
||||||
|
|
||||||
@runyearly
|
@runyearly
|
||||||
def update_medals():
|
def update_medals():
|
||||||
|
|
||||||
global medals_artists, medals_tracks
|
global medals_artists, medals_tracks, medals_albums
|
||||||
medals_artists.clear()
|
medals_artists.clear()
|
||||||
medals_tracks.clear()
|
medals_tracks.clear()
|
||||||
|
medals_albums.clear()
|
||||||
|
|
||||||
with sqldb.engine.begin() as conn:
|
with sqldb.engine.begin() as conn:
|
||||||
for year in mjt.ranges(step="year"):
|
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_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_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_artists = {'gold':[],'silver':[],'bronze':[]}
|
||||||
entry_tracks = {'gold':[],'silver':[],'bronze':[]}
|
entry_tracks = {'gold':[],'silver':[],'bronze':[]}
|
||||||
|
entry_albums = {'gold':[],'silver':[],'bronze':[]}
|
||||||
medals_artists[year.desc()] = entry_artists
|
medals_artists[year.desc()] = entry_artists
|
||||||
medals_tracks[year.desc()] = entry_tracks
|
medals_tracks[year.desc()] = entry_tracks
|
||||||
|
medals_albums[year.desc()] = entry_albums
|
||||||
|
|
||||||
for entry in charts_artists:
|
for entry in charts_artists:
|
||||||
if entry['rank'] == 1: entry_artists['gold'].append(entry['artist_id'])
|
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'] == 2: entry_tracks['silver'].append(entry['track_id'])
|
||||||
elif entry['rank'] == 3: entry_tracks['bronze'].append(entry['track_id'])
|
elif entry['rank'] == 3: entry_tracks['bronze'].append(entry['track_id'])
|
||||||
else: break
|
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
|
@rundaily
|
||||||
def update_weekly():
|
def update_weekly():
|
||||||
|
|
||||||
global weekly_topartists, weekly_toptracks
|
global weekly_topartists, weekly_toptracks, weekly_topalbums
|
||||||
weekly_topartists.clear()
|
weekly_topartists.clear()
|
||||||
weekly_toptracks.clear()
|
weekly_toptracks.clear()
|
||||||
|
weekly_topalbums.clear()
|
||||||
|
|
||||||
with sqldb.engine.begin() as conn:
|
with sqldb.engine.begin() as conn:
|
||||||
for week in mjt.ranges(step="week"):
|
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_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_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:
|
for entry in charts_artists:
|
||||||
if entry['rank'] == 1: weekly_topartists.append(entry['artist_id'])
|
if entry['rank'] == 1: weekly_topartists.append(entry['artist_id'])
|
||||||
@ -72,3 +87,6 @@ def update_weekly():
|
|||||||
for entry in charts_tracks:
|
for entry in charts_tracks:
|
||||||
if entry['rank'] == 1: weekly_toptracks.append(entry['track_id'])
|
if entry['rank'] == 1: weekly_toptracks.append(entry['track_id'])
|
||||||
else: break
|
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
|
# name - type - foreign key - kwargs
|
||||||
'scrobbles':{
|
'scrobbles':{
|
||||||
'columns':[
|
'columns':[
|
||||||
("timestamp", sql.Integer, {'primary_key':True}),
|
("timestamp", sql.Integer, {'primary_key':True}),
|
||||||
("rawscrobble", sql.String, {}),
|
("rawscrobble", sql.String, {}),
|
||||||
("origin", sql.String, {}),
|
("origin", sql.String, {}),
|
||||||
("duration", sql.Integer, {}),
|
("duration", sql.Integer, {}),
|
||||||
("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}),
|
("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {}),
|
||||||
("extra", sql.String, {})
|
("extra", sql.String, {})
|
||||||
],
|
],
|
||||||
'extraargs':(),'extrakwargs':{}
|
'extraargs':(),'extrakwargs':{}
|
||||||
},
|
},
|
||||||
'tracks':{
|
'tracks':{
|
||||||
'columns':[
|
'columns':[
|
||||||
("id", sql.Integer, {'primary_key':True}),
|
("id", sql.Integer, {'primary_key':True}),
|
||||||
("title", sql.String, {}),
|
("title", sql.String, {}),
|
||||||
("title_normalized",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}
|
'extraargs':(),'extrakwargs':{'sqlite_autoincrement':True}
|
||||||
},
|
},
|
||||||
'artists':{
|
'artists':{
|
||||||
'columns':[
|
'columns':[
|
||||||
("id", sql.Integer, {'primary_key':True}),
|
("id", sql.Integer, {'primary_key':True}),
|
||||||
("name", sql.String, {}),
|
("name", sql.String, {}),
|
||||||
("name_normalized", 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}
|
'extraargs':(),'extrakwargs':{'sqlite_autoincrement':True}
|
||||||
},
|
},
|
||||||
'trackartists':{
|
'trackartists':{
|
||||||
'columns':[
|
'columns':[
|
||||||
("id", sql.Integer, {'primary_key':True}),
|
("id", sql.Integer, {'primary_key':True}),
|
||||||
("artist_id", sql.Integer, sql.ForeignKey('artists.id'), {}),
|
("artist_id", sql.Integer, sql.ForeignKey('artists.id'), {}),
|
||||||
("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {})
|
("track_id", sql.Integer, sql.ForeignKey('tracks.id'), {})
|
||||||
],
|
],
|
||||||
'extraargs':(sql.UniqueConstraint('artist_id', 'track_id'),),'extrakwargs':{}
|
'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':{
|
'associated_artists':{
|
||||||
'columns':[
|
'columns':[
|
||||||
("source_artist", sql.Integer, sql.ForeignKey('artists.id'), {}),
|
("source_artist", sql.Integer, sql.ForeignKey('artists.id'), {}),
|
||||||
("target_artist", sql.Integer, sql.ForeignKey('artists.id'), {})
|
("target_artist", sql.Integer, sql.ForeignKey('artists.id'), {})
|
||||||
],
|
],
|
||||||
'extraargs':(sql.UniqueConstraint('source_artist', 'target_artist'),),'extrakwargs':{}
|
'extraargs':(sql.UniqueConstraint('source_artist', 'target_artist'),),'extrakwargs':{}
|
||||||
}
|
}
|
||||||
@ -138,7 +166,7 @@ def connection_provider(func):
|
|||||||
# "artists":list,
|
# "artists":list,
|
||||||
# "title":string,
|
# "title":string,
|
||||||
# "album":{
|
# "album":{
|
||||||
# "name":string,
|
# "albumtitle":string,
|
||||||
# "artists":list
|
# "artists":list
|
||||||
# },
|
# },
|
||||||
# "length":None
|
# "length":None
|
||||||
@ -185,11 +213,12 @@ def scrobble_db_to_dict(row,dbconn=None):
|
|||||||
|
|
||||||
def tracks_db_to_dict(rows,dbconn=None):
|
def tracks_db_to_dict(rows,dbconn=None):
|
||||||
artists = get_artists_of_tracks(set(row.id for row in rows),dbconn=dbconn)
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
"artists":artists[row.id],
|
"artists":artists[row.id],
|
||||||
"title":row.title,
|
"title":row.title,
|
||||||
#"album":
|
"album":albums.get(row.album_id),
|
||||||
"length":row.length
|
"length":row.length
|
||||||
}
|
}
|
||||||
for row in rows
|
for row in rows
|
||||||
@ -207,18 +236,31 @@ def artists_db_to_dict(rows,dbconn=None):
|
|||||||
def artist_db_to_dict(row,dbconn=None):
|
def artist_db_to_dict(row,dbconn=None):
|
||||||
return artists_db_to_dict([row],dbconn=dbconn)[0]
|
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
|
### DICT -> DB
|
||||||
# These should return None when no data is in the dict so they can be used for update statements
|
# 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 {
|
return {
|
||||||
"timestamp":info.get('time'),
|
"timestamp":info.get('time'),
|
||||||
"origin":info.get('origin'),
|
"origin":info.get('origin'),
|
||||||
"duration":info.get('duration'),
|
"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,
|
"extra":json.dumps(info.get('extra')) if info.get('extra') else None,
|
||||||
"rawscrobble":json.dumps(info.get('rawscrobble')) if info.get('rawscrobble') 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)
|
"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
|
@connection_provider
|
||||||
def add_scrobble(scrobbledict,dbconn=None):
|
def add_scrobble(scrobbledict,update_album=False,dbconn=None):
|
||||||
add_scrobbles([scrobbledict],dbconn=dbconn)
|
add_scrobbles([scrobbledict],update_album=update_album,dbconn=dbconn)
|
||||||
|
|
||||||
@connection_provider
|
@connection_provider
|
||||||
def add_scrobbles(scrobbleslist,dbconn=None):
|
def add_scrobbles(scrobbleslist,update_album=False,dbconn=None):
|
||||||
|
|
||||||
with SCROBBLE_LOCK:
|
with SCROBBLE_LOCK:
|
||||||
|
|
||||||
ops = [
|
ops = [
|
||||||
DB['scrobbles'].insert().values(
|
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
|
) for s in scrobbleslist
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -285,11 +333,34 @@ def delete_scrobble(scrobble_id,dbconn=None):
|
|||||||
return True
|
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
|
### these will 'get' the ID of an entity, creating it if necessary
|
||||||
|
|
||||||
@cached_wrapper
|
@cached_wrapper
|
||||||
@connection_provider
|
@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'])
|
ntitle = normalize_name(trackdict['title'])
|
||||||
artist_ids = [get_artist_id(a,dbconn=dbconn) for a in trackdict['artists']]
|
artist_ids = [get_artist_id(a,dbconn=dbconn) for a in trackdict['artists']]
|
||||||
artist_ids = list(set(artist_ids))
|
artist_ids = list(set(artist_ids))
|
||||||
@ -310,18 +381,19 @@ def get_track_id(trackdict,create_new=True,dbconn=None):
|
|||||||
op = DB['trackartists'].select(
|
op = DB['trackartists'].select(
|
||||||
# DB['trackartists'].c.artist_id
|
# DB['trackartists'].c.artist_id
|
||||||
).where(
|
).where(
|
||||||
DB['trackartists'].c.track_id==row[0]
|
DB['trackartists'].c.track_id==row.id
|
||||||
)
|
)
|
||||||
result = dbconn.execute(op).all()
|
result = dbconn.execute(op).all()
|
||||||
match_artist_ids = [r.artist_id for r in result]
|
match_artist_ids = [r.artist_id for r in result]
|
||||||
#print("required artists",artist_ids,"this match",match_artist_ids)
|
#print("required artists",artist_ids,"this match",match_artist_ids)
|
||||||
if set(artist_ids) == set(match_artist_ids):
|
if set(artist_ids) == set(match_artist_ids):
|
||||||
#print("ID for",trackdict['title'],"was",row[0])
|
#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
|
return row.id
|
||||||
|
|
||||||
if not create_new: return None
|
if not create_new: return None
|
||||||
|
|
||||||
|
|
||||||
op = DB['tracks'].insert().values(
|
op = DB['tracks'].insert().values(
|
||||||
**track_dict_to_db(trackdict,dbconn=dbconn)
|
**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)
|
result = dbconn.execute(op)
|
||||||
#print("Created",trackdict['title'],track_id)
|
#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
|
return track_id
|
||||||
|
|
||||||
@cached_wrapper
|
@cached_wrapper
|
||||||
@ -364,6 +439,59 @@ def get_artist_id(artistname,create_new=True,dbconn=None):
|
|||||||
return result.inserted_primary_key[0]
|
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
|
### 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]
|
#result = [scrobble_db_to_dict(row) for row in result]
|
||||||
return 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
|
@cached_wrapper
|
||||||
@connection_provider
|
@connection_provider
|
||||||
def get_scrobbles(since=None,to=None,resolve_references=True,dbconn=None):
|
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')
|
result = rank(result,key='scrobbles')
|
||||||
return result
|
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
|
@cached_wrapper
|
||||||
@connection_provider
|
@connection_provider
|
||||||
def count_scrobbles_by_track_of_artist(since,to,artist,dbconn=None):
|
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
|
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
|
### 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))
|
artists.setdefault(row.track_id,[]).append(artist_db_to_dict(row,dbconn=dbconn))
|
||||||
return artists
|
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
|
@cached_wrapper_individual
|
||||||
@connection_provider
|
@connection_provider
|
||||||
@ -769,6 +1068,22 @@ def get_artists_map(artist_ids,dbconn=None):
|
|||||||
return artists
|
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
|
### associations
|
||||||
|
|
||||||
@cached_wrapper
|
@cached_wrapper
|
||||||
@ -835,6 +1150,16 @@ def get_artist(id,dbconn=None):
|
|||||||
artistinfo = result[0]
|
artistinfo = result[0]
|
||||||
return artist_db_to_dict(artistinfo,dbconn=dbconn)
|
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
|
@cached_wrapper
|
||||||
@connection_provider
|
@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('expire',sql.Integer),
|
||||||
sql.Column('raw',sql.String)
|
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)
|
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}"
|
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)
|
resolve_semaphore = BoundedSemaphore(8)
|
||||||
@ -132,7 +144,7 @@ def resolve_track_image(track_id):
|
|||||||
|
|
||||||
# local image
|
# local image
|
||||||
if malojaconfig["USE_LOCAL_IMAGES"]:
|
if malojaconfig["USE_LOCAL_IMAGES"]:
|
||||||
images = local_files(artists=track['artists'],title=track['title'])
|
images = local_files(track=track)
|
||||||
if len(images) != 0:
|
if len(images) != 0:
|
||||||
result = random.choice(images)
|
result = random.choice(images)
|
||||||
result = urllib.parse.quote(result)
|
result = urllib.parse.quote(result)
|
||||||
@ -176,31 +188,56 @@ def resolve_artist_image(artist_id):
|
|||||||
return result
|
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
|
# removes emojis and weird shit from names
|
||||||
def clean(name):
|
def clean(name):
|
||||||
return "".join(c for c in name if c.isalnum() or c in []).strip()
|
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):
|
# new and improved
|
||||||
# check if we're dealing with a track or artist, then clean up names
|
def get_all_possible_filenames(artist=None,track=None,album=None):
|
||||||
# (only remove non-alphanumeric, allow korean and stuff)
|
if track:
|
||||||
|
title, artists = clean(track['title']), [clean(a) for a in track['artists']]
|
||||||
if title is not None and artists is not None:
|
superfolder = "tracks/"
|
||||||
track = True
|
elif album:
|
||||||
title, artists = clean(title), [clean(a) for a in artists]
|
title, artists = clean(album['albumtitle']), [clean(a) for a in album.get('artists') or []]
|
||||||
elif artist is not None:
|
superfolder = "albums/"
|
||||||
track = False
|
elif artist:
|
||||||
artist = clean(artist)
|
artist = clean(artist)
|
||||||
else: return []
|
superfolder = "artists/"
|
||||||
|
else:
|
||||||
|
return []
|
||||||
superfolder = "tracks/" if track else "artists/"
|
|
||||||
|
|
||||||
filenames = []
|
filenames = []
|
||||||
|
|
||||||
if track:
|
if track or album:
|
||||||
#unsafeartists = [artist.translate(None,"-_./\\") for artist in artists]
|
|
||||||
safeartists = [re.sub("[^a-zA-Z0-9]","",artist) for artist in artists]
|
safeartists = [re.sub("[^a-zA-Z0-9]","",artist) for artist in artists]
|
||||||
#unsafetitle = title.translate(None,"-_./\\")
|
|
||||||
safetitle = re.sub("[^a-zA-Z0-9]","",title)
|
safetitle = re.sub("[^a-zA-Z0-9]","",title)
|
||||||
|
|
||||||
if len(artists) < 4:
|
if len(artists) < 4:
|
||||||
@ -210,7 +247,6 @@ def get_all_possible_filenames(artist=None,artists=None,title=None):
|
|||||||
unsafeperms = [sorted(artists)]
|
unsafeperms = [sorted(artists)]
|
||||||
safeperms = [sorted(safeartists)]
|
safeperms = [sorted(safeartists)]
|
||||||
|
|
||||||
|
|
||||||
for unsafeartistlist in unsafeperms:
|
for unsafeartistlist in unsafeperms:
|
||||||
filename = "-".join(unsafeartistlist) + "_" + title
|
filename = "-".join(unsafeartistlist) + "_" + title
|
||||||
if filename != "":
|
if filename != "":
|
||||||
@ -241,10 +277,11 @@ def get_all_possible_filenames(artist=None,artists=None,title=None):
|
|||||||
|
|
||||||
return [superfolder + name for name in filenames]
|
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 = []
|
images = []
|
||||||
|
|
||||||
@ -271,13 +308,18 @@ class MalformedB64(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def set_image(b64,**keys):
|
def set_image(b64,**keys):
|
||||||
track = "title" in keys
|
if "title" in keys:
|
||||||
if track:
|
entity = {"track":keys}
|
||||||
entity = {'artists':keys['artists'],'title':keys['title']}
|
id = database.sqldb.get_track_id(entity['track'])
|
||||||
id = database.sqldb.get_track_id(entity)
|
dbtable = "tracks"
|
||||||
else:
|
elif "albumtitle" in keys:
|
||||||
entity = keys['artist']
|
entity = {"album":keys}
|
||||||
id = database.sqldb.get_artist_id(entity)
|
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")
|
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()
|
type,b64 = match.groups()
|
||||||
b64 = base64.b64decode(b64)
|
b64 = base64.b64decode(b64)
|
||||||
filename = "webupload" + str(int(datetime.datetime.now().timestamp())) + "." + type
|
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)):
|
if os.path.exists(data_dir['images'](folder)):
|
||||||
with open(data_dir['images'](folder,filename),"wb") as f:
|
with open(data_dir['images'](folder,filename),"wb") as f:
|
||||||
f.write(b64)
|
f.write(b64)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
folder = get_all_possible_filenames(**keys)[0]
|
folder = get_all_possible_filenames(**entity)[0]
|
||||||
os.makedirs(data_dir['images'](folder))
|
os.makedirs(data_dir['images'](folder))
|
||||||
with open(data_dir['images'](folder,filename),"wb") as f:
|
with open(data_dir['images'](folder,filename),"wb") as f:
|
||||||
f.write(b64)
|
f.write(b64)
|
||||||
@ -303,7 +345,6 @@ def set_image(b64,**keys):
|
|||||||
log("Saved image as " + data_dir['images'](folder,filename),module="debug")
|
log("Saved image as " + data_dir['images'](folder,filename),module="debug")
|
||||||
|
|
||||||
# set as current picture in rotation
|
# set as current picture in rotation
|
||||||
if track: set_image_in_cache(id,'tracks',os.path.join("/images",folder,filename))
|
set_image_in_cache(id,dbtable,os.path.join("/images",folder,filename))
|
||||||
else: set_image_in_cache(id,'artists',os.path.join("/images",folder,filename))
|
|
||||||
|
|
||||||
return os.path.join("/images",folder,filename)
|
return os.path.join("/images",folder,filename)
|
||||||
|
@ -4,7 +4,7 @@ import urllib
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
# this also sets defaults!
|
# 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:
|
# output:
|
||||||
# 1 keys that define the filtered object like artist or track
|
# 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
|
# 3 keys that define interal time ranges
|
||||||
# 4 keys that define amount limits
|
# 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
|
# 1
|
||||||
if "title" in keys and not forceArtist:
|
if type == "track":
|
||||||
filterkeys = {"track":{"artists":keys.getall("artist"),"title":keys.get("title")}}
|
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")}
|
filterkeys = {"artist":keys.get("artist")}
|
||||||
if "associated" in keys: filterkeys["associated"] = True
|
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:
|
else:
|
||||||
filterkeys = {}
|
filterkeys = {}
|
||||||
|
|
||||||
@ -84,6 +95,10 @@ def internal_to_uri(keys):
|
|||||||
for a in keys["track"]["artists"]:
|
for a in keys["track"]["artists"]:
|
||||||
urikeys.append("artist",a)
|
urikeys.append("artist",a)
|
||||||
urikeys.append("title",keys["track"]["title"])
|
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
|
#time
|
||||||
if "timerange" in keys:
|
if "timerange" in keys:
|
||||||
|
@ -177,6 +177,7 @@ malojaconfig = Configuration(
|
|||||||
|
|
||||||
},
|
},
|
||||||
"Database":{
|
"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"),
|
"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"),
|
"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"),
|
"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"),
|
"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),
|
"charts_display_tiles":(tp.Boolean(), "Display Chart Tiles", False),
|
||||||
"display_art_icons":(tp.Boolean(), "Display Album/Artist Icons", True),
|
"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!"),
|
"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),
|
"use_local_images":(tp.Boolean(), "Use Local Images", True),
|
||||||
#"local_image_rotate":(tp.Integer(), "Local Image Rotate", 3600),
|
#"local_image_rotate":(tp.Integer(), "Local Image Rotate", 3600),
|
||||||
|
@ -19,7 +19,7 @@ from doreah import auth
|
|||||||
# rest of the project
|
# rest of the project
|
||||||
from . import database
|
from . import database
|
||||||
from .database.jinjaview import JinjaDBConnection
|
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 .malojauri import uri_to_internal, remove_identical
|
||||||
from .pkg_global.conf import malojaconfig, data_dir
|
from .pkg_global.conf import malojaconfig, data_dir
|
||||||
from .jinjaenv.context import jinja_environment
|
from .jinjaenv.context import jinja_environment
|
||||||
@ -124,6 +124,8 @@ def dynamic_image():
|
|||||||
result = resolve_track_image(keys['id'])
|
result = resolve_track_image(keys['id'])
|
||||||
elif keys['type'] == 'artist':
|
elif keys['type'] == 'artist':
|
||||||
result = resolve_artist_image(keys['id'])
|
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,'']:
|
if result is None or result['value'] in [None,'']:
|
||||||
return ""
|
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)
|
log("Could not get artist image for " + str(artist) + " from " + service.name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log("Error getting artist image from " + service.name + ": " + repr(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:
|
class GenericInterface:
|
||||||
@ -217,6 +228,23 @@ class MetadataInterface(GenericInterface,abstract=True):
|
|||||||
if imgurl is not None: imgurl = self.postprocess_url(imgurl)
|
if imgurl is not None: imgurl = self.postprocess_url(imgurl)
|
||||||
return 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
|
# default function to parse response by descending down nodes
|
||||||
# override if more complicated
|
# override if more complicated
|
||||||
def metadata_parse_response_artist(self,data):
|
def metadata_parse_response_artist(self,data):
|
||||||
@ -225,6 +253,9 @@ class MetadataInterface(GenericInterface,abstract=True):
|
|||||||
def metadata_parse_response_track(self,data):
|
def metadata_parse_response_track(self,data):
|
||||||
return self._parse_response("response_parse_tree_track", 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):
|
def _parse_response(self, resp, data):
|
||||||
res = data
|
res = data
|
||||||
for node in self.metadata[resp]:
|
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 = {
|
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}",
|
"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_type":"json",
|
||||||
#"response_parse_tree_track": ["tracks",0,"astrArtistThumb"],
|
#"response_parse_tree_track": ["tracks",0,"astrArtistThumb"],
|
||||||
"response_parse_tree_artist": ["artists",0,"strArtistThumb"],
|
"response_parse_tree_artist": ["artists",0,"strArtistThumb"],
|
||||||
"required_settings": ["api_key"],
|
"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
|
return None
|
||||||
|
11
maloja/thirdparty/deezer.py
vendored
11
maloja/thirdparty/deezer.py
vendored
@ -8,10 +8,17 @@ class Deezer(MetadataInterface):
|
|||||||
}
|
}
|
||||||
|
|
||||||
metadata = {
|
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}",
|
"artisturl": "https://api.deezer.com/search?q={artist}",
|
||||||
|
"albumurl": "https://api.deezer.com/search?q={artist}%20{title}",
|
||||||
"response_type":"json",
|
"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_artist": ["data",0,"artist","picture_medium"],
|
||||||
|
"response_parse_tree_album": ["data",0,"album","cover_medium"],
|
||||||
"required_settings": [],
|
"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"
|
"activated_setting": "SCROBBLE_LASTFM"
|
||||||
}
|
}
|
||||||
metadata = {
|
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",
|
"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_type":"json",
|
||||||
"response_parse_tree_track": ["track","album","image",-1,"#text"],
|
"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"],
|
"required_settings": ["apikey"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_image_artist(self,artist):
|
def get_image_artist(self,artist):
|
||||||
return None
|
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):
|
def proxyscrobble_parse_response(self,data):
|
||||||
return data.attrib.get("status") == "ok" and data.find("scrobbles").attrib.get("ignored") == "0"
|
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
|
return None
|
||||||
# not supported
|
# not supported
|
||||||
|
|
||||||
|
def get_image_album(self,album):
|
||||||
|
return None
|
||||||
|
|
||||||
def get_image_track(self,track):
|
def get_image_track(self,track):
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
|
4
maloja/thirdparty/spotify.py
vendored
4
maloja/thirdparty/spotify.py
vendored
@ -15,9 +15,11 @@ class Spotify(MetadataInterface):
|
|||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
"trackurl": "https://api.spotify.com/v1/search?q=artist:{artist}%20track:{title}&type=track&access_token={token}",
|
"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}",
|
"artisturl": "https://api.spotify.com/v1/search?q=artist:{artist}&type=artist&access_token={token}",
|
||||||
"response_type":"json",
|
"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"],
|
"response_parse_tree_artist": ["artists","items",0,"images",0,"url"],
|
||||||
"required_settings": ["apiid","secret"],
|
"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>
|
</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} %}
|
{% with amountkeys={"perpage":15,"page":0} %}
|
||||||
{% include 'partials/charts_tracks.jinja' %}
|
{% 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" %}
|
{% extends "abstracts/base.jinja" %}
|
||||||
{% block title %}Maloja - Artist Charts{% endblock %}
|
{% block title %}Maloja - Artist Charts{% endblock %}
|
||||||
|
|
||||||
|
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/datechange.js" async></script>
|
<script src="/datechange.js" async></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -25,7 +27,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text">
|
<td class="text">
|
||||||
<h1>Artist Charts</h1><a href="/top_artists"><span>View #1 Artists</span></a><br/>
|
<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/>
|
<br/><br/>
|
||||||
{% with delimitkeys = {} %}
|
{% with delimitkeys = {} %}
|
||||||
{% include 'snippets/timeselection.jinja' %}
|
{% include 'snippets/timeselection.jinja' %}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
{% block title %}Maloja - Track Charts{% endblock %}
|
{% block title %}Maloja - Track Charts{% endblock %}
|
||||||
|
|
||||||
{% import 'snippets/links.jinja' as links %}
|
{% import 'snippets/links.jinja' as links %}
|
||||||
|
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/datechange.js" async></script>
|
<script src="/datechange.js" async></script>
|
||||||
@ -26,8 +27,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text">
|
<td class="text">
|
||||||
<h1>Track Charts</h1><a href="/top_tracks"><span>View #1 Tracks</span></a><br/>
|
<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 %}
|
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
{% with delimitkeys = {} %}
|
{% with delimitkeys = {} %}
|
||||||
{% include 'snippets/timeselection.jinja' %}
|
{% 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 %}
|
{% 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) %}
|
{% set img = images.get_track_image(entity) %}
|
||||||
|
{% elif entity is mapping and 'albumtitle' in entity %}
|
||||||
|
{% set img = images.get_album_image(entity) %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set img = images.get_artist_image(entity) %}
|
{% set img = images.get_artist_image(entity) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -20,6 +22,10 @@
|
|||||||
<td class='track'>
|
<td class='track'>
|
||||||
<span class='artist_in_trackcolumn'>{{ links.links(entity.artists) }}</span> – {{ links.link(entity) }}
|
<span class='artist_in_trackcolumn'>{{ links.links(entity.artists) }}</span> – {{ links.link(entity) }}
|
||||||
</td>
|
</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 %}
|
{% else %}
|
||||||
<td class='artist'>{{ links.link(entity) }}
|
<td class='artist'>{{ links.link(entity) }}
|
||||||
{% if counting != [] %}
|
{% if counting != [] %}
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
{% elif filterkeys.get('track') is not none %}
|
{% elif filterkeys.get('track') is not none %}
|
||||||
of {{ links.link(filterkeys.get('track')) }}
|
of {{ links.link(filterkeys.get('track')) }}
|
||||||
by {{ links.links(filterkeys["track"]["artists"]) }}
|
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 %}
|
{% endif %}
|
||||||
{{ limitkeys.timerange.desc(prefix=True) }}
|
{{ limitkeys.timerange.desc(prefix=True) }}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% macro link(entity) -%}
|
{% 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 %}
|
{% set name = entity.title or entity.albumtitle %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set name = entity %}
|
{% set name = entity %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -9,15 +9,21 @@
|
|||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro links(entities) -%}
|
{% macro links(entities) -%}
|
||||||
{% for entity in entities -%}
|
{% if entities is none or entities == [] %}
|
||||||
{{ link(entity) }}{{ ", " if not loop.last }}
|
{{ settings["DEFAULT_ALBUM_ARTIST"] }}
|
||||||
{%- endfor %}
|
{% else %}
|
||||||
|
{% for entity in entities -%}
|
||||||
|
{{ link(entity) }}{{ ", " if not loop.last }}
|
||||||
|
{%- endfor %}
|
||||||
|
{% endif %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% macro url(entity) %}
|
{% 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}) }}
|
{{ mlj_uri.create_uri("/track",{'track':entity}) }}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{ mlj_uri.create_uri("/artist",{'artist':entity}) }}
|
{{ mlj_uri.create_uri("/artist",{'artist':entity}) }}
|
||||||
@ -43,6 +49,8 @@
|
|||||||
|
|
||||||
{% if 'track' in filterkeys %}
|
{% if 'track' in filterkeys %}
|
||||||
{% set url = mlj_uri.create_uri("/charts_tracks",{'timerange':timerange}) %}
|
{% 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 %}
|
{% elif 'artist' in filterkeys %}
|
||||||
{% set url = mlj_uri.create_uri("/charts_artists",{'timerange':timerange}) %}
|
{% set url = mlj_uri.create_uri("/charts_artists",{'timerange':timerange}) %}
|
||||||
{% endif %}
|
{% 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" %}
|
{% extends "abstracts/base.jinja" %}
|
||||||
{% block title %}Maloja - #1 Artists{% endblock %}
|
{% block title %}Maloja - #1 Artists{% endblock %}
|
||||||
|
|
||||||
|
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||||
|
|
||||||
<!-- find representative -->
|
<!-- find representative -->
|
||||||
|
|
||||||
@ -17,7 +18,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text">
|
<td class="text">
|
||||||
<h1>#1 Artists</h1><br/>
|
<h1>#1 Artists</h1><br/>
|
||||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||||
|
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
{% include 'snippets/timeselection.jinja' %}
|
{% include 'snippets/timeselection.jinja' %}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends "abstracts/base.jinja" %}
|
{% extends "abstracts/base.jinja" %}
|
||||||
{% block title %}Maloja - #1 Tracks{% endblock %}
|
{% block title %}Maloja - #1 Tracks{% endblock %}
|
||||||
|
|
||||||
|
{% import 'snippets/filterdescription.jinja' as filterdesc %}
|
||||||
|
|
||||||
<!-- find representative -->
|
<!-- find representative -->
|
||||||
|
|
||||||
@ -17,7 +18,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text">
|
<td class="text">
|
||||||
<h1>#1 Tracks</h1><br/>
|
<h1>#1 Tracks</h1><br/>
|
||||||
<span>{{ limitkeys.timerange.desc(prefix=True) }}</span>
|
{{ filterdesc.desc(filterkeys,limitkeys) }}
|
||||||
|
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
{% include 'snippets/timeselection.jinja' %}
|
{% include 'snippets/timeselection.jinja' %}
|
||||||
|
@ -64,6 +64,9 @@
|
|||||||
{{ awards.certs(track) }}
|
{{ awards.certs(track) }}
|
||||||
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
|
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
|
||||||
<br/>
|
<br/>
|
||||||
|
{% if info.track.album %}
|
||||||
|
from {{ links.link(info.track.album) }}<br/>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<p class="stats">
|
<p class="stats">
|
||||||
{% if adminmode %}<button type="button" onclick="scrobble('{{ encodedtrack }}')">Scrobble now</button>{% endif %}
|
{% if adminmode %}<button type="button" onclick="scrobble('{{ encodedtrack }}')">Scrobble now</button>{% endif %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user