1
0
mirror of https://github.com/krateng/maloja.git synced 2023-08-10 21:12:55 +03:00

Merged master changes

This commit is contained in:
Krateng 2019-06-13 12:22:02 +02:00
commit 95b4fc7426
23 changed files with 201 additions and 182 deletions

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
# generic temporary / dev files
*.pyc
*.sh
*.txt
*.note
*.xcf
nohup.out
/.dev

View File

@ -19,7 +19,9 @@ Also neat: You can use your **custom artist or track images**.
* [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.7.2)
* [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)
* 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
@ -28,13 +30,15 @@ Also neat: You can use your **custom artist or track images**.
./maloja install
2) Start the server with
2) Install required packages with
pip3 install -r requirements.txt
3) Start the server with
maloja start
If you're missing packages, the console output will tell you so. Install them.
2) (Recommended) Put your server behind a reverse proxy for SSL encryption.
4) (Recommended) Put your server behind a reverse proxy for SSL encryption.
## How to use
@ -58,13 +62,15 @@ If you didn't install Maloja from the package (and therefore don't have it in `/
3) Various folders have `.info` files with more information on how to use their associated features.
4) If you'd like to implement anything on top of Maloja, visit `/api_explorer`.
## How to scrobble
### 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 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`.
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.
### Standard-compliant API

View File

@ -43,17 +43,12 @@ def handler(apiname,version):
return cls
return deco
def handle(path,keys,headers,auth):
def handle(path,keys):
print("API request: " + str(path))
print("Keys:")
for k in keys:
print("\t" + str(k) + ": " + str(keys.get(k)))
print("Headers:")
for h in headers:
print("\t" + str(h) + ": " + str(headers.get(h)))
print("Auth: " + str(auth))
print("\t",k,":",keys.get(k))
keys = {**keys,**headers}
if len(path)>1 and (path[0],path[1]) in handlers:
handler = handlers[(path[0],path[1])]
@ -179,7 +174,7 @@ class LBrnz1(APIHandler):
}
self.errors = {
BadAuthException:(401,{"code":401,"error":"You need to provide an Authorization header."}),
InvalidAuthException:(401,{"code":401,"error":"Bad Auth"}),
InvalidAuthException:(401,{"code":401,"error":"Incorrect Authorization"}),
InvalidMethodException:(200,{"code":200,"error":"Invalid Method"}),
MalformedJSONException:(400,{"code":400,"error":"Invalid JSON document submitted."}),
ScrobblingException:(500,{"code":500,"error":"Unspecified server error."})
@ -191,7 +186,7 @@ class LBrnz1(APIHandler):
def submit(self,pathnodes,keys):
try:
token = keys.get("Authorization").replace("token ","").strip()
token = keys.get("Authorization").replace("token ","").replace("Token ","").strip()
except:
raise BadAuthException()

View File

@ -1,19 +1,23 @@
# server
from bottle import request, response, FormsDict
# rest of the project
from cleanup import *
from utilities import *
from malojatime import *
from cleanup import CleanerAgent, CollectorAgent
import utilities
from malojatime import register_scrobbletime, time_stamps, ranges
from urihandler import uri_to_internal, internal_to_uri, compose_querystring
import compliant_api
# doreah toolkit
from doreah.logging import log
from doreah import tsv
from doreah import settings
from doreah.caching import Cache, DeepCache
try:
from doreah.persistence import DiskDict
except: pass
import doreah
# nimrodel API
from nimrodel import EAPI as API
from nimrodel import Multi
# technical
import os
import datetime
@ -27,7 +31,6 @@ import urllib
dblock = Lock() #global database lock
SCROBBLES = [] # Format: tuple(track_ref,timestamp,saved)
@ -182,54 +185,13 @@ def getTrackID(artists,title):
########
# silly patch to get old syntax working without dbserver
# function to register all the functions to the real server
def register_subroutes(server,path):
for subpath in dbserver.handlers_get:
func = dbserver.handlers_get[subpath]
decorator = server.get(path + subpath)
decorator(func)
for subpath in dbserver.handlers_post:
func = dbserver.handlers_post[subpath]
decorator = server.post(path + subpath)
decorator(func)
dbserver = API(delay=True,path="api")
# fake server
class FakeBottle:
def __init__(self):
self.handlers_get = {}
self.handlers_post = {}
# these functions pretend that they're the bottle decorators, but only write
# down which functions asked for them so they can later report their names
# to the real bottle server
def get(self,path):
def register(func):
self.handlers_get[path] = func
return func
return register
def post(self,path):
def register(func):
self.handlers_post[path] = func
return func
return register
def route(self,path):
return self.get(path)
dbserver = FakeBottle()
@dbserver.route("/test")
def test_server():
apikey = request.query.get("key")
@dbserver.get("test")
def test_server(key=None):
response.set_header("Access-Control-Allow-Origin","*")
if apikey is not None and not (checkAPIkey(apikey)):
if key is not None and not (checkAPIkey(key)):
response.status = 403
return "Wrong API key"
@ -247,9 +209,8 @@ def test_server():
## All database functions are separated - the external wrapper only reads the request keys, converts them into lists and renames them where necessary, and puts the end result in a dict if not already so it can be returned as json
@dbserver.route("/scrobbles")
def get_scrobbles_external():
keys = FormsDict.decode(request.query)
@dbserver.get("scrobbles")
def get_scrobbles_external(**keys):
k_filter, k_time, _, k_amount = uri_to_internal(keys)
ckeys = {**k_filter, **k_time, **k_amount}
@ -277,9 +238,8 @@ def get_scrobbles(**keys):
# return {"scrobbles":len(SCROBBLES),"tracks":len(TRACKS),"artists":len(ARTISTS)}
@dbserver.route("/numscrobbles")
def get_scrobbles_num_external():
keys = FormsDict.decode(request.query)
@dbserver.get("numscrobbles")
def get_scrobbles_num_external(**keys):
k_filter, k_time, _, k_amount = uri_to_internal(keys)
ckeys = {**k_filter, **k_time, **k_amount}
@ -342,9 +302,8 @@ def get_scrobbles_num(**keys):
@dbserver.route("/tracks")
def get_tracks_external():
keys = FormsDict.decode(request.query)
@dbserver.get("tracks")
def get_tracks_external(**keys):
k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True)
ckeys = {**k_filter}
@ -366,7 +325,7 @@ def get_tracks(artist=None):
#ls = [t for t in tracklist if (artist in t["artists"]) or (artist==None)]
@dbserver.route("/artists")
@dbserver.get("artists")
def get_artists_external():
result = get_artists()
return {"list":result}
@ -380,9 +339,8 @@ def get_artists():
@dbserver.route("/charts/artists")
def get_charts_artists_external():
keys = FormsDict.decode(request.query)
@dbserver.get("charts/artists")
def get_charts_artists_external(**keys):
_, k_time, _, _ = uri_to_internal(keys)
ckeys = {**k_time}
@ -397,9 +355,8 @@ def get_charts_artists(**keys):
@dbserver.route("/charts/tracks")
def get_charts_tracks_external():
keys = FormsDict.decode(request.query)
@dbserver.get("charts/tracks")
def get_charts_tracks_external(**keys):
k_filter, k_time, _, _ = uri_to_internal(keys,forceArtist=True)
ckeys = {**k_filter, **k_time}
@ -417,9 +374,8 @@ def get_charts_tracks(**keys):
@dbserver.route("/pulse")
def get_pulse_external():
keys = FormsDict.decode(request.query)
@dbserver.get("pulse")
def get_pulse_external(**keys):
k_filter, k_time, k_internal, k_amount = uri_to_internal(keys)
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
@ -440,9 +396,8 @@ def get_pulse(**keys):
@dbserver.route("/performance")
def get_performance_external():
keys = FormsDict.decode(request.query)
@dbserver.get("performance")
def get_performance_external(**keys):
k_filter, k_time, k_internal, k_amount = uri_to_internal(keys)
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
@ -480,10 +435,8 @@ def get_performance(**keys):
@dbserver.route("/top/artists")
def get_top_artists_external():
keys = FormsDict.decode(request.query)
@dbserver.get("top/artists")
def get_top_artists_external(**keys):
_, k_time, k_internal, _ = uri_to_internal(keys)
ckeys = {**k_time, **k_internal}
@ -513,9 +466,8 @@ def get_top_artists(**keys):
@dbserver.route("/top/tracks")
def get_top_tracks_external():
keys = FormsDict.decode(request.query)
@dbserver.get("top/tracks")
def get_top_tracks_external(**keys):
_, k_time, k_internal, _ = uri_to_internal(keys)
ckeys = {**k_time, **k_internal}
@ -548,9 +500,8 @@ def get_top_tracks(**keys):
@dbserver.route("/artistinfo")
def artistInfo_external():
keys = FormsDict.decode(request.query)
@dbserver.get("artistinfo")
def artistInfo_external(**keys):
k_filter, _, _, _ = uri_to_internal(keys,forceArtist=True)
ckeys = {**k_filter}
@ -560,14 +511,16 @@ def artistInfo_external():
def artistInfo(artist):
charts = db_aggregate(by="ARTIST")
scrobbles = len(db_query(artists=[artist])) #we cant take the scrobble number from the charts because that includes all countas scrobbles
scrobbles = len(db_query(artists=[artist]))
#we cant take the scrobble number from the charts because that includes all countas scrobbles
try:
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)}
except:
# if the artist isnt in the charts, they are not being credited and we need to show information about the credited one
# if the artist isnt in the charts, they are not being credited and we
# need to show information about the credited one
artist = coa.getCredited(artist)
c = [e for e in charts if e["artist"] == artist][0]
position = c["rank"]
@ -577,23 +530,37 @@ def artistInfo(artist):
@dbserver.route("/trackinfo")
def trackInfo_external():
keys = FormsDict.decode(request.query)
@dbserver.get("trackinfo")
def trackInfo_external(artist:Multi[str],**keys):
# transform into a multidict so we can use our nomral uri_to_internal function
keys = FormsDict(keys)
for a in artist:
keys.append("artist",a)
k_filter, _, _, _ = uri_to_internal(keys,forceTrack=True)
ckeys = {**k_filter}
results = trackInfo(**ckeys)
return results
def trackInfo(artists,title):
def trackInfo(track):
charts = db_aggregate(by="TRACK")
#scrobbles = len(db_query(artists=artists,title=title)) #chart entry of track always has right scrobble number, no countas rules here
c = [e for e in charts if set(e["track"]["artists"]) == set(artists) and e["track"]["title"] == title][0]
#c = [e for e in charts if set(e["track"]["artists"]) == set(artists) and e["track"]["title"] == title][0]
c = [e for e in charts if e["track"] == track][0]
scrobbles = c["scrobbles"]
position = c["rank"]
cert = None
threshold_gold, threshold_platinum, threshold_diamond = settings.get_settings("SCROBBLES_GOLD","SCROBBLES_PLATINUM","SCROBBLES_DIAMOND")
if scrobbles >= threshold_diamond: cert = "diamond"
elif scrobbles >= threshold_platinum: cert = "platinum"
elif scrobbles >= threshold_gold: cert = "gold"
return {"scrobbles":scrobbles,"position":position,"medals":MEDALS_TRACKS.get((frozenset(artists),title))}
return {
"scrobbles":scrobbles,
"position":position,
"medals":MEDALS_TRACKS.get((frozenset(track["artists"]),track["title"])),
"certification":cert
}
@ -601,9 +568,8 @@ def trackInfo(artists,title):
@dbserver.get("/newscrobble")
def pseudo_post_scrobble():
keys = FormsDict.decode(request.query) # The Dal★Shabet handler
@dbserver.get("newscrobble")
def pseudo_post_scrobble(**keys):
artists = keys.get("artist")
title = keys.get("title")
apikey = keys.get("key")
@ -626,9 +592,8 @@ def pseudo_post_scrobble():
return {"status":"success","track":trackdict}
@dbserver.post("/newscrobble")
def post_scrobble():
keys = FormsDict.decode(request.forms) # The Dal★Shabet handler
@dbserver.post("newscrobble")
def post_scrobble(**keys):
artists = keys.get("artist")
title = keys.get("title")
apikey = keys.get("key")
@ -659,28 +624,26 @@ def post_scrobble():
# standard-compliant scrobbling methods
@dbserver.post("/s/<path:path>")
@dbserver.get("/s/<path:path>")
def sapi(path):
path = path.split("/")
@dbserver.post("s/{path}",pass_headers=True)
@dbserver.get("s/{path}",pass_headers=True)
def sapi(path:Multi,**keys):
"""Scrobbles according to a standardized protocol.
:param string path: Path according to the scrobble protocol
:param string keys: Query keys according to the scrobble protocol
"""
path = list(filter(None,path))
headers = request.headers
if request.get_header("Content-Type") is not None and "application/json" in request.get_header("Content-Type"):
keys = request.json
else:
keys = FormsDict.decode(request.params)
auth = request.auth
return compliant_api.handle(path,keys,headers,auth)
return compliant_api.handle(path,keys)
@dbserver.route("/sync")
@dbserver.get("sync")
def abouttoshutdown():
sync()
#sys.exit()
@dbserver.post("/newrule")
@dbserver.post("newrule")
def newrule():
keys = FormsDict.decode(request.forms)
apikey = keys.pop("key",None)
@ -691,7 +654,7 @@ def newrule():
db_rulestate = False
@dbserver.route("/issues")
@dbserver.get("issues")
def issues_external(): #probably not even needed
return issues()
@ -787,7 +750,7 @@ def issues():
return {"duplicates":duplicates,"combined":combined,"newartists":newartists,"inconsistent":inconsistent}
@dbserver.post("/importrules")
@dbserver.post("importrules")
def import_rulemodule():
keys = FormsDict.decode(request.forms)
apikey = keys.pop("key",None)
@ -807,7 +770,7 @@ def import_rulemodule():
@dbserver.post("/rebuild")
@dbserver.post("rebuild")
def rebuild():
keys = FormsDict.decode(request.forms)
@ -827,9 +790,8 @@ def rebuild():
@dbserver.get("/search")
def search():
keys = FormsDict.decode(request.query)
@dbserver.get("search")
def search(**keys):
query = keys.get("query")
max_ = keys.get("max")
if max_ is not None: max_ = int(max_)
@ -923,10 +885,10 @@ def build_db():
# coa.updateIDs(ARTISTS)
#start regular tasks
update_medals()
utilities.update_medals()
global db_rulestate
db_rulestate = consistentRulestate("scrobbles",cla.checksums)
db_rulestate = utilities.consistentRulestate("scrobbles",cla.checksums)
log("Database fully built!")
@ -959,7 +921,7 @@ def sync():
for e in entries:
tsv.add_entries("scrobbles/" + e + ".tsv",entries[e],comments=False)
#addEntries("scrobbles/" + e + ".tsv",entries[e],escape=False)
combineChecksums("scrobbles/" + e + ".tsv",cla.checksums)
utilities.combineChecksums("scrobbles/" + e + ".tsv",cla.checksums)
global lastsync
@ -989,7 +951,7 @@ cacheday = (0,0,0)
def db_query(**kwargs):
check_cache_age()
global cache_query, cache_query_permanent
key = serialize(kwargs)
key = utilities.serialize(kwargs)
if "timerange" in kwargs and not kwargs["timerange"].active():
if key in cache_query_permanent:
#print("Hit")
@ -1014,7 +976,7 @@ else:
def db_aggregate(**kwargs):
check_cache_age()
global cache_aggregate, cache_aggregate_permanent
key = serialize(kwargs)
key = utilities.serialize(kwargs)
if "timerange" in kwargs and not kwargs["timerange"].active():
if key in cache_aggregate_permanent: return copy.copy(cache_aggregate_permanent.get(key))
result = db_aggregate_full(**kwargs)

View File

@ -5,53 +5,60 @@ from doreah.settings import get_settings
from doreah.logging import log
apis_artists = [
{
apis_artists = []
if get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"]:
apis_artists.append({
"name":"LastFM + Fanart.tv",
"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"],
#"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"],
"steps":[
("get","http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artiststring}&api_key=" + get_settings("LASTFM_API_KEY") + "&format=json"),
("get","http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artiststring}&api_key=" + str(get_settings("LASTFM_API_KEY")) + "&format=json"),
("parse",["artist","mbid"]),
("get","http://webservice.fanart.tv/v3/music/{var}?api_key=" + get_settings("FANARTTV_API_KEY")),
("get","http://webservice.fanart.tv/v3/music/{var}?api_key=" + str(get_settings("FANARTTV_API_KEY"))),
("parse",["artistthumb",0,"url"])
]
},
{
})
if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"]:
apis_artists.append({
"name":"Spotify",
"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
#"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
"steps":[
("post","https://accounts.spotify.com/api/token",{"Authorization":"Basic " + base64.b64encode(bytes(get_settings("SPOTIFY_API_ID") + ":" + get_settings("SPOTIFY_API_SECRET"),encoding="utf-8")).decode("utf-8")},{"grant_type":"client_credentials"}),
("parse",["access_token"]),
("get","https://api.spotify.com/v1/search?q={artiststring}&type=artist&access_token={var}"),
("parse",["artists","items",0,"images",0,"url"])
]
}
]
})
apis_tracks = [
{
apis_tracks = []
if get_settings("LASTFM_API_KEY") not in [None,"ASK"]:
apis_tracks.append({
"name":"LastFM",
"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"],
#"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"],
"steps":[
("get","https://ws.audioscrobbler.com/2.0/?method=track.getinfo&track={titlestring}&artist={artiststring}&api_key=" + get_settings("LASTFM_API_KEY") + "&format=json"),
("parse",["track","album","image",3,"#text"])
]
},
{
})
if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"]:
apis_tracks.append({
"name":"Spotify",
"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
#"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
"steps":[
("post","https://accounts.spotify.com/api/token",{"Authorization":"Basic " + base64.b64encode(bytes(get_settings("SPOTIFY_API_ID") + ":" + get_settings("SPOTIFY_API_SECRET"),encoding="utf-8")).decode("utf-8")},{"grant_type":"client_credentials"}),
("parse",["access_token"]),
("get","https://api.spotify.com/v1/search?q={artiststring}%20{titlestring}&type=track&access_token={var}"),
("parse",["tracks","items",0,"album","images",0,"url"])
]
}
]
})
def api_request_artist(artist):
for api in apis_artists:
if api["check"]:
if True:
log("API: " + api["name"] + "; Image request: " + artist,module="external")
try:
artiststring = urllib.parse.quote(artist)
@ -85,7 +92,7 @@ def api_request_artist(artist):
def api_request_track(track):
artists, title = track
for api in apis_tracks:
if api["check"]:
if True:
log("API: " + api["name"] + "; Image request: " + "/".join(artists) + " - " + title,module="external")
try:
artiststring = urllib.parse.quote(", ".join(artists))

View File

@ -14,8 +14,9 @@ def entity_column(element,counting=[],image=None):
if "artists" in element:
# track
html += "<td class='artists'>" + html_links(element["artists"]) + "</td>"
html += "<td class='title'>" + html_link(element) + "</td>"
# 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>"
else:
# artist
html += "<td class='artist'>" + html_link(element)

9
maloja
View File

@ -13,7 +13,8 @@ neededmodules = [
"bottle",
"waitress",
"setproctitle",
"doreah"
"doreah",
"nimrodel"
]
recommendedmodules = [
@ -117,10 +118,10 @@ def install():
if toinstall != [] or toinstallr != []:
if os.geteuid() != 0:
print("Installing python modules should be fairly straight-forward, but Maloja can try to install them automatically. For this, you need to run this script as a root user.")
print("You can install them with",yellow("pip install -r requirements.txt"),"or Maloja can try to install them automatically. For this, you need to run this script as a root user.")
return False
else:
print("Installing python modules should be fairly straight-forward, but Maloja can try to install them automatically, This might or might not work / bloat your system / cause a nuclear war.")
print("You can install them with",yellow("pip install -r requirements.txt"),"or Maloja can try to install them automatically, This might or might not work / bloat your system / cause a nuclear war.")
fail = False
if toinstall != []:
print("Attempt to install required modules? [Y/n]")
@ -283,7 +284,7 @@ def update():
os.chmod("./maloja",os.stat("./maloja").st_mode | stat.S_IXUSR)
print("Make sure to install the latest version of doreah! (" + yellow("pip3 install --upgrade --no-cache-dir doreah") + ")")
print("Make sure to update required modules! (" + yellow("pip3 install -r requirements.txt --upgrade --no-cache-dir") + ")")
if stop(): start() #stop returns whether it was running before, in which case we restart it

View File

@ -66,6 +66,9 @@ class MRangeDescriptor:
def unlimited(self):
return False
def active(self):
return (self.last_stamp() > datetime.datetime.utcnow().timestamp())
# returns the description of the range including buttons to go back and forth
#def desc_interactive(self,**kwargs):
# if self.next(1) is None:
@ -99,7 +102,7 @@ class MTime(MRangeDescriptor):
# whether we currently live or will ever again live in this range
def active(self):
tod = datetime.date.today()
tod = datetime.datetime.utcnow().date()
if tod.year > self.year: return False
if self.precision == 1: return True
if tod.year == self.year:
@ -235,13 +238,13 @@ class MTimeWeek(MRangeDescriptor):
return str(self)
# whether we currently live or will ever again live in this range
def active(self):
tod = datetime.date.today()
if tod.year > self.year: return False
if tod.year == self.year:
if tod.chrcalendar()[1] > self.week: return False
return True
# def active(self):
# tod = datetime.date.today()
# if tod.year > self.year: return False
# if tod.year == self.year:
# if tod.chrcalendar()[1] > self.week: return False
#
# return True
def urikeys(self):
return {"in":str(self)}

6
requirements.txt Normal file
View File

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

View File

@ -5,6 +5,7 @@
countas Selena Gomez & The Scene Selena Gomez
countas The Police Sting
countas Trouble Maker HyunA
countas S Club 7 Tina Barrett
# Group more famous than single artist
countas RenoakRhythm Approaching Nirvana
countas Shirley Manson Garbage

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

View File

@ -46,6 +46,9 @@ replacetitle 종이 심장 (Paper Heart) Paper Heart
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
# Stellar
replaceartist STELLAR Stellar
@ -111,6 +114,7 @@ replacetitle 음오아예 (Um Oh Ah Yeh) (Um Oh Ah Yeh) Um Oh Ah Yeh
replacetitle 따끔 (a little bit) A Little Bit
# Hello Venus
replaceartist HELLOVENUS Hello Venus
replaceartist Hello/Venus Hello Venus
# BESTie

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

View File

@ -18,5 +18,6 @@ replaceartist Sir James Galway Viggo Mortensen And Renée Fleming Album Version
notanartist In Dreams
notanartist Aniron Theme For Aragorn And Arwen
notanartist Lament for Gandalf
replaceartist James Galway Sir James Galway
replaceartist Ben del Maestro Ben Del Maestro
replaceartist James Galway Sir James Galway
replaceartist Ben del Maestro Ben Del Maestro
replacetitle Anduril Andúril

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

View File

@ -202,7 +202,8 @@ class Controller {
// Already played full song
while (this.alreadyPlayed > this.currentLength) {
this.alreadyPlayed = this.alreadyPlayed - this.currentLength
scrobble(this.currentArtist,this.currentTitle,this.currentLength)
var secondsago = this.alreadyPlayed
scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago)
}
this.setUpdate()
@ -248,7 +249,8 @@ class Controller {
// Already played full song
while (this.alreadyPlayed > this.currentLength) {
this.alreadyPlayed = this.alreadyPlayed - this.currentLength
scrobble(this.currentArtist,this.currentTitle,this.currentLength)
var secondsago = this.alreadyPlayed
scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago)
}
this.currentlyPlaying = false
@ -282,17 +284,22 @@ class Controller {
function scrobble(artist,title,seconds) {
console.log("Scrobbling " + artist + " - " + title + "; " + seconds + " seconds playtime")
artiststring = encodeURIComponent(artist)
titlestring = encodeURIComponent(title)
function scrobble(artist,title,seconds,secondsago=0) {
console.log("Scrobbling " + artist + " - " + title + "; " + seconds + " seconds playtime, " + secondsago + " seconds ago")
var artiststring = encodeURIComponent(artist)
var titlestring = encodeURIComponent(title)
var d = new Date()
var time = Math.floor(d.getTime()/1000) - secondsago
//console.log("Time: " + time)
var requestbody = "artist=" + artiststring + "&title=" + titlestring + "&duration=" + seconds + "&time=" + time
chrome.storage.local.get("apikey",function(result) {
APIKEY = result["apikey"]
chrome.storage.local.get("serverurl",function(result) {
URL = result["serverurl"]
var xhttp = new XMLHttpRequest();
xhttp.open("POST",URL + "/api/newscrobble",true);
xhttp.send("artist=" + artiststring + "&title=" + titlestring + "&duration=" + seconds + "&key=" + APIKEY)
xhttp.send(requestbody + "&key=" + APIKEY)
//console.log("Sent: " + requestbody)
});
});

Binary file not shown.

View File

@ -124,6 +124,7 @@ def static_image(pth):
@webserver.route("/<name:re:.*\\.png>")
@webserver.route("/<name:re:.*\\.jpeg>")
@webserver.route("/<name:re:.*\\.ico>")
@webserver.route("/<name:re:.*\\.txt>")
def static(name):
response = static_file("website/" + name,root="")
response.set_header("Cache-Control", "public, max-age=3600")
@ -214,7 +215,8 @@ setproctitle.setproctitle("Maloja")
## start database
database.start_db()
database.register_subroutes(webserver,"/api")
#database.register_subroutes(webserver,"/api")
database.dbserver.mount(server=webserver)
log("Starting up Maloja server...")
run(webserver, host='::', port=MAIN_PORT, server='waitress')

View File

@ -30,6 +30,13 @@ DEFAULT_RANGE_CHARTS_TRACKS = year
# can be days, weeks, months, years
DEFAULT_RANGE_PULSE = months
[Fluff]
# how many scrobbles a track needs to aquire this status
SCROBBLES_GOLD = 250
SCROBBLES_PLATINUM = 500
SCROBBLES_DIAMOND = 1000
[Misc]
EXPERIMENTAL_FEATURES = no

View File

@ -375,6 +375,12 @@ a.bronze {
img.certrecord {
height:30px;
vertical-align: text-bottom;
}
/*
**
**
@ -463,12 +469,16 @@ table.list td.icon div {
background-position:center;
}
table.list td.artists,td.artist,td.title {
table.list td.artists,td.artist,td.title,td.track {
min-width:100px;
}
table.list td.track span.artist_in_trackcolumn {
color:#bbb;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

2
website/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /api/

View File

@ -15,7 +15,7 @@
</td>
<td class="text">
<span>KEY_ARTISTS</span><br/>
<h1>KEY_TRACKTITLE</h1> <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?max=100">KEY_POSITION</a></span>
<p class="stats"><a href="/scrobbles?KEY_SCROBBLELINK">KEY_SCROBBLES Scrobbles</a></p>

View File

@ -16,11 +16,14 @@ def instructions(keys):
imgurl = getTrackImage(track["artists"],track["title"],fast=True)
pushresources = [{"file":imgurl,"type":"image"}] if imgurl.startswith("/") else []
data = database.trackInfo(track["artists"],track["title"])
data = database.trackInfo(track)
scrobblesnum = str(data["scrobbles"])
pos = "#" + str(data["position"])
html_cert = ""
if data["certification"] is not None:
html_cert = "<img class='certrecord' src='/media/record_{cert}.png' title='This track has reached {certc} status' />".format(cert=data["certification"],certc=data["certification"].capitalize())
html_medals = ""
if "medals" in data and data["medals"] is not None:
@ -61,6 +64,7 @@ def instructions(keys):
"KEY_IMAGEURL":imgurl,
"KEY_SCROBBLELINK":compose_querystring(keys),
"KEY_MEDALS":html_medals,
"KEY_CERTS":html_cert,
"KEY_SCROBBLELIST":html_scrobbles,
# pulse
"KEY_PULSE_MONTHS":html_pulse_months,