Merge branch 'master' into pyhp

This commit is contained in:
Krateng 2019-10-24 15:52:06 +02:00
commit b955777637
57 changed files with 1339 additions and 399 deletions

14
.doreah
View File

@ -1,4 +1,10 @@
logging.logfolder = logs
settings.files = [ "settings/default.ini" , "settings/settings.ini" ]
caching.folder = "cache/"
regular.autostart = false
logging:
logfolder: "logs"
settings:
files:
- "settings/default.ini"
- "settings/settings.ini"
caching:
folder: "cache/"
regular:
autostart: false

3
.gitignore vendored
View File

@ -1,6 +1,7 @@
# generic temporary / dev files
*.pyc
*.sh
!/update_requirements.sh
*.note
*.xcf
nohup.out
@ -10,10 +11,10 @@ nohup.out
*.tsv
*.rulestate
*.log
*.css
# currently not using
/screenshot*.png
/proxyscrobble.py
# only for development, normally external
/doreah

View File

@ -16,12 +16,8 @@ Also neat: You can use your **custom artist or track images**.
## Requirements
* [python3](https://www.python.org/) - [GitHub](https://github.com/python/cpython)
* [bottle.py](https://bottlepy.org/) - [GitHub](https://github.com/bottlepy/bottle)
* [waitress](https://docs.pylonsproject.org/projects/waitress/) - [GitHub](https://github.com/Pylons/waitress)
* [doreah](https://pypi.org/project/doreah/) - [GitHub](https://github.com/krateng/doreah) (at least Version 0.9.1)
* [nimrodel](https://pypi.org/project/nimrodel/) - [GitHub](https://github.com/krateng/nimrodel) (at least Version 0.4.9)
* [setproctitle](https://pypi.org/project/setproctitle/) - [GitHub](https://github.com/dvarrazzo/py-setproctitle)
* Python 3
* Pip packages specified in `requirements.txt`
* If you'd like to display images, you will need API keys for [Last.fm](https://www.last.fm/api/account/create) and [Fanart.tv](https://fanart.tv/get-an-api-key/). These are free of charge!
## How to install
@ -68,9 +64,9 @@ If you didn't install Maloja from the package (and therefore don't have it in `/
### Native API
If you use Plex Web or Youtube Music on Chromium, you can use the included extension (also available on the [Chrome Web Store](https://chrome.google.com/webstore/detail/maloja-scrobbler/cfnbifdmgbnaalphodcbandoopgbfeeh)). Make sure to enter the random key Maloja generates on first startup in the extension settings.
If you use Plex Web, Spotify, Bandcamp, Soundcloud or Youtube Music on Chromium, you can use the included extension (also available on the [Chrome Web Store](https://chrome.google.com/webstore/detail/maloja-scrobbler/cfnbifdmgbnaalphodcbandoopgbfeeh)). Make sure to enter the random key Maloja generates on first startup in the extension settings.
If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key` - either as from-data or json.
If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key` - either as form-data or json.
### Standard-compliant API

View File

@ -1,6 +1,6 @@
import re
import utilities
from doreah import tsv
from doreah import tsv, settings
# need to do this as a class so it can retain loaded settings from file
# apparently this is not true
@ -11,11 +11,16 @@ class CleanerAgent:
self.updateRules()
def updateRules(self):
raw = tsv.parse_all("rules","string","string","string")
self.rules_belongtogether = [b for [a,b,c] in raw if a=="belongtogether"]
self.rules_notanartist = [b for [a,b,c] in raw if a=="notanartist"]
self.rules_replacetitle = {b.lower():c for [a,b,c] in raw if a=="replacetitle"}
self.rules_replaceartist = {b.lower():c for [a,b,c] in raw if a=="replaceartist"}
raw = tsv.parse_all("rules","string","string","string","string")
self.rules_belongtogether = [b for [a,b,c,d] in raw if a=="belongtogether"]
self.rules_notanartist = [b for [a,b,c,d] in raw if a=="notanartist"]
self.rules_replacetitle = {b.lower():c for [a,b,c,d] in raw if a=="replacetitle"}
self.rules_replaceartist = {b.lower():c for [a,b,c,d] in raw if a=="replaceartist"}
self.rules_ignoreartist = [b.lower() for [a,b,c,d] in raw if a=="ignoreartist"]
self.rules_addartists = {c.lower():(b.lower(),d) for [a,b,c,d] in raw if a=="addartists"}
#self.rules_regexartist = [[b,c] for [a,b,c,d] in raw if a=="regexartist"]
#self.rules_regextitle = [[b,c] for [a,b,c,d] in raw if a=="regextitle"]
# TODO
# we always need to be able to tell if our current database is made with the current rules
self.checksums = utilities.checksumTSV("rules")
@ -27,6 +32,12 @@ class CleanerAgent:
title = self.parseTitle(self.removespecial(title))
(title,moreartists) = self.parseTitleForArtists(title)
artists += moreartists
if title.lower() in self.rules_addartists:
reqartists, allartists = self.rules_addartists[title.lower()]
reqartists = reqartists.split("")
allartists = allartists.split("")
if set(reqartists).issubset(set(a.lower() for a in artists)):
artists += allartists
artists = list(set(artists))
artists.sort()
@ -52,6 +63,12 @@ class CleanerAgent:
def parseArtists(self,a):
if a.strip() in settings.get_settings("INVALID_ARTISTS"):
return []
if a.strip().lower() in self.rules_ignoreartist:
return []
if a.strip() == "":
return []

View File

@ -68,6 +68,7 @@ def handle(path,keys):
def scrobbletrack(artiststr,titlestr,timestamp):
try:
log("Incoming scrobble (compliant API): ARTISTS: " + artiststr + ", TRACK: " + titlestr,module="debug")
(artists,title) = cla.fullclean(artiststr,titlestr)
database.createScrobble(artists,title,timestamp)
database.sync()

View File

@ -6,6 +6,7 @@ import utilities
from malojatime import register_scrobbletime, time_stamps, ranges
from urihandler import uri_to_internal, internal_to_uri, compose_querystring
import compliant_api
from external import proxy_scrobble
# doreah toolkit
from doreah.logging import log
from doreah import tsv
@ -49,8 +50,11 @@ TRACKS_LOWER = []
ARTISTS_LOWER = []
ARTIST_SET = set()
TRACK_SET = set()
MEDALS = {} #literally only changes once per year, no need to calculate that on the fly
MEDALS_TRACKS = {}
WEEKLY_TOPTRACKS = {}
WEEKLY_TOPARTISTS = {}
cla = CleanerAgent()
coa = CollectorAgent()
@ -73,7 +77,12 @@ def loadAPIkeys():
log("Authenticated Machines: " + ", ".join([m[1] for m in clients]))
def checkAPIkey(k):
return (k in [k for [k,d] in clients])
#return (k in [k for [k,d] in clients])
for key, identifier in clients:
if key == k: return identifier
return False
def allAPIkeys():
return [k for [k,d] in clients]
@ -102,10 +111,23 @@ def get_track_dict(o):
def createScrobble(artists,title,time,volatile=False):
if len(artists) == 0 or title == "":
return {}
dblock.acquire()
i = getTrackID(artists,title)
# idempotence
if time in SCROBBLESDICT:
if i == SCROBBLESDICT[time].track:
dblock.release()
return get_track_dict(TRACKS[i])
# timestamp as unique identifier
while (time in SCROBBLESDICT):
time += 1
i = getTrackID(artists,title)
obj = Scrobble(i,time,volatile) # if volatile generated, we simply pretend we have already saved it to disk
#SCROBBLES.append(obj)
# immediately insert scrobble correctly so we can guarantee sorted list
@ -116,6 +138,8 @@ def createScrobble(artists,title,time,volatile=False):
invalidate_caches()
dblock.release()
proxy_scrobble(artists,title,time)
return get_track_dict(TRACKS[obj.track])
@ -225,7 +249,22 @@ def get_scrobbles(**keys):
# return r
return r
# info for comparison
@dbserver.get("info")
def info_external(**keys):
result = info()
return result
def info():
totalscrobbles = get_scrobbles_num()
artists = {}
return {
"name":settings.get_settings("NAME"),
"artists":{
chartentry["artist"]:round(chartentry["scrobbles"] * 100 / totalscrobbles,3)
for chartentry in get_charts_artists() if chartentry["scrobbles"]/totalscrobbles >= 0}
}
@ -517,7 +556,14 @@ def artistInfo(artist):
c = [e for e in charts if e["artist"] == artist][0]
others = [a for a in coa.getAllAssociated(artist) if a in ARTISTS]
position = c["rank"]
return {"scrobbles":scrobbles,"position":position,"associated":others,"medals":MEDALS.get(artist)}
performance = get_performance(artist=artist,step="week")
return {
"scrobbles":scrobbles,
"position":position,
"associated":others,
"medals":MEDALS.get(artist),
"topweeks":WEEKLY_TOPARTISTS.get(artist,0)
}
except:
# if the artist isnt in the charts, they are not being credited and we
# need to show information about the credited one
@ -555,11 +601,13 @@ def trackInfo(track):
elif scrobbles >= threshold_platinum: cert = "platinum"
elif scrobbles >= threshold_gold: cert = "gold"
return {
"scrobbles":scrobbles,
"position":position,
"medals":MEDALS_TRACKS.get((frozenset(track["artists"]),track["title"])),
"certification":cert
"certification":cert,
"topweeks":WEEKLY_TOPTRACKS.get(((frozenset(track["artists"]),track["title"])),0)
}
@ -573,13 +621,16 @@ def pseudo_post_scrobble(**keys):
artists = keys.get("artist")
title = keys.get("title")
apikey = keys.get("key")
if not (checkAPIkey(apikey)):
client = checkAPIkey(apikey)
if client == False: # empty string allowed!
response.status = 403
return ""
try:
time = int(keys.get("time"))
except:
time = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
log("Incoming scrobble (native API): Client " + client + ", ARTISTS: " + str(artists) + ", TRACK: " + title,module="debug")
(artists,title) = cla.fullclean(artists,title)
## this is necessary for localhost testing
@ -587,8 +638,9 @@ def pseudo_post_scrobble(**keys):
trackdict = createScrobble(artists,title,time)
if (time - lastsync) > 3600:
sync()
sync()
return {"status":"success","track":trackdict}
@ -597,7 +649,8 @@ def post_scrobble(**keys):
artists = keys.get("artist")
title = keys.get("title")
apikey = keys.get("key")
if not (checkAPIkey(apikey)):
client = checkAPIkey(apikey)
if client == False: # empty string allowed!
response.status = 403
return ""
@ -605,6 +658,8 @@ def post_scrobble(**keys):
time = int(keys.get("time"))
except:
time = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
log("Incoming scrobble (native API): Client " + client + ", ARTISTS: " + str(artists) + ", TRACK: " + title,module="debug")
(artists,title) = cla.fullclean(artists,title)
## this is necessary for localhost testing
@ -612,12 +667,11 @@ def post_scrobble(**keys):
trackdict = createScrobble(artists,title,time)
#if (time - lastsync) > 3600:
# sync()
sync()
#always sync, one filesystem access every three minutes shouldn't matter
return {"status":"success","track":trackdict}
@ -644,8 +698,7 @@ def abouttoshutdown():
#sys.exit()
@dbserver.post("newrule")
def newrule():
keys = FormsDict.decode(request.forms)
def newrule(**keys):
apikey = keys.pop("key",None)
if (checkAPIkey(apikey)):
tsv.add_entry("rules/webmade.tsv",[k for k in keys])
@ -751,8 +804,7 @@ def issues():
@dbserver.post("importrules")
def import_rulemodule():
keys = FormsDict.decode(request.forms)
def import_rulemodule(**keys):
apikey = keys.pop("key",None)
if (checkAPIkey(apikey)):
@ -771,9 +823,7 @@ def import_rulemodule():
@dbserver.post("rebuild")
def rebuild():
keys = FormsDict.decode(request.forms)
def rebuild(**keys):
apikey = keys.pop("key",None)
if (checkAPIkey(apikey)):
log("Database rebuild initiated!")
@ -886,6 +936,7 @@ def build_db():
#start regular tasks
utilities.update_medals()
utilities.update_weekly()
global db_rulestate
db_rulestate = utilities.consistentRulestate("scrobbles",cla.checksums)
@ -899,6 +950,7 @@ def sync():
# all entries by file collected
# so we don't open the same file for every entry
#log("Syncing",module="debug")
entries = {}
for idx in range(len(SCROBBLES)):
@ -918,15 +970,19 @@ def sync():
SCROBBLES[idx] = (SCROBBLES[idx][0],SCROBBLES[idx][1],True)
#log("Sorted into months",module="debug")
for e in entries:
tsv.add_entries("scrobbles/" + e + ".tsv",entries[e],comments=False)
#addEntries("scrobbles/" + e + ".tsv",entries[e],escape=False)
utilities.combineChecksums("scrobbles/" + e + ".tsv",cla.checksums)
#log("Written files",module="debug")
global lastsync
lastsync = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
log("Database saved to disk.")
#log("Database saved to disk.")
# save cached images
#saveCache()

View File

@ -3,6 +3,10 @@ import json
import base64
from doreah.settings import get_settings
from doreah.logging import log
import hashlib
import xml.etree.ElementTree as ET
### PICTURES
apis_artists = []
@ -130,3 +134,49 @@ def api_request_track(track):
pass
return None
### SCROBBLING
# creates signature and returns full query string
def lfmbuild(parameters):
m = hashlib.md5()
keys = sorted(str(k) for k in parameters)
m.update(utf("".join(str(k) + str(parameters[k]) for k in keys)))
m.update(utf(get_settings("LASTFM_API_SECRET")))
sig = m.hexdigest()
return urllib.parse.urlencode(parameters) + "&api_sig=" + sig
def utf(st):
return st.encode(encoding="UTF-8")
apis_scrobble = []
if get_settings("LASTFM_API_SK") not in [None,"ASK"] and get_settings("LASTFM_API_SECRET") not in [None,"ASK"] and get_settings("LASTFM_API_KEY") not in [None,"ASK"]:
apis_scrobble.append({
"name":"LastFM",
"scrobbleurl":"http://ws.audioscrobbler.com/2.0/",
"requestbody":lambda artists,title,timestamp: lfmbuild({"method":"track.scrobble","artist[0]":", ".join(artists),"track[0]":title,"timestamp":timestamp,"api_key":get_settings("LASTFM_API_KEY"),"sk":get_settings("LASTFM_API_SK")})
})
def proxy_scrobble(artists,title,timestamp):
for api in apis_scrobble:
response = urllib.request.urlopen(api["scrobbleurl"],data=utf(api["requestbody"](artists,title,timestamp)))
xml = response.read()
data = ET.fromstring(xml)
if data.attrib.get("status") == "ok":
if data.find("scrobbles").attrib.get("ignored") == "0":
log(api["name"] + ": Scrobble accepted: " + "/".join(artists) + " - " + title)
else:
log(api["name"] + ": Scrobble not accepted: " + "/".join(artists) + " - " + title)

View File

@ -2,6 +2,8 @@ import urllib
from bottle import FormsDict
import datetime
from urihandler import compose_querystring
import urllib.parse
from doreah.settings import get_settings
# returns the proper column(s) for an artist or track
@ -16,7 +18,9 @@ def entity_column(element,counting=[],image=None):
# track
# html += "<td class='artists'>" + html_links(element["artists"]) + "</td>"
# html += "<td class='title'>" + html_link(element) + "</td>"
html += "<td class='track'><span class='artist_in_trackcolumn'>" + html_links(element["artists"]) + "</span> " + html_link(element) + "</td>"
html += "<td class='track'><span class='artist_in_trackcolumn'>"
html += trackSearchLink(element)
html += html_links(element["artists"]) + "</span> " + html_link(element) + "</td>"
else:
# artist
html += "<td class='artist'>" + html_link(element)
@ -74,6 +78,33 @@ def trackLink(track):
#artists,title = track["artists"],track["title"]
#return "<a href='/track?title=" + urllib.parse.quote(title) + "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in artists]) + "'>" + title + "</a>"
def trackSearchLink(track):
searchProvider = get_settings("TRACK_SEARCH_PROVIDER")
if searchProvider is None: return ""
link = "<a class='trackProviderSearch' href='"
if searchProvider == "YouTube":
link += "https://www.youtube.com/results?search_query="
elif searchProvider == "YouTube Music":
link += "https://music.youtube.com/search?q="
elif searchProvider == "Google Play Music":
link += "https://play.google.com/music/listen#/srs/"
elif searchProvider == "Spotify":
link += "https://open.spotify.com/search/results/"
elif searchProvider == "Tidal":
link += "https://listen.tidal.com/search/tracks?q="
elif searchProvider == "SoundCloud":
link += "https://soundcloud.com/search?q="
elif searchProvider == "Amazon Music":
link += "https://music.amazon.com/search/"
elif searchProvider == "Deezer":
link += "https://www.deezer.com/search/"
else:
link += "https://www.google.com/search?q=" # ¯\_(ツ)_/¯
link += urllib.parse.quote(", ".join(track["artists"]) + " " + track["title"]) + "'>&#127925;</a>"
return link
#def scrobblesTrackLink(artists,title,timekeys,amount=None,pixels=None):
def scrobblesTrackLink(track,timekeys,amount=None,percent=None):
artists,title = track["artists"],track["title"]

View File

@ -18,29 +18,38 @@ import math
# result.append(element.get("image"))
# artist=None,track=None,since=None,to=None,within=None,associated=False,max_=None,pictures=False
def module_scrobblelist(max_=None,pictures=False,shortTimeDesc=False,earlystop=False,**kwargs):
#max_ indicates that no pagination should occur (because this is not the primary module)
def module_scrobblelist(page=0,perpage=100,max_=None,pictures=False,shortTimeDesc=False,earlystop=False,**kwargs):
kwargs_filter = pickKeys(kwargs,"artist","track","associated")
kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
# if earlystop, we don't care about the actual amount and only request as many from the db
# without, we request everything and filter on site
maxkey = {"max_":max_} if earlystop else {}
maxkey = {"max_":lastindex} if earlystop else {}
scrobbles = database.get_scrobbles(**kwargs_time,**kwargs_filter,**maxkey)
if pictures:
scrobbleswithpictures = scrobbles if max_ is None else scrobbles[:max_]
scrobbleswithpictures = [""] * firstindex + scrobbles[firstindex:lastindex]
#scrobbleimages = [e.get("image") for e in getTracksInfo(scrobbleswithpictures)] #will still work with scrobble objects as they are a technically a subset of track objects
#scrobbleimages = ["/image?title=" + urllib.parse.quote(t["title"]) + "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in t["artists"]]) for t in scrobbleswithpictures]
scrobbleimages = [getTrackImage(t["artists"],t["title"],fast=True) for t in scrobbleswithpictures]
pages = math.ceil(len(scrobbles) / perpage)
representative = scrobbles[0] if len(scrobbles) is not 0 else None
# build list
i = 0
html = "<table class='list'>"
for s in scrobbles:
if i<firstindex:
i += 1
continue
html += "<tr>"
html += "<td class='time'>" + timestamp_desc(s["time"],short=shortTimeDesc) + "</td>"
@ -48,32 +57,38 @@ def module_scrobblelist(max_=None,pictures=False,shortTimeDesc=False,earlystop=F
img = scrobbleimages[i]
else: img = None
html += entity_column(s,image=img)
# Alternative way: Do it in one cell
#html += "<td class='title'><span>" + artistLinks(s["artists"]) + "</span> — " + trackLink({"artists":s["artists"],"title":s["title"]}) + "</td>"
html += "</tr>"
i += 1
if max_ is not None and i>=max_:
if i>=lastindex:
break
html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return (html,len(scrobbles),representative)
def module_pulse(max_=None,**kwargs):
def module_pulse(page=0,perpage=100,max_=None,**kwargs):
from doreah.timing import clock, clockp
kwargs_filter = pickKeys(kwargs,"artist","track","associated")
kwargs_time = pickKeys(kwargs,"since","to","within","timerange","step","stepn","trail")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
ranges = database.get_pulse(**kwargs_time,**kwargs_filter)
pages = math.ceil(len(ranges) / perpage)
if max_ is not None: ranges = ranges[:max_]
ranges = ranges[firstindex:lastindex]
# if time range not explicitly specified, only show from first appearance
# if "since" not in kwargs:
@ -94,19 +109,27 @@ def module_pulse(max_=None,**kwargs):
html += "</tr>"
html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return html
def module_performance(max_=None,**kwargs):
def module_performance(page=0,perpage=100,max_=None,**kwargs):
kwargs_filter = pickKeys(kwargs,"artist","track")
kwargs_time = pickKeys(kwargs,"since","to","within","timerange","step","stepn","trail")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
ranges = database.get_performance(**kwargs_time,**kwargs_filter)
if max_ is not None: ranges = ranges[:max_]
pages = math.ceil(len(ranges) / perpage)
ranges = ranges[firstindex:lastindex]
# if time range not explicitly specified, only show from first appearance
# if "since" not in kwargs:
@ -130,18 +153,26 @@ def module_performance(max_=None,**kwargs):
html += "</tr>"
html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return html
def module_trackcharts(max_=None,**kwargs):
def module_trackcharts(page=0,perpage=100,max_=None,**kwargs):
kwargs_filter = pickKeys(kwargs,"artist","associated")
kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
tracks = database.get_charts_tracks(**kwargs_filter,**kwargs_time)
pages = math.ceil(len(tracks) / perpage)
# last time range (to compare)
try:
trackslast = database.get_charts_tracks(**kwargs_filter,timerange=kwargs_time["timerange"].next(step=-1))
@ -167,13 +198,16 @@ def module_trackcharts(max_=None,**kwargs):
i = 0
html = "<table class='list'>"
for e in tracks:
if i<firstindex:
i += 1
continue
i += 1
if max_ is not None and i>max_:
if i>lastindex:
break
html += "<tr>"
# rank
if i == 1 or e["scrobbles"] < prev["scrobbles"]:
html += "<td class='rank'>#" + str(i) + "</td>"
if i == firstindex+1 or e["scrobbles"] < prev["scrobbles"]:
html += "<td class='rank'>#" + str(e["rank"]) + "</td>"
else:
html += "<td class='rank'></td>"
# rank change
@ -196,16 +230,26 @@ def module_trackcharts(max_=None,**kwargs):
prev = e
html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return (html,representative)
def module_artistcharts(max_=None,**kwargs):
def module_artistcharts(page=0,perpage=100,max_=None,**kwargs):
kwargs_filter = pickKeys(kwargs,"associated") #not used right now
kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
if max_ is not None: perpage,page=max_,0
firstindex = page * perpage
lastindex = firstindex + perpage
artists = database.get_charts_artists(**kwargs_filter,**kwargs_time)
pages = math.ceil(len(artists) / perpage)
# last time range (to compare)
try:
#from malojatime import _get_next
@ -231,13 +275,16 @@ def module_artistcharts(max_=None,**kwargs):
i = 0
html = "<table class='list'>"
for e in artists:
if i<firstindex:
i += 1
continue
i += 1
if max_ is not None and i>max_:
if i>lastindex:
break
html += "<tr>"
# rank
if i == 1 or e["scrobbles"] < prev["scrobbles"]:
html += "<td class='rank'>#" + str(i) + "</td>"
if i == firstindex+1 or e["scrobbles"] < prev["scrobbles"]:
html += "<td class='rank'>#" + str(e["rank"]) + "</td>"
else:
html += "<td class='rank'></td>"
# rank change
@ -262,6 +309,8 @@ def module_artistcharts(max_=None,**kwargs):
html += "</table>"
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
return (html, representative)
@ -308,7 +357,7 @@ def module_toptracks(pictures=True,**kwargs):
if pictures:
html += "<td><div></div></td>"
html += "<td class='stats'>" + "No scrobbles" + "</td>"
html += "<td>" + "" + "</td>"
#html += "<td>" + "" + "</td>"
html += "<td class='amount'>" + "0" + "</td>"
html += "<td class='bar'>" + "" + "</td>"
else:
@ -478,22 +527,60 @@ def module_trackcharts_tiles(**kwargs):
return html
def module_paginate(page,pages,perpage,**keys):
unchangedkeys = internal_to_uri({**keys,"perpage":perpage})
html = "<div class='paginate'>"
if page > 1:
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":0})) + "'><span class='stat_selector'>" + "1" + "</span></a>"
html += " | "
if page > 2:
html += " ... | "
if page > 0:
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":page-1})) + "'><span class='stat_selector'>" + str(page) + "</span></a>"
html += " « "
html += "<span style='opacity:0.5;' class='stat_selector'>" + str(page+1) + "</span>"
if page < pages-1:
html += " » "
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":page+1})) + "'><span class='stat_selector'>" + str(page+2) + "</span></a>"
if page < pages-3:
html += " | ... "
if page < pages-2:
html += " | "
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":pages-1})) + "'><span class='stat_selector'>" + str(pages) + "</span></a>"
html += "</div>"
return html
# THIS FUNCTION USES THE ORIGINAL URI KEYS!!!
def module_filterselection(keys,time=True,delimit=False):
filterkeys, timekeys, delimitkeys, extrakeys = uri_to_internal(keys)
from malojatime import today, thisweek, thismonth, thisyear, alltime
filterkeys, timekeys, delimitkeys, extrakeys = uri_to_internal(keys)
# drop keys that are not relevant so they don't clutter the URI
if not time: timekeys = {}
if not delimit: delimitkeys = {}
if "page" in extrakeys: del extrakeys["page"]
internalkeys = {**filterkeys,**timekeys,**delimitkeys,**extrakeys}
html = ""
if time:
# all other keys that will not be changed by clicking another filter
#keystr = "?" + compose_querystring(keys,exclude=["since","to","in"])
unchangedkeys = internal_to_uri({**filterkeys,**delimitkeys,**extrakeys})
if time:
# wonky selector for precise date range
@ -513,139 +600,78 @@ def module_filterselection(keys,time=True,delimit=False):
# html += "to <input id='dateselect_to' onchange='datechange()' type='date' value='" + "-".join(todate) + "'/>"
# html += "</div>"
from malojatime import today, thisweek, thismonth, thisyear
### temp!!! this will not allow weekly rank changes
# weekday = ((now.isoweekday()) % 7)
# weekbegin = now - datetime.timedelta(days=weekday)
# weekend = weekbegin + datetime.timedelta(days=6)
# weekbegin = [weekbegin.year,weekbegin.month,weekbegin.day]
# weekend = [weekend.year,weekend.month,weekend.day]
# weekbeginstr = "/".join((str(num) for num in weekbegin))
# weekendstr = "/".join((str(num) for num in weekend))
# relative to current range
html += "<div>"
# if timekeys.get("timerange").next(-1) is not None:
# html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":timekeys.get("timerange").next(-1)})) + "'><span class='stat_selector'>«</span></a>"
# if timekeys.get("timerange").next(-1) is not None or timekeys.get("timerange").next(1) is not None:
# html += " " + timekeys.get("timerange").desc() + " "
# if timekeys.get("timerange").next(1) is not None:
# html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":timekeys.get("timerange").next(1)})) + "'><span class='stat_selector'>»</span></a>"
if timekeys.get("timerange").next(-1) is not None:
prevrange = timekeys.get("timerange").next(-1)
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":prevrange})) + "'><span class='stat_selector'>" + prevrange.desc() + "</span></a>"
thisrange = timekeys.get("timerange")
prevrange = thisrange.next(-1)
nextrange = thisrange.next(1)
if prevrange is not None:
link = compose_querystring(internal_to_uri({**internalkeys,"timerange":prevrange}))
html += "<a href='?" + link + "'><span class='stat_selector'>" + prevrange.desc() + "</span></a>"
html += " « "
if timekeys.get("timerange").next(-1) is not None or timekeys.get("timerange").next(1) is not None:
html += "<span class='stat_selector' style='opacity:0.5;'>" + timekeys.get("timerange").desc() + "</span>"
if timekeys.get("timerange").next(1) is not None:
if prevrange is not None or nextrange is not None:
html += "<span class='stat_selector' style='opacity:0.5;'>" + thisrange.desc() + "</span>"
if nextrange is not None:
html += " » "
nextrange = timekeys.get("timerange").next(1)
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":nextrange})) + "'><span class='stat_selector'>" + nextrange.desc() + "</span></a>"
html += "</div>"
# predefined ranges
html += "<div>"
if timekeys.get("timerange") == today():
html += "<span class='stat_selector' style='opacity:0.5;'>Today</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"today"}) + "'><span class='stat_selector'>Today</span></a>"
html += " | "
if timekeys.get("timerange") == thisweek():
html += "<span class='stat_selector' style='opacity:0.5;'>This Week</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"week"}) + "'><span class='stat_selector'>This Week</span></a>"
html += " | "
if timekeys.get("timerange") == thismonth():
html += "<span class='stat_selector' style='opacity:0.5;'>This Month</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"month"}) + "'><span class='stat_selector'>This Month</span></a>"
html += " | "
if timekeys.get("timerange") == thisyear():
html += "<span class='stat_selector' style='opacity:0.5;'>This Year</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"year"}) + "'><span class='stat_selector'>This Year</span></a>"
html += " | "
if timekeys.get("timerange") is None or timekeys.get("timerange").unlimited():
html += "<span class='stat_selector' style='opacity:0.5;'>All Time</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys) + "'><span class='stat_selector'>All Time</span></a>"
html += "</div>"
if delimit:
#keystr = "?" + compose_querystring(keys,exclude=["step","stepn"])
unchangedkeys = internal_to_uri({**filterkeys,**timekeys,**extrakeys})
# only for this element (delimit selector consists of more than one)
unchangedkeys_sub = internal_to_uri({k:delimitkeys[k] for k in delimitkeys if k not in ["step","stepn"]})
html += "<div>"
if delimitkeys.get("step") == "day" and delimitkeys.get("stepn") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Daily</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"day"}) + "'><span class='stat_selector'>Daily</span></a>"
html += " | "
if delimitkeys.get("step") == "week" and delimitkeys.get("stepn") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Weekly</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"week"}) + "'><span class='stat_selector'>Weekly</span></a>"
html += " | "
if delimitkeys.get("step") == "month" and delimitkeys.get("stepn") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Monthly</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"month"}) + "'><span class='stat_selector'>Monthly</span></a>"
html += " | "
if delimitkeys.get("step") == "year" and delimitkeys.get("stepn") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Yearly</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"year"}) + "'><span class='stat_selector'>Yearly</span></a>"
link = compose_querystring(internal_to_uri({**internalkeys,"timerange":nextrange}))
html += "<a href='?" + link + "'><span class='stat_selector'>" + nextrange.desc() + "</span></a>"
html += "</div>"
unchangedkeys_sub = internal_to_uri({k:delimitkeys[k] for k in delimitkeys if k != "trail"})
html += "<div>"
if delimitkeys.get("trail") == 1:
html += "<span class='stat_selector' style='opacity:0.5;'>Standard</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"1"}) + "'><span class='stat_selector'>Standard</span></a>"
html += " | "
categories = [
{
"active":time,
"options":{
"Today":{"timerange":today()},
"This Week":{"timerange":thisweek()},
"This Month":{"timerange":thismonth()},
"This Year":{"timerange":thisyear()},
"All Time":{"timerange":alltime()}
}
},
{
"active":delimit,
"options":{
"Daily":{"step":"day","stepn":1},
"Weekly":{"step":"week","stepn":1},
"Fortnightly":{"step":"week","stepn":2},
"Monthly":{"step":"month","stepn":1},
"Quarterly":{"step":"month","stepn":3},
"Yearly":{"step":"year","stepn":1}
}
},
{
"active":delimit,
"options":{
"Standard":{"trail":1},
"Trailing":{"trail":2},
"Long Trailing":{"trail":3},
"Inert":{"trail":10},
"Cumulative":{"trail":math.inf}
}
}
if delimitkeys.get("trail") == 2:
html += "<span class='stat_selector' style='opacity:0.5;'>Trailing</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"2"}) + "'><span class='stat_selector'>Trailing</span></a>"
html += " | "
]
if delimitkeys.get("trail") == 3:
html += "<span class='stat_selector' style='opacity:0.5;'>Long Trailing</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"3"}) + "'><span class='stat_selector'>Long Trailing</span></a>"
html += " | "
for c in categories:
if delimitkeys.get("trail") == math.inf:
html += "<span class='stat_selector' style='opacity:0.5;'>Cumulative</span>"
else:
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"cumulative":"yes"}) + "'><span class='stat_selector'>Cumulative</span></a>"
if c["active"]:
html += "</div>"
optionlist = []
for option in c["options"]:
values = c["options"][option]
link = "?" + compose_querystring(internal_to_uri({**internalkeys,**values}))
if all(internalkeys.get(k) == values[k] for k in values):
optionlist.append("<span class='stat_selector' style='opacity:0.5;'>" + option + "</span>")
else:
optionlist.append("<a href='" + link + "'><span class='stat_selector'>" + option + "</span></a>")
html += "<div>" + " | ".join(optionlist) + "</div>"
return html

22
maloja
View File

@ -183,12 +183,21 @@ def getInstance():
except:
return None
def getInstanceSupervisor():
try:
output = subprocess.check_output(["pidof","maloja_supervisor"])
pid = int(output)
return pid
except:
return None
def start():
if install():
if gotodir():
setup()
p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
p = subprocess.Popen(["python3","supervisor.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
print(green("Maloja started!") + " PID: " + str(p.pid))
from doreah import settings
@ -221,8 +230,12 @@ def restart():
return wasrunning
def stop():
pid_sv = getInstanceSupervisor()
if pid_sv is not None:
os.kill(pid_sv,signal.SIGTERM)
pid = getInstance()
if pid == None:
if pid is None:
print("Server is not running")
return False
else:
@ -283,8 +296,13 @@ def update():
print("Done!")
os.chmod("./maloja",os.stat("./maloja").st_mode | stat.S_IXUSR)
os.chmod("./update_requirements.sh",os.stat("./update_requirements.sh").st_mode | stat.S_IXUSR)
print("Make sure to update required modules! (" + yellow("pip3 install -r requirements.txt --upgrade --no-cache-dir") + ")")
try:
returnval = os.system("./update_requirements.sh")
assert returnval == 0
except:
print("Make sure to update required modules! (" + yellow("./update_requirements.sh") + ")")
if stop(): start() #stop returns whether it was running before, in which case we restart it

View File

@ -391,6 +391,7 @@ def time_fix(t):
if isinstance(t,MRangeDescriptor): return t
if isinstance(t, str):
if t in ["alltime"]: return None
tod = datetime.datetime.utcnow()
months = ["january","february","march","april","may","june","july","august","september","october","november","december"]
weekdays = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"]
@ -545,9 +546,8 @@ def time_stamps(since=None,to=None,within=None,range=None):
def delimit_desc(step="month",stepn=1,trail=1):
txt = ""
if stepn is not 1: txt += _num(stepn) + "-"
if stepn is not 1: txt += str(stepn) + "-"
txt += {"year":"Yearly","month":"Monthly","week":"Weekly","day":"Daily"}[step.lower()]
#if trail is not 1: txt += " " + _num(trail) + "-Trailing"
if trail is math.inf: txt += " Cumulative"
elif trail is not 1: txt += " Trailing" #we don't need all the info in the title
@ -587,10 +587,11 @@ def ranges(since=None,to=None,within=None,timerange=None,step="month",stepn=1,tr
d_start = d_start.next(stepn-1) #last part of first included range
i = 0
current_end = d_start
current_start = current_end.next((stepn*trail-1)*-1)
#ranges = []
while current_end.first_stamp() <= lastincluded and (max_ is None or i < max_):
while current_end.first_stamp() < lastincluded and (max_ is None or i < max_):
current_start = current_end.next((stepn*trail-1)*-1)
if current_start == current_end:
yield current_start
#ranges.append(current_start)
@ -598,6 +599,7 @@ def ranges(since=None,to=None,within=None,timerange=None,step="month",stepn=1,tr
yield MRange(current_start,current_end)
#ranges.append(MRange(current_start,current_end))
current_end = current_end.next(stepn)
current_start = current_end.next((stepn*trail-1)*-1)
i += 1
@ -619,6 +621,8 @@ def thismonth():
def thisyear():
tod = datetime.datetime.utcnow()
return MTime(tod.year)
def alltime():
return MRange(None,None)
#def _get_start_of(timestamp,unit):
# date = datetime.datetime.utcfromtimestamp(timestamp)

Binary file not shown.

View File

@ -1,6 +1,7 @@
bottle>=0.12.16
waitress>=1.3
doreah>=0.9.1
doreah>=1.1.7
nimrodel>=0.4.9
setproctitle>=1.1.10
wand>=0.5.4
lesscpy>=0.13

View File

@ -10,4 +10,5 @@ countas S Club 7 Tina Barrett
countas RenoakRhythm Approaching Nirvana
countas Shirley Manson Garbage
countas Lewis Brindley The Yogscast
countas Sips The Yogscast
countas Sips The Yogscast
countas Sjin The Yogscast

Can't render this file because it has a wrong number of fields in line 5.

View File

@ -16,6 +16,7 @@ replacetitle Cause I'm God Girl Roll Deep
countas 2Yoon 4Minute
replaceartist 4minute 4Minute
replacetitle 미쳐 Crazy
addartists HyunA Change Jun Hyung
# BLACKPINK
countas Jennie BLACKPINK
@ -47,8 +48,8 @@ replacetitle 나비 (Butterfly) Butterfly
replacetitle Déjà vu Déjà Vu
replacetitle 라차타 (LA chA TA) LA chA TA
replacetitle 여우 같은 내 친구 (No More) No More
replacetitle 시그널 (Signal) Signal
replacetitle 미행 (그림자 : Shadow) Shadow
replacetitle 시그널 (Signal) Signal
replacetitle 미행 (그림자 : Shadow) Shadow
# Stellar
replaceartist STELLAR Stellar
@ -58,6 +59,7 @@ replacetitle 찔려 Sting Sting
# Red Velvet
countas Seulgi Red Velvet
countas Joy Red Velvet
replacetitle 러시안 룰렛 Russian Roulette Russian Roulette
replacetitle 피카부 Peek-a-Boo Peek-A-Boo
replacetitle 빨간 맛 Red Flavor Red Flavor
@ -81,6 +83,7 @@ replacetitle CHEER UP Cheer Up
replacetitle OOH-AHH하게 Like OOH-AHH Like Ooh-Ahh
replacetitle OOH-AHH Like Ooh-Ahh
replacetitle LIKEY Likey
countas Tzuyu TWICE
# AOA
countas AOA Black AOA
@ -145,5 +148,8 @@ replaceartist A pink Apink
# Chungha & IOI
replaceartist CHUNG HA Chungha
replaceartist 청하 CHUNGHA Chungha
#countas Chungha I.O.I # Chungha is too famous
#countas Chungha I.O.I # Chungha is too famous
replacetitle 벌써 12시 Gotta Go Gotta Go
# ITZY
replacetitle 달라달라 (DALLA DALLA) Dalla Dalla

Can't render this file because it has a wrong number of fields in line 5.

View File

@ -16,6 +16,10 @@ The first column defines the type of the rule:
This will not change the separation in the database and all effects of this rule will disappear as soon as it is no longer active.
Second column is the artist
Third column the replacement artist / grouping label
addartists Defines a certain combination of artists and song title that should always have other artists added.
Second column is artists that need to be already present for this rule to apply
Third column is the song title
Fourth column are artists that shoud be added, separated by ␟
Rules in non-tsv files are ignored. '#' is used for comments. Additional columns are ignored. To have a '#' in a name, use '\num'
Comments are not supported in scrobble lists, but you probably never edit these manually anyway.
@ -30,3 +34,4 @@ replacetitle 첫 사랑니 (Rum Pum Pum Pum) Rum Pum Pum Pum
replaceartist Dal Shabet Dal★Shabet
replaceartist Mr FijiWiji, AgNO3 Mr FijiWiji␟AgNO3 # one artist is replaced by two artists
countas Trouble Maker HyunA
addartists HyunA Change Jun Hyung

View File

@ -28,6 +28,18 @@ pages = {
"https://open.spotify.com"
],
"script":"spotify.js"
},
"Bandcamp":{
"patterns":[
"bandcamp.com"
],
"script":"bandcamp.js"
},
"Soundcloud":{
"patterns":[
"https://soundcloud.com"
],
"script":"soundcloud.js"
}
}
@ -51,7 +63,7 @@ function onTabUpdated(tabId, changeInfo, tab) {
patterns = pages[page]["patterns"];
//console.log("Page was managed by a " + page + " manager")
for (var i=0;i<patterns.length;i++) {
if (tab.url.startsWith(patterns[i])) {
if (tab.url.includes(patterns[i])) {
//console.log("Still on same page!")
tabManagers[tabId].update();
@ -67,7 +79,7 @@ function onTabUpdated(tabId, changeInfo, tab) {
if (pages.hasOwnProperty(key)) {
patterns = pages[key]["patterns"];
for (var i=0;i<patterns.length;i++) {
if (tab.url.startsWith(patterns[i])) {
if (tab.url.includes(patterns[i])) {
console.log("New page on tab " + tabId + " will be handled by new " + key + " manager!");
tabManagers[tabId] = new Controller(tabId,key);
updateTabNum();
@ -166,8 +178,13 @@ class Controller {
actuallyupdate() {
this.messageID++;
//console.log("Update! Our page is " + this.page + ", our tab id " + this.tabId)
chrome.tabs.executeScript(this.tabId,{"file":"sites/" + pages[this.page]["script"]});
chrome.tabs.executeScript(this.tabId,{"file":"sitescript.js"});
try {
chrome.tabs.executeScript(this.tabId,{"file":"sites/" + pages[this.page]["script"]});
chrome.tabs.executeScript(this.tabId,{"file":"sitescript.js"});
}
catch (e) {
console.log("Could not run site script. Tab probably closed or something idk.")
}
this.alreadyQueued = false;
}

View File

@ -1,6 +1,6 @@
{
"name": "Maloja Scrobbler",
"version": "1.3",
"version": "1.4",
"description": "Scrobbles tracks from various sites to your Maloja server",
"manifest_version": 2,
"permissions": ["activeTab",

View File

@ -0,0 +1,15 @@
maloja_scrobbler_selector_playbar = "//div[contains(@class,'trackView')]"
maloja_scrobbler_selector_metadata = "."
// need to select everything as bar / metadata block because artist isn't shown in the inline player
maloja_scrobbler_selector_title = ".//span[@class='title']/text()"
maloja_scrobbler_selector_artist = ".//span[contains(@itemprop,'byArtist')]/a/text()"
maloja_scrobbler_selector_duration = ".//span[@class='time_total']/text()"
maloja_scrobbler_selector_control = ".//td[@class='play_cell']/a[@role='button']/div[contains(@class,'playbutton')]/@class"
maloja_scrobbler_label_playing = "playbutton playing"
maloja_scrobbler_label_paused = "playbutton"

View File

@ -0,0 +1,14 @@
maloja_scrobbler_selector_playbar = "//div[contains(@class,'playControls')]"
maloja_scrobbler_selector_metadata = ".//div[contains(@class,'playControls__soundBadge')]//div[contains(@class,'playbackSoundBadge__titleContextContainer')]"
maloja_scrobbler_selector_title = ".//div/a/@title"
maloja_scrobbler_selector_artist = ".//a/text()"
maloja_scrobbler_selector_duration = ".//div[contains(@class,'playbackTimeline__duration')]//span[@aria-hidden='true']/text()"
maloja_scrobbler_selector_control = ".//button[contains(@class,'playControl')]/@title"
maloja_scrobbler_label_playing = "Pause current"
maloja_scrobbler_label_paused = "Play current"

View File

@ -9,4 +9,4 @@ maloja_scrobbler_selector_artist = "./text()"
maloja_scrobbler_selector_duration = ".//div[@class='playback-bar__progress-time'][2]/text()"
maloja_scrobbler_selector_control = ".//div[contains(@class,'player-controls__buttons')]/button[3]/@title"
maloja_scrobbler_selector_control = ".//div[contains(@class,'player-controls__buttons')]/div[3]/button/@title"

View File

@ -65,12 +65,24 @@ else {
control = bar.xpath(maloja_scrobbler_selector_control, XPathResult.STRING_TYPE);
if (control == "Play") {
try {
label_playing = maloja_scrobbler_label_playing
}
catch {
label_playing = "Pause"
}
try {
label_paused = maloja_scrobbler_label_paused
}
catch {
label_paused = "Play"
}
if (control == label_paused) {
console.log("Not playing right now");
chrome.runtime.sendMessage({type:"stopPlayback",time:Math.floor(Date.now()),artist:artist,title:title});
//stopPlayback()
}
else if (control == "Pause") {
else if (control == label_playing) {
console.log("Playing " + artist + " - " + title + " (" + durationSeconds + " sec)");
chrome.runtime.sendMessage({type:"startPlayback",time:Math.floor(Date.now()),artist:artist,title:title,duration:durationSeconds});
//startPlayback(artist,title,durationSeconds)

View File

@ -1,17 +1,17 @@
#!/usr/bin/env python
# server stuff
from bottle import Bottle, route, get, post, error, run, template, static_file, request, response, FormsDict, redirect, template
from bottle import Bottle, route, get, post, error, run, template, static_file, request, response, FormsDict, redirect, template, HTTPResponse
import waitress
# monkey patching
import monkey
# rest of the project
import database
import utilities
import htmlmodules
import htmlgenerators
import malojatime
from utilities import *
import utilities
from utilities import resolveImage
from urihandler import uri_to_internal, remove_identical
import urihandler
# doreah toolkit
@ -26,20 +26,28 @@ import os
import setproctitle
# url handling
import urllib
import urllib.request
import urllib.parse
from urllib.error import *
#settings.config(files=["settings/default.ini","settings/settings.ini"])
#settings.update("settings/default.ini","settings/settings.ini")
MAIN_PORT = settings.get_settings("WEB_PORT")
HOST = settings.get_settings("HOST")
webserver = Bottle()
import lesscpy
css = ""
for f in os.listdir("website/less"):
css += lesscpy.compile("website/less/" + f)
os.makedirs("website/css",exist_ok=True)
with open("website/css/style.css","w") as f:
f.write(css)
@webserver.route("")
@webserver.route("/")
def mainpage():
@ -77,7 +85,11 @@ def customerror(error):
def graceful_exit(sig=None,frame=None):
#urllib.request.urlopen("http://[::1]:" + str(DATABASE_PORT) + "/sync")
database.sync()
log("Received signal to shutdown")
try:
database.sync()
except Exception as e:
log("Error while shutting down!",e)
log("Server shutting down...")
os._exit(42)
@ -121,6 +133,7 @@ def static_image(pth):
#@webserver.route("/<name:re:.*\\.html>")
@webserver.route("/<name:re:.*\\.js>")
@webserver.route("/<name:re:.*\\.css>")
@webserver.route("/<name:re:.*\\.less>")
@webserver.route("/<name:re:.*\\.png>")
@webserver.route("/<name:re:.*\\.jpeg>")
@webserver.route("/<name:re:.*\\.ico>")
@ -132,7 +145,7 @@ def static(name):
@webserver.route("/<name>")
def static_html(name):
linkheaders = ["</css/maloja.css>; rel=preload; as=style"]
linkheaders = ["</css/style.css>; rel=preload; as=style"]
keys = remove_identical(FormsDict.decode(request.query))
# if a pyhp file exists, use this
@ -206,6 +219,16 @@ def static_html(name):
return html
#return static_file("website/" + name + ".html",root="")
# Shortlinks
@webserver.get("/artist/<artist>")
def redirect_artist(artist):
redirect("/artist?artist=" + artist)
@webserver.get("/track/<artists:path>/<title>")
def redirect_track(artists,title):
redirect("/track?title=" + title + "&" + "&".join("artist=" + artist for artist in artists.split("/")))
#set graceful shutdown
signal.signal(signal.SIGINT, graceful_exit)
signal.signal(signal.SIGTERM, graceful_exit)
@ -215,8 +238,7 @@ setproctitle.setproctitle("Maloja")
## start database
database.start_db()
#database.register_subroutes(webserver,"/api")
database.dbserver.mount(server=webserver)
log("Starting up Maloja server...")
run(webserver, host='::', port=MAIN_PORT, server='waitress')
run(webserver, host=HOST, port=MAIN_PORT, server='waitress')

View File

@ -1,19 +1,30 @@
# Do not change settings in this file
# Instead, simply write an entry with the same name in your own settings.ini file
# Category headers in [brackets] are only for organization and not necessary
[HTTP]
WEB_PORT = 42010
HOST = "::" # You most likely want either :: for IPv6 or 0.0.0.0 for IPv4 here
[Third Party Services]
LASTFM_API_KEY = "ASK" # 'ASK' signifies that the user has not yet indicated to not use any key at all.
LASTFM_API_SECRET = "ASK"
FANARTTV_API_KEY = "ASK"
SPOTIFY_API_ID = "ASK"
SPOTIFY_API_SECRET = "ASK"
CACHE_EXPIRE_NEGATIVE = 30 # after how many days negative results should be tried again
CACHE_EXPIRE_POSITIVE = 300 # after how many days positive results should be refreshed
# Can be 'YouTube', 'YouTube Music', 'Google Play Music', 'Spotify', 'Tidal', 'SoundCloud', 'Deezer', 'Amazon Music'
# Omit or set to none to disable
TRACK_SEARCH_PROVIDER = None
[Database]
DB_CACHE_SIZE = 8192 # how many MB on disk each database cache should have available.
INVALID_ARTISTS = ["[Unknown Artist]","Unknown Artist"]
[Local Images]
@ -27,8 +38,11 @@ LOCAL_IMAGE_ROTATE = 3600 # when multiple images are present locally, how many s
DEFAULT_RANGE_CHARTS_ARTISTS = year
DEFAULT_RANGE_CHARTS_TRACKS = year
# same for pulse view
# can be days, weeks, months, years
DEFAULT_RANGE_PULSE = months
# can be day, week, month, year
DEFAULT_STEP_PULSE = month
# display top tiles on artist and track chart pages
CHARTS_DISPLAY_TILES = false
[Fluff]
@ -36,6 +50,8 @@ DEFAULT_RANGE_PULSE = months
SCROBBLES_GOLD = 250
SCROBBLES_PLATINUM = 500
SCROBBLES_DIAMOND = 1000
# name for comparisons
NAME = "Generic Maloja User"
[Misc]

25
supervisor.py Normal file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
import subprocess
import time
import setproctitle
import signal
from doreah.logging import log
setproctitle.setproctitle("maloja_supervisor")
while True:
time.sleep(60)
try:
output = subprocess.check_output(["pidof","Maloja"])
pid = int(output)
log("Maloja is running, PID " + str(pid),module="supervisor")
except:
log("Maloja is not running, restarting...",module="supervisor")
try:
p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
except e:
log("Error starting Maloja: " + str(e),module="supervisor")

2
update_requirements.sh Normal file
View File

@ -0,0 +1,2 @@
#!/bin/sh
pip3 install -r requirements.txt --upgrade --no-cache-dir

View File

@ -103,8 +103,12 @@ def uri_to_internal(keys,forceTrack=False,forceArtist=False):
#4
resultkeys4 = {"max_":300}
if "max" in keys: resultkeys4["max_"] = int(keys["max"])
resultkeys4 = {"page":0,"perpage":100}
# if "max" in keys: resultkeys4["max_"] = int(keys["max"])
if "max" in keys: resultkeys4["page"],resultkeys4["perpage"] = 0, int(keys["max"])
#different max than the internal one! the user doesn't get to disable pagination
if "page" in keys: resultkeys4["page"] = int(keys["page"])
if "perpage" in keys: resultkeys4["perpage"] = int(keys["perpage"])
return resultkeys1, resultkeys2, resultkeys3, resultkeys4
@ -146,8 +150,12 @@ def internal_to_uri(keys):
urikeys.append("trail",str(keys["trail"]))
# stuff
if "max_" in keys:
urikeys.append("max",str(keys["max_"]))
#if "max_" in keys:
# urikeys.append("max",str(keys["max_"]))
if "page" in keys:
urikeys.append("page",str(keys["page"]))
if "perpage" in keys:
urikeys.append("perpage",str(keys["perpage"]))
return urikeys

View File

@ -99,14 +99,11 @@ def consistentRulestate(folder,checksums):
if (scrobblefile.endswith(".tsv")):
try:
f = open(folder + "/" + scrobblefile + ".rulestate","r")
if f.read() != checksums:
return False
with open(folder + "/" + scrobblefile + ".rulestate","r") as f:
if f.read() != checksums:
return False
except:
return False
finally:
f.close()
return True
@ -442,8 +439,12 @@ def update_medals():
from database import MEDALS, MEDALS_TRACKS, STAMPS, get_charts_artists, get_charts_tracks
firstyear = datetime.datetime.utcfromtimestamp(STAMPS[0]).year
currentyear = datetime.datetime.utcnow().year
try:
firstyear = datetime.datetime.utcfromtimestamp(STAMPS[0]).year
except:
firstyear = currentyear
MEDALS.clear()
for year in range(firstyear,currentyear):
@ -468,3 +469,23 @@ def update_medals():
elif t["rank"] == 2: MEDALS_TRACKS.setdefault(track,{}).setdefault("silver",[]).append(year)
elif t["rank"] == 3: MEDALS_TRACKS.setdefault(track,{}).setdefault("bronze",[]).append(year)
else: break
@daily
def update_weekly():
from database import WEEKLY_TOPTRACKS, WEEKLY_TOPARTISTS, get_charts_artists, get_charts_tracks
from malojatime import ranges, thisweek
WEEKLY_TOPARTISTS.clear()
WEEKLY_TOPTRACKS.clear()
for week in ranges(step="week"):
if week == thisweek(): break
for a in get_charts_artists(timerange=week):
artist = a["artist"]
if a["rank"] == 1: WEEKLY_TOPARTISTS[artist] = WEEKLY_TOPARTISTS.setdefault(artist,0) + 1
for t in get_charts_tracks(timerange=week):
track = (frozenset(t["track"]["artists"]),t["track"]["title"])
if t["rank"] == 1: WEEKLY_TOPTRACKS[track] = WEEKLY_TOPTRACKS.setdefault(track,0) + 1

View File

@ -4,7 +4,8 @@
<head>
<meta charset="UTF-8" />
<title>Maloja - KEY_ARTISTNAME</title>
<script src="javascript/rangeselect.js" async></script>
<script src="javascript/cookies.js"></script>
<script src="javascript/rangeselect.js"></script>
</head>
<body>
@ -15,13 +16,13 @@
</td>
<td class="text">
<h1>KEY_ARTISTNAME</h1>
<span class="rank"><a href="/charts_artists?max=100">KEY_POSITION</a></span>
<span class="rank"><a href="/charts_artists">KEY_POSITION</a></span>
<br/>
<span>KEY_ASSOCIATED</span>
<p class="stats"><a href="/scrobbles?artist=KEY_ENC_ARTISTNAME">KEY_SCROBBLES Scrobbles</a></p>
<p class="desc">KEY_DESCRIPTION</p>
<span>KEY_MEDALS</span>
<span>KEY_MEDALS</span> <span>KEY_TOPWEEKS</span> <span>KEY_CERTS</span>
</td>
</tr>
</table>
@ -32,35 +33,35 @@
<table class="twopart">
<tr>
<td>
<h2><a href='/pulse?artist=KEY_ENC_ARTISTNAME&step=year&trail=1'>Pulse</a></h2>
<h2><a class="stat_link_pulse" href='/pulse?artist=KEY_ENC_ARTISTNAME&trail=1&step=month'>Pulse</a></h2>
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span>
<span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PULSE_WEEKS</span>
<span class="stat_module_pulse pulse_month">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_day" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_year" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEKS</span>
</td>
<td>
<!-- We use the same classes / function calls here because we want it to switch together with pulse -->
<h2><a href='/performance?artist=KEY_ENC_CREDITEDARTISTNAME&step=year&trail=1'>Performance</a></h2>
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span>
<h2><a class="stat_link_pulse" href='/performance?artist=KEY_ENC_CREDITEDARTISTNAME&trail=1&step=month'>Performance</a></h2>
<span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PERFORMANCE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PERFORMANCE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PERFORMANCE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PERFORMANCE_WEEKS</span>
<span class="stat_module_pulse pulse_month">KEY_PERFORMANCE_MONTHS</span>
<span class="stat_module_pulse pulse_day" style="display:none;">KEY_PERFORMANCE_DAYS</span>
<span class="stat_module_pulse pulse_year" style="display:none;">KEY_PERFORMANCE_YEARS</span>
<span class="stat_module_pulse pulse_week" style="display:none;">KEY_PERFORMANCE_WEEKS</span>
</td>
</tr>
</table>

View File

@ -5,7 +5,7 @@ from malojatime import today,thisweek,thismonth,thisyear
def instructions(keys):
from utilities import getArtistImage
from htmlgenerators import artistLink, artistLinks
from htmlgenerators import artistLink, artistLinks, link_address
from urihandler import compose_querystring, uri_to_internal
from htmlmodules import module_pulse, module_performance, module_trackcharts, module_scrobblelist
@ -22,13 +22,31 @@ def instructions(keys):
if "medals" in data and data["medals"] is not None:
if "gold" in data["medals"]:
for y in data["medals"]["gold"]:
html_medals += "<a title='Best Artist in " + str(y) + "' class='hidelink medal shiny gold' href='/charts_artists?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_medals += "<a title='Best Artist in " + str(y) + "' class='hidelink medal shiny gold' href='/charts_artists?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
if "silver" in data["medals"]:
for y in data["medals"]["silver"]:
html_medals += "<a title='Second Best Artist in " + str(y) + "' class='hidelink medal shiny silver' href='/charts_artists?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_medals += "<a title='Second Best Artist in " + str(y) + "' class='hidelink medal shiny silver' href='/charts_artists?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
if "bronze" in data["medals"]:
for y in data["medals"]["bronze"]:
html_medals += "<a title='Third Best Artist in " + str(y) + "' class='hidelink medal shiny bronze' href='/charts_artists?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_medals += "<a title='Third Best Artist in " + str(y) + "' class='hidelink medal shiny bronze' href='/charts_artists?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_cert = ""
for track in database.get_tracks(artist=artist):
info = database.trackInfo(track)
if info.get("certification") is not None:
img = "/media/record_{cert}.png".format(cert=info["certification"])
trackname = track["title"].replace("'","&#39;")
tracklink = link_address(track)
tooltip = "{title} has reached {cert} status".format(title=trackname,cert=info["certification"].capitalize())
html_cert += "<a href='{link}'><img class='certrecord_small' src='{img}' title='{tooltip}' /></a>".format(tooltip=tooltip,img=img,link=tracklink)
html_topweeks = ""
if data.get("topweeks") not in [0,None]:
link = "/performance?artist=" + urllib.parse.quote(keys["artist"]) + "&trail=1&step=week"
title = str(data["topweeks"]) + " weeks on #1"
html_topweeks = "<a title='" + title + "' href='" + link + "'><img class='star' src='/media/star.png' />" + str(data["topweeks"]) + "</a>"
credited = data.get("replace")
includestr = " "
@ -69,6 +87,8 @@ def instructions(keys):
"KEY_POSITION":pos,
"KEY_ASSOCIATED":includestr,
"KEY_MEDALS":html_medals,
"KEY_CERTS":html_cert,
"KEY_TOPWEEKS":html_topweeks,
# tracks
"KEY_TRACKLIST":html_tracks,
# pulse

View File

@ -11,7 +11,7 @@
<table class="top_info">
<tr>
<td class="image">
<div style="background-image:url('KEY_TOPARTIST_IMAGEURL')"></div>
KEY_TOPARTIST_IMAGEDIV
</td>
<td class="text">
<h1>Artist Charts</h1><a href="/top_artists"><span>View #1 Artists</span></a><br/>
@ -24,6 +24,9 @@
</tr>
</table>
<span class="stat_module_topartists">
KEY_ARTISTCHART
</span>
KEY_ARTISTLIST

View File

@ -4,8 +4,9 @@ import urllib
def instructions(keys):
from utilities import getArtistImage
from urihandler import compose_querystring, uri_to_internal
from htmlmodules import module_artistcharts, module_filterselection
from htmlmodules import module_artistcharts, module_filterselection, module_artistcharts_tiles
from malojatime import range_desc
from doreah.settings import get_settings
_, timekeys, _, amountkeys = uri_to_internal(keys)
@ -16,6 +17,7 @@ def instructions(keys):
html_charts, rep = module_artistcharts(**amountkeys,**timekeys)
if rep is not None:
@ -23,12 +25,23 @@ def instructions(keys):
else:
imgurl = ""
html_tiles = ""
if get_settings("CHARTS_DISPLAY_TILES"):
html_tiles = module_artistcharts_tiles(timerange=timekeys["timerange"])
imgurl = "favicon.png"
imgdiv = '<div style="background-image:url('+imgurl+')"></div>'
pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else []
replace = {"KEY_TOPARTIST_IMAGEURL":imgurl,
"KEY_ARTISTLIST":html_charts,
"KEY_RANGE":limitstring,
"KEY_FILTERSELECTOR":html_filterselector}
replace = {
"KEY_TOPARTIST_IMAGEDIV":imgdiv,
"KEY_ARTISTCHART":html_tiles,
"KEY_ARTISTLIST":html_charts,
"KEY_RANGE":limitstring,
"KEY_FILTERSELECTOR":html_filterselector
}
return (replace,pushresources)

View File

@ -10,7 +10,7 @@
<table class="top_info">
<tr>
<td class="image">
<div style="background-image:url('KEY_TOPARTIST_IMAGEURL')"></div>
KEY_TOPARTIST_IMAGEDIV
</td>
<td class="text">
<h1>Track Charts</h1>TOP_TRACKS_LINK<br/>
@ -22,6 +22,9 @@
</tr>
</table>
<span class="stat_module_topartists">
KEY_TRACKCHART
</span>
KEY_TRACKLIST

View File

@ -5,8 +5,9 @@ def instructions(keys):
from utilities import getArtistImage, getTrackImage
from htmlgenerators import artistLink
from urihandler import compose_querystring, uri_to_internal
from htmlmodules import module_trackcharts, module_filterselection
from htmlmodules import module_trackcharts, module_filterselection, module_trackcharts_tiles
from malojatime import range_desc
from doreah.settings import get_settings
filterkeys, timekeys, _, amountkeys = uri_to_internal(keys)
@ -23,6 +24,9 @@ def instructions(keys):
html_charts, rep = module_trackcharts(**amountkeys,**timekeys,**filterkeys)
html_tiles = ""
if filterkeys.get("artist") is not None:
imgurl = getArtistImage(filterkeys.get("artist"))
limitstring = "by " + artistLink(filterkeys.get("artist"))
@ -31,6 +35,15 @@ def instructions(keys):
else:
imgurl = ""
html_tiles = ""
if get_settings("CHARTS_DISPLAY_TILES"):
html_tiles = module_trackcharts_tiles(timerange=timekeys["timerange"])
imgurl = "favicon.png"
imgdiv = '<div style="background-image:url('+imgurl+')"></div>'
limitstring += " " + timekeys["timerange"].desc(prefix=True)
pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else []
@ -38,7 +51,8 @@ def instructions(keys):
replace = {
"KEY_TOPARTIST_IMAGEURL":imgurl,
"KEY_TOPARTIST_IMAGEDIV":imgdiv,
"KEY_TRACKCHART":html_tiles,
"KEY_TRACKLIST":html_charts,
"KEY_LIMITS":limitstring,
"KEY_FILTERSELECTOR":html_filterselector,

View File

@ -1,3 +1,9 @@
<meta name="description" content='Maloja is a self-hosted music scrobble server.' />
<!--<link rel="stylesheet/less" href="/less/maloja.less" />
<link rel="stylesheet/less" href="/less/grisons.less" />
<link rel="stylesheet" href="/css/maloja.css" />
<script src="/javascript/search.js" async="yes"></script>
<link rel="stylesheet" href="/css/grisons.css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/less.js/3.9.0/less.min.js" ></script> -->
<link rel="stylesheet" href="/css/style.css" />
<script src="/javascript/search.js" async></script>

78
website/compare.html Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Maloja - Compare</title>
<style>
.comparecircle {
height:500px;
width:500px;
border-radius:250px;
border: 1px solid rgba(245,245,220,0.3);
margin:auto;
margin-top:100px;
text-align:center;
line-height:500px;
font-size:60px;
color:black;
/* background-image: linear-gradient(to right,KEY_CIRCLE_CSS); */
background-image: radial-gradient(#KEY_CICLE_COLOR KEY_FULLMATCHpx, transparent KEY_PARTIALMATCHpx);
}
table tr td:first-child {
text-align: left;
padding:10px;
width:33%;
}
table tr td {
text-align: center;
padding:10px;
}
table tr td:last-child {
text-align: right;
padding:10px;
width:33%;
}
</style>
</head>
<body>
<table style="width:99%;">
<tr>
<td><h1>KEY_NAME_SELF</h1></td>
<td>
<div class="comparecircle">
KEY_MATCH%
</div>
</td>
<td><h1>KEY_NAME_OTHER</h1></td>
</tr>
<tr>
<td></td>
<td style="font-size:70%;color:grey;">
The size of the circle shows matching music taste.
The fuzziness of its border indicates differences in quantity.
</td>
<td></td>
</tr>
<tr>
<td></td>
<td>
<span>Common Favorite</span>
<h2 style="margin:7px;">KEY_BESTARTIST_LINK</h2>
<img src="KEY_BESTARTIST_IMAGE" style="width:80px;" />
</td>
<td></td>
</tr>
</table>
</body>
</html>

88
website/compare.py Normal file
View File

@ -0,0 +1,88 @@
import urllib
import database
import json
from htmlgenerators import artistLink
from utilities import getArtistImage
def instructions(keys):
compareto = keys.get("to")
compareurl = compareto + "/api/info"
response = urllib.request.urlopen(compareurl)
strangerinfo = json.loads(response.read())
owninfo = database.info()
artists = {}
for a in owninfo["artists"]:
artists[a.lower()] = {"name":a,"self":int(owninfo["artists"][a]*1000),"other":0}
for a in strangerinfo["artists"]:
artists[a.lower()] = artists.setdefault(a.lower(),{"name":a,"self":0})
artists[a.lower()]["other"] = int(strangerinfo["artists"][a]*1000)
for a in artists:
common = min(artists[a]["self"],artists[a]["other"])
artists[a]["self"] -= common
artists[a]["other"] -= common
artists[a]["common"] = common
best = sorted((artists[a]["name"] for a in artists),key=lambda x: artists[x.lower()]["common"],reverse=True)
result = {
"unique_self":sum(artists[a]["self"] for a in artists if artists[a]["common"] == 0),
"more_self":sum(artists[a]["self"] for a in artists if artists[a]["common"] != 0),
# "common":{
# **{
# artists[a]["name"]:artists[a]["common"]
# for a in best[:3]},
# None: sum(artists[a]["common"] for a in artists if a not in best[:3])
# },
"common":sum(artists[a]["common"] for a in artists),
"more_other":sum(artists[a]["other"] for a in artists if artists[a]["common"] != 0),
"unique_other":sum(artists[a]["other"] for a in artists if artists[a]["common"] == 0)
}
total = sum(result[c] for c in result)
percentages = {c:result[c]*100/total for c in result}
css = []
cumulative = 0
for color,category in [
("rgba(255,255,255,0.2)","unique_self"),
("rgba(255,255,255,0.5)","more_self"),
("white","common"),
("rgba(255,255,255,0.5)","more_other"),
("rgba(255,255,255,0.2)","unique_other")]:
cumulative += percentages[category]
css.append(color + " " + str(cumulative) + "%")
fullmatch = percentages["common"]
partialmatch = percentages["more_self"] + percentages["more_other"]
match = fullmatch + (partialmatch)/2
pixel_fullmatch = fullmatch * 2.5
pixel_partialmatch = (fullmatch+partialmatch) * 2.5
match = min(match,100)
matchcolor = format(int(min(1,match/50)*255),"02x") * 2 + format(int(max(0,match/50-1)*255),"02x")
return {
"KEY_CIRCLE_CSS":",".join(css),
"KEY_CICLE_COLOR":matchcolor,
"KEY_MATCH":str(round(match,2)),
"KEY_FULLMATCH":str(int(pixel_fullmatch)),
"KEY_PARTIALMATCH":str(int(pixel_partialmatch)),
"KEY_NAME_SELF":owninfo["name"],
"KEY_NAME_OTHER":strangerinfo["name"],
"KEY_BESTARTIST_LINK":artistLink(best[0]),
"KEY_BESTARTIST_IMAGE":getArtistImage(best[0])
},[]

View File

@ -7,7 +7,7 @@
<script src="javascript/cookies.js"></script>
</head>
<body onload="insertAPIKeyFromCookie()">
<body>
<table class="top_info">
<tr>
<td class="image">

View File

@ -1,32 +1,81 @@
apikeycorrect = false;
function insertAPIKeyFromCookie() {
cookies = decodeURIComponent(document.cookie).split(';');
for(var i = 0; i <cookies.length; i++) {
cookies[i] = cookies[i].trim()
if (cookies[i].startsWith("apikey=")) {
document.getElementById("apikey").value = cookies[i].replace("apikey=","")
checkAPIkey()
}
var cookies = {};
function getCookies() {
cookiestrings = decodeURIComponent(document.cookie).split(';');
for(var i = 0; i <cookiestrings.length; i++) {
cookiestrings[i] = cookiestrings[i].trim();
[key,value] = cookiestrings[i].split("=");
cookies[key] = value;
}
}
// always on document load, but call specifically when needed early
document.addEventListener("load",getCookies);
function setCookie(key,val,session=true) {
cookies[key] = val;
if (!session) {
var d = new Date();
d.setTime(d.getTime() + (500*24*60*60*1000));
expirestr = "expires=" + d.toUTCString();
}
else {
expirestr = ""
}
document.cookie = encodeURIComponent(key) + "=" + encodeURIComponent(val) + ";" + expirestr;
}
function saveCookies() {
for (var c in cookies) {
document.cookie = encodeURIComponent(c) + "=" + encodeURIComponent(cookies[c]);
}
}
/// RANGE SELECTORS
// in rangeselect.js
/// API KEY
function insertAPIKeyFromCookie() {
element = document.getElementById("apikey")
if (element != null && element != undefined) {
getCookies();
key = cookies["apikey"];
if (key != null && key != undefined) {
element.value = key;
checkAPIkey();
}
}
}
window.addEventListener("load",insertAPIKeyFromCookie);
function saveAPIkey() {
key = document.getElementById("apikey").value
document.cookie = "apikey=" + encodeURIComponent(key)
key = APIkey();
setCookie("apikey",key,false);
}
function checkAPIkey() {
saveAPIkey()
url = "/api/test?key=" + document.getElementById("apikey").value
url = "/api/test?key=" + APIkey()
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && (this.status == 204 || this.status == 205)) {
document.getElementById("apikey").style.backgroundColor = "lawngreen"
apikeycorrect = true
saveAPIkey();
}
else {
document.getElementById("apikey").style.backgroundColor = "red"

View File

@ -1,28 +1,53 @@
function showRange(identifier,unit) {
// Make all modules disappear
modules = document.getElementsByClassName("stat_module_" + identifier)
modules = document.getElementsByClassName("stat_module_" + identifier);
for (var i=0;i<modules.length;i++) {
//modules[i].setAttribute("style","width:0px;overflow:hidden;")
// cheesy trick to make the allocated space always whatever the biggest module needs
// somehow that messes up pulse on the start page tho
modules[i].setAttribute("style","display:none;")
modules[i].setAttribute("style","display:none;");
}
// Make requested module appear
reactivate = document.getElementsByClassName(identifier + "_" + unit)
reactivate = document.getElementsByClassName(identifier + "_" + unit);
for (var i=0;i<reactivate.length;i++) {
reactivate[i].setAttribute("style","")
reactivate[i].setAttribute("style","");
}
// Set all selectors to unselected
selectors = document.getElementsByClassName("stat_selector_" + identifier)
selectors = document.getElementsByClassName("stat_selector_" + identifier);
for (var i=0;i<selectors.length;i++) {
selectors[i].setAttribute("style","")
selectors[i].setAttribute("style","");
}
// Set the active selector to selected
reactivate = document.getElementsByClassName("selector_" + identifier + "_" + unit)
reactivate = document.getElementsByClassName("selector_" + identifier + "_" + unit);
for (var i=0;i<reactivate.length;i++) {
reactivate[i].setAttribute("style","opacity:0.5;")
reactivate[i].setAttribute("style","opacity:0.5;");
}
links = document.getElementsByClassName("stat_link_" + identifier);
for (let l of links) {
var a = l.href.split("=");
a.splice(-1);
a.push(unit);
l.href = a.join("=");
}
}
function showRangeManual(identifier,unit) {
showRange(identifier,unit);
setCookie("rangeselect_" + identifier,unit);
}
document.addEventListener('DOMContentLoaded',function() {
getCookies();
for (c in cookies) {
if (c.startsWith("rangeselect_")) {
showRange(c.slice(12),cookies[c]);
}
}
})

134
website/less/grisons.less Normal file
View File

@ -0,0 +1,134 @@
/**
COMMON STYLES FOR MALOJA, ALBULA AND POSSIBLY OTHERS
**/
@BASE_COLOR: #333337;
@BASE_COLOR_DARK: #0a0a0a;
@BASE_COLOR_LIGHT: #444447;
@TEXT_COLOR: beige;
@TEXT_COLOR_SELECTED: fadeout(@TEXT_COLOR,40%);
@TEXT_COLOR_SECONDARY: #bbb;
@TEXT_COLOR_TERTIARY: grey;
@FOCUS_COLOR: yellow;
@CONTROL_ELEMENT_BG_COLOR: rgba(0,255,255,0.1);
@CONTROL_ELEMENT_FG_COLOR: rgba(103,85,0,0.7);
@CONTROL_ELEMENT_FOCUS_COLOR: gold;
@BUTTON_BG_COLOR: @TEXT_COLOR;
@BUTTON_FOCUS_BG_COLOR: @FOCUS_COLOR;
@BUTTON_FG_COLOR: @BASE_COLOR;
@BUTTON_FOCUS_FG_COLOR: @BASE_COLOR;
//@import url('https://fonts.googleapis.com/css?family=Ubuntu');
body {
background-color: @BASE_COLOR;
color: @TEXT_COLOR;
font-family:"Ubuntu";
}
/* TOP INFO TABLE */
table.top_info td.image div {
margin-right:20px;
margin-bottom:20px;
background-size:cover;
background-position:center;
height:174px;
width:174px
}
table.top_info td.text {
vertical-align: top;
}
table.top_info td.text h1 {
display:inline;
padding-right:5px;
}
table.top_info td.text table.image_row td {
height:50px;
width:50px;
background-size:cover;
background-position:center;
background-repeat: no-repeat;
opacity:0.5;
filter: grayscale(80%);
}
table.top_info td.text table.image_row td:hover {
opacity:1;
filter: grayscale(0%);
}
/** SCROLLBAR **/
::-webkit-scrollbar {
width: 8px;
cursor: pointer;
}
::-webkit-scrollbar-track {
background: grey;
background-color: @CONTROL_ELEMENT_BG_COLOR;
}
::-webkit-scrollbar-thumb {
background-color: @CONTROL_ELEMENT_FG_COLOR;
}
::-webkit-scrollbar-thumb:hover {
background: @CONTROL_ELEMENT_FOCUS_COLOR;
}
[onclick]:hover, a:hover {
cursor: pointer;
}
/** HOVERABLE LOAD/PROGRESS BAR **/
div.grisons_bar {
background-color: @CONTROL_ELEMENT_BG_COLOR;
}
div.grisons_bar>div {
height:100%;
background-color: @CONTROL_ELEMENT_FG_COLOR;
}
div.grisons_bar:hover>div {
background-color: @CONTROL_ELEMENT_FOCUS_COLOR;
}
/** LINKS **/
a {
color:inherit;
text-decoration:none;
}
// for links in running text
a.textlink {
color:@FOCUS_COLOR;
}
a.hidelink:hover {
text-decoration:none;
}
a:hover {
text-decoration:underline;
}

View File

@ -0,0 +1,45 @@
/* cyrillic-ext */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKcg72j00.woff2) format('woff2');
}
/* cyrillic */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKew72j00.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKcw72j00.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKfA72j00.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* latin-ext */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKcQ72j00.woff2) format('woff2');
}
/* latin */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu Regular'), local('Ubuntu-Regular'), url(https://fonts.gstatic.com/s/ubuntu/v14/4iCs6KVjbNBYlgoKfw72.woff2) format('woff2');
}

View File

@ -1,9 +1,6 @@
@import url('https://fonts.googleapis.com/css?family=Ubuntu');
@import "website/less/grisons";
body {
background-color:#333337;
color:beige;
font-family:"Ubuntu";
padding:15px;
padding-bottom:35px;
/**
@ -15,21 +12,6 @@ body {
*/
}
a {
color:inherit;
text-decoration:none;
}
a.textlink {
color:gold;
}
a.hidelink:hover {
text-decoration:none;
}
a:hover {
text-decoration:underline;
}
input[type="date"] {
@ -42,6 +24,9 @@ input[type="date"] {
}
/**
Header (unused)
**/
@ -76,7 +61,7 @@ div.footer {
position:fixed;
height:20px;
/**width:100%;**/
background-color:rgba(10,10,10,0.9);
background-color:@BASE_COLOR_DARK;
bottom:0px;
left:0px;
right:0px;
@ -185,7 +170,7 @@ div.searchresults table.searchresults_tracks td span:nth-child(1) {
position:fixed;
/*height:30px;*/
/**width:100%;**/
background-color:rgba(10,10,10,0.9);
background-color:@BASE_COLOR_DARK;
bottom:0px;
left:0px;
right:0px;
@ -218,36 +203,6 @@ div.searchresults table.searchresults_tracks td span:nth-child(1) {
/*
**
**
** TOP INFO TABLE
**
**
*/
table.top_info td.image {
padding:20px;
padding-left:0px;
padding-top:0px;
}
table.top_info td.image div {
background-size:cover;
background-position:center;
height:174px;
width:174px
}
table.top_info td.text {
vertical-align: top;
padding-left: 30px;
}
table.top_info td.text h1 {
display:inline;
padding-right:5px;
}
p.desc a {
padding-left:20px;
@ -257,7 +212,10 @@ p.desc a {
background-image:url("https://www.last.fm/static/images/lastfm_avatar_twitter.66cd2c48ce03.png");
}
table.top_info + .stat_module_topartists table,
table.top_info + .stat_module_toptracks table {
margin:15px 0;
}
/*
**
@ -268,17 +226,22 @@ p.desc a {
*/
.paginate {
text-align: center;
padding:30px;
}
.stats {
color:grey;
color:@TEXT_COLOR_TERTIARY;
}
.rank {
text-align:right;
color:grey;
color:@TEXT_COLOR_TERTIARY;
}
.extra {
color:gray; /*sue me*/
color:@TEXT_COLOR_TERTIARY;
font-size:80%;
}
@ -292,14 +255,14 @@ input#apikey {
input.simpleinput {
font-family:'Ubuntu';
color:beige;
color:@TEXT_COLOR;
outline:none;
border-top: 0px solid;
border-left: 0px solid;
border-right: 0px solid;
padding:2px;
background-color:inherit;
border-bottom: 1px solid beige;
border-bottom: 1px solid @TEXT_COLOR;
}
@ -379,6 +342,15 @@ img.certrecord {
height:30px;
vertical-align: text-bottom;
}
img.certrecord_small {
height:20px;
vertical-align: text-bottom;
}
img.star {
height:20px;
vertical-align: text-bottom;
}
/*
@ -430,7 +402,7 @@ table.list tr:hover {
table.list td.time {
width:11%;
color:gray;
color:@TEXT_COLOR_TERTIARY;
}
@ -476,10 +448,13 @@ table.list td.artists,td.artist,td.title,td.track {
}
table.list td.track span.artist_in_trackcolumn {
color:#bbb;
color:@TEXT_COLOR_SECONDARY;
}
table.list td.track a.trackProviderSearch {
margin-right: 5px;
padding: 0 10px;
}
@ -519,23 +494,23 @@ table.list td.amount {
}
table.list td.bar {
width:500px;
background-color:#333337;
background-color:@BASE_COLOR;
/* Remove 5er separators for bars */
/*border-color:rgba(0,0,0,0)!important;*/
}
table.list td.bar div {
background-color:beige;
background-color:@TEXT_COLOR;
height:20px; /* can only do this absolute apparently */
position:relative;
}
table.list tr:hover td.bar div {
background-color:yellow;
background-color:@FOCUS_COLOR;
cursor:pointer;
}
table.list td.chart {
width:500px;
background-color:#333337;
background-color:@BASE_COLOR;
/* Remove 5er separators for bars */
/*border-color:rgba(0,0,0,0)!important;*/
}
@ -581,8 +556,14 @@ table.list tr td.button {
table.list td.button div {
background-color:yellow;
color:#333337;
background-color:@BUTTON_BG_COLOR;
color:@BUTTON_FG_COLOR;
padding:3px;
border-radius:4px;
}
table.list td.button div:hover {
background-color:@BUTTON_FOCUS_BG_COLOR;
color:@BUTTON_FOCUS_FG_COLOR;
padding:3px;
border-radius:4px;
}

View File

@ -154,7 +154,7 @@
<body onload="insertAPIKeyFromCookie()">
<body>
<table class="top_info">
<tr>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
website/media/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
website/media/star_alt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -9,10 +9,10 @@ def instructions(keys):
from htmlmodules import module_performance, module_filterselection
from malojatime import range_desc, delimit_desc
filterkeys, timekeys, delimitkeys, _ = uri_to_internal(keys)
filterkeys, timekeys, delimitkeys, paginatekeys = uri_to_internal(keys)
#equivalent pulse chart
pulselink_keys = internal_to_uri({**filterkeys,**timekeys,**delimitkeys})
pulselink_keys = internal_to_uri({**filterkeys,**timekeys,**delimitkeys,**paginatekeys})
pulselink = "/pulse?" + compose_querystring(pulselink_keys)
pulselink = "<a href=\"" + pulselink + "\"><span>View Pulse</span></a>"
@ -54,7 +54,7 @@ def instructions(keys):
html_performance = module_performance(**filterkeys,**timekeys,**delimitkeys)
html_performance = module_performance(**filterkeys,**timekeys,**delimitkeys,**paginatekeys)
replace = {
"KEY_PULSE_LINK":pulselink,

46
website/proxy.html Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Maloja - Proxyscrobble</title>
<script src="javascript/cookies.js"></script>
<script>
window.addEventListener("load",function(){
try {
document.getElementById("lastfmlink").href += window.location.href;
}
catch (e) {
}
});
</script>
</head>
<body>
<table class="top_info">
<tr>
<td class="image">
<div style="background-image:url('/favicon.png')"></div>
</td>
<td class="text">
<h1>Proxyscrobble</h1>
<p class="desc">Duplicate your scrobbles to another service.
Your API key is required to make any changes to the server: <input id='apikey' onchange='checkAPIkey()' style='width:300px;'/></p>
</td>
</tr>
</table>
<table class="list">
<tr>
<td>Last.fm</td>
KEY_STATUS_LASTFM
</tr>
</table>
</body>
</html>

53
website/proxy.py Normal file
View File

@ -0,0 +1,53 @@
from doreah.settings import get_settings, update_settings
import urllib.request
import hashlib
import xml.etree.ElementTree as ET
from bottle import redirect, request
from database import checkAPIkey
from external import lfmbuild
def instructions(keys):
authenticated = False
if "Cookie" in request.headers:
cookies = request.headers["Cookie"].split(";")
for c in cookies:
if c.strip().startswith("apikey="):
authenticated = checkAPIkey(c.strip()[7:])
if "token" in keys and authenticated:
token = keys.get("token")
parameters = {
"method":"auth.getSession",
"token":token,
"api_key":get_settings("LASTFM_API_KEY")
}
response = urllib.request.urlopen("http://ws.audioscrobbler.com/2.0/?" + lfmbuild(parameters))
xml = response.read()
data = ET.fromstring(xml)
if data.attrib.get("status") == "ok":
username = data.find("session").find("name").text
sessionkey = data.find("session").find("key").text
update_settings("settings/settings.ini",{"LASTFM_API_SK":sessionkey,"LASTFM_USERNAME":username},create_new=True)
return "/proxy"
else:
key,secret,sessionkey,name = get_settings("LASTFM_API_KEY","LASTFM_API_SECRET","LASTFM_API_SK","LASTFM_USERNAME")
if key is None:
lastfm = "<td>No Last.fm key provided</td>"
elif secret is None:
lastfm = "<td>No Last.fm secret provided</td>"
elif sessionkey is None and authenticated:
url = "http://www.last.fm/api/auth/?api_key=" + key + "&cb="
lastfm = "<td class='button'><a id='lastfmlink' href='" + url + "'><div>Connect</div></a></td>"
elif sessionkey is None:
lastfm = "<td>Not active</td>"
else:
lastfm = "<td>Account: " + name + "</td>"
return {"KEY_STATUS_LASTFM":lastfm},[]

View File

@ -9,11 +9,11 @@ def instructions(keys):
from htmlmodules import module_pulse, module_filterselection
from malojatime import range_desc, delimit_desc
filterkeys, timekeys, delimitkeys, _ = uri_to_internal(keys)
filterkeys, timekeys, delimitkeys, paginatekeys = uri_to_internal(keys)
#equivalent performance chart if we're not looking at the overall pulse
if len(filterkeys) != 0:
performancelink_keys = internal_to_uri({**filterkeys,**timekeys,**delimitkeys})
performancelink_keys = internal_to_uri({**filterkeys,**timekeys,**delimitkeys,**paginatekeys})
performancelink = "/performance?" + compose_querystring(performancelink_keys)
performancelink = "<a href=\"" + performancelink + "\"><span>View Rankings</span></a>"
@ -57,7 +57,7 @@ def instructions(keys):
html_pulse = module_pulse(**filterkeys,**timekeys,**delimitkeys)
html_pulse = module_pulse(**filterkeys,**timekeys,**delimitkeys,**paginatekeys)
replace = {
"KEY_RANKINGS_LINK":performancelink,

View File

@ -58,7 +58,7 @@
</head>
<body onload="replace();insertAPIKeyFromCookie()">
<body onload="replace()">
<table class="top_info">
<tr>
<td class="image">

View File

@ -4,10 +4,12 @@
<head>
<meta charset="UTF-8" />
<title>Maloja</title>
<script src="javascript/rangeselect.js"></script>
<script>document.addEventListener('DOMContentLoaded',function() {
KEY_JS_INIT_RANGES
})</script>
<script src="javascript/cookies.js"></script>
<script src="javascript/rangeselect.js"></script>
</head>
@ -18,15 +20,15 @@
</div>-->
<h1><a href="/charts_artists?max=50">Top Artists</a></h1>
<h1><a class="stat_link_topartists" href="/charts_artists?in=alltime">Top Artists</a></h1>
<!--All Time | This Year | This Month | This Week-->
<span onclick="showRange('topartists','week')" class="stat_selector_topartists selector_topartists_week">This Week</span>
| <span onclick="showRange('topartists','month')" class="stat_selector_topartists selector_topartists_month">This Month</span>
| <span onclick="showRange('topartists','year')" class="stat_selector_topartists selector_topartists_year">This Year</span>
| <span onclick="showRange('topartists','alltime')" class="stat_selector_topartists selector_topartists_alltime" style="opacity:0.5;">All Time</span>
<span onclick="showRangeManual('topartists','week')" class="stat_selector_topartists selector_topartists_week">This Week</span>
| <span onclick="showRangeManual('topartists','month')" class="stat_selector_topartists selector_topartists_month">This Month</span>
| <span onclick="showRangeManual('topartists','year')" class="stat_selector_topartists selector_topartists_year">This Year</span>
| <span onclick="showRangeManual('topartists','alltime')" class="stat_selector_topartists selector_topartists_alltime" style="opacity:0.5;">All Time</span>
<br/><br/>
@ -38,13 +40,13 @@
<h1><a href="/charts_tracks?max=50">Top Tracks</a></h1>
<h1><a class="stat_link_toptracks" href="/charts_tracks?in=alltime">Top Tracks</a></h1>
<span onclick="showRange('toptracks','week')" class="stat_selector_toptracks selector_toptracks_week">This Week</span>
| <span onclick="showRange('toptracks','month')" class="stat_selector_toptracks selector_toptracks_month">This Month</span>
| <span onclick="showRange('toptracks','year')" class="stat_selector_toptracks selector_toptracks_year">This Year</span>
| <span onclick="showRange('toptracks','alltime')" class="stat_selector_toptracks selector_toptracks_alltime" style="opacity:0.5;">All Time</span>
| <span onclick="showRangeManual('toptracks','month')" class="stat_selector_toptracks selector_toptracks_month">This Month</span>
| <span onclick="showRangeManual('toptracks','year')" class="stat_selector_toptracks selector_toptracks_year">This Year</span>
| <span onclick="showRangeManual('toptracks','alltime')" class="stat_selector_toptracks selector_toptracks_alltime" style="opacity:0.5;">All Time</span>
<br/><br/>
@ -59,7 +61,7 @@
<div class="sidelist">
<h1><a href="/scrobbles?max=100">Last Scrobbles</a></h1>
<h1><a href="/scrobbles">Last Scrobbles</a></h1>
<span class="stats">Today</span> KEY_SCROBBLE_NUM_TODAY
<span class="stats">This Week</span> KEY_SCROBBLE_NUM_WEEK
<span class="stats">This Month</span> KEY_SCROBBLE_NUM_MONTH
@ -73,7 +75,7 @@
<br/>
<h1><a href="/pulse?step=month&trail=1">Pulse</a></h1>
<h1><a class="stat_link_pulse" href="/pulse?trail=1&step=month">Pulse</a></h1>
<!--
<a href="/pulse?step=day&trail=1">Days</a>
<a href="/pulse?step=week&trail=1">Weeks</a>
@ -81,10 +83,10 @@
<a href="/pulse?step=year&trail=1">Years</a>
-->
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span>
<span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<!--
### this is for extra views of the current canonical week / month / year
<br/>
@ -94,10 +96,10 @@
-->
<br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PULSE_WEEKS</span>
<span class="stat_module_pulse pulse_month">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_day" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_year" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEKS</span>
<!--
<span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEK</span>
<span class="stat_module_pulse pulse_month" style="display:none;">KEY_PULSE_MONTH</span>

View File

@ -12,7 +12,7 @@ def instructions(keys):
# commands to execute on load for default ranges
js_command = "showRange('topartists','" + get_settings("DEFAULT_RANGE_CHARTS_ARTISTS") + "');"
js_command += "showRange('toptracks','" + get_settings("DEFAULT_RANGE_CHARTS_TRACKS") + "');"
js_command += "showRange('pulse','" + get_settings("DEFAULT_RANGE_PULSE") + "');"
js_command += "showRange('pulse','" + get_settings("DEFAULT_STEP_PULSE") + "');"
clock()

View File

@ -4,7 +4,8 @@
<head>
<meta charset="UTF-8" />
<title>Maloja - KEY_TRACKTITLE</title>
<script src="javascript/rangeselect.js" async></script>
<script src="javascript/cookies.js" ></script>
<script src="javascript/rangeselect.js"></script>
</head>
<body>
@ -15,12 +16,12 @@
</td>
<td class="text">
<span>KEY_ARTISTS</span><br/>
<h1>KEY_TRACKTITLE</h1> KEY_CERTS <span class="rank"><a href="/charts_tracks?max=100">KEY_POSITION</a></span>
<h1>KEY_TRACKTITLE</h1> KEY_CERTS <span class="rank"><a href="/charts_tracks">KEY_POSITION</a></span>
<p class="stats"><a href="/scrobbles?KEY_SCROBBLELINK">KEY_SCROBBLES Scrobbles</a></p>
<p class="desc"></p>
<span>KEY_MEDALS</span>
<span>KEY_MEDALS</span> <span>KEY_TOPWEEKS</span>
</td>
</tr>
</table>
@ -29,32 +30,32 @@
<table class="twopart">
<tr>
<td>
<h2><a href='/pulse?KEY_SCROBBLELINK&step=year&trail=1'>Pulse</a></h2>
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span>
<h2><a class="stat_link_pulse" href='/pulse?KEY_SCROBBLELINK&trail=1&step=month'>Pulse</a></h2>
<span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PULSE_WEEKS</span>
<span class="stat_module_pulse pulse_month">KEY_PULSE_MONTHS</span>
<span class="stat_module_pulse pulse_day" style="display:none;">KEY_PULSE_DAYS</span>
<span class="stat_module_pulse pulse_year" style="display:none;">KEY_PULSE_YEARS</span>
<span class="stat_module_pulse pulse_week" style="display:none;">KEY_PULSE_WEEKS</span>
</td>
<td>
<h2><a href='/performance?KEY_SCROBBLELINK&step=year&trail=1'>Performance</a></h2>
<span onclick="showRange('pulse','days')" class="stat_selector_pulse selector_pulse_days">7 days</span>
| <span onclick="showRange('pulse','weeks')" class="stat_selector_pulse selector_pulse_weeks">12 weeks</span>
| <span onclick="showRange('pulse','months')" class="stat_selector_pulse selector_pulse_months" style="opacity:0.5;">12 months</span>
| <span onclick="showRange('pulse','years')" class="stat_selector_pulse selector_pulse_years">10 years</span>
<h2><a class="stat_link_pulse" href='/performance?KEY_SCROBBLELINK&trail=1&step=month'>Performance</a></h2>
<span onclick="showRangeManual('pulse','day')" class="stat_selector_pulse selector_pulse_day">7 days</span>
| <span onclick="showRangeManual('pulse','week')" class="stat_selector_pulse selector_pulse_week">12 weeks</span>
| <span onclick="showRangeManual('pulse','month')" class="stat_selector_pulse selector_pulse_month" style="opacity:0.5;">12 months</span>
| <span onclick="showRangeManual('pulse','year')" class="stat_selector_pulse selector_pulse_year">10 years</span>
<br/><br/>
<span class="stat_module_pulse pulse_months">KEY_PERFORMANCE_MONTHS</span>
<span class="stat_module_pulse pulse_days" style="display:none;">KEY_PERFORMANCE_DAYS</span>
<span class="stat_module_pulse pulse_years" style="display:none;">KEY_PERFORMANCE_YEARS</span>
<span class="stat_module_pulse pulse_weeks" style="display:none;">KEY_PERFORMANCE_WEEKS</span>
<span class="stat_module_pulse pulse_month">KEY_PERFORMANCE_MONTHS</span>
<span class="stat_module_pulse pulse_day" style="display:none;">KEY_PERFORMANCE_DAYS</span>
<span class="stat_module_pulse pulse_year" style="display:none;">KEY_PERFORMANCE_YEARS</span>
<span class="stat_module_pulse pulse_week" style="display:none;">KEY_PERFORMANCE_WEEKS</span>
</td>
</tr>
</table>

View File

@ -29,13 +29,19 @@ def instructions(keys):
if "medals" in data and data["medals"] is not None:
if "gold" in data["medals"]:
for y in data["medals"]["gold"]:
html_medals += "<a title='Best Track in " + str(y) + "' class='hidelink medal shiny gold' href='/charts_tracks?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_medals += "<a title='Best Track in " + str(y) + "' class='hidelink medal shiny gold' href='/charts_tracks?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
if "silver" in data["medals"]:
for y in data["medals"]["silver"]:
html_medals += "<a title='Second Best Track in " + str(y) + "' class='hidelink medal shiny silver' href='/charts_tracks?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_medals += "<a title='Second Best Track in " + str(y) + "' class='hidelink medal shiny silver' href='/charts_tracks?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
if "bronze" in data["medals"]:
for y in data["medals"]["bronze"]:
html_medals += "<a title='Third Best Track in " + str(y) + "' class='hidelink medal shiny bronze' href='/charts_tracks?max=50&in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_medals += "<a title='Third Best Track in " + str(y) + "' class='hidelink medal shiny bronze' href='/charts_tracks?in=" + str(y) + "'><span>" + str(y) + "</span></a>"
html_topweeks = ""
if data.get("topweeks") not in [0,None]:
link = "/performance?" + compose_querystring(keys) + "&trail=1&step=week"
title = str(data["topweeks"]) + " weeks on #1"
html_topweeks = "<a title='" + title + "' href='" + link + "'><img class='star' src='/media/star.png' />" + str(data["topweeks"]) + "</a>"
@ -65,6 +71,7 @@ def instructions(keys):
"KEY_SCROBBLELINK":compose_querystring(keys),
"KEY_MEDALS":html_medals,
"KEY_CERTS":html_cert,
"KEY_TOPWEEKS":html_topweeks,
"KEY_SCROBBLELIST":html_scrobbles,
# pulse
"KEY_PULSE_MONTHS":html_pulse_months,