mirror of
https://github.com/krateng/maloja.git
synced 2025-04-16 08:50:32 +03:00
Merge branch 'backend'
This commit is contained in:
commit
833048440c
@ -74,6 +74,8 @@ I can support you with issues best if you use **Alpine Linux**. In my experience
|
||||
|
||||
There is a Dockerfile in the repo that should work by itself. You can also use the unofficial [Dockerhub repository](https://hub.docker.com/r/foxxmd/maloja) kindly provided by FoxxMD.
|
||||
|
||||
You might want to set the environment variables `MALOJA_DEFAULT_PASSWORD`, `MALOJA_SKIP_SETUP` and `MALOJA_DATA_DIRECTORY`.
|
||||
|
||||
|
||||
## How to use
|
||||
|
||||
@ -141,7 +143,7 @@ It is recommended to define a different API key for every scrobbler you use in `
|
||||
|
||||
### Manual
|
||||
|
||||
If you can't automatically scrobble your music, you can always do it manually on the `/manual` page of your Maloja server.
|
||||
If you can't automatically scrobble your music, you can always do it manually on the `/admin_manual` page of your Maloja server.
|
||||
|
||||
|
||||
## How to extend
|
||||
|
@ -15,7 +15,7 @@ links = {
|
||||
requires = [
|
||||
"bottle>=0.12.16",
|
||||
"waitress>=1.3",
|
||||
"doreah>=1.6.7",
|
||||
"doreah>=1.6.9",
|
||||
"nimrodel>=0.6.3",
|
||||
"setproctitle>=1.1.10",
|
||||
"wand>=0.5.4",
|
||||
|
0
maloja/data_files/auth/dummy
Normal file
0
maloja/data_files/auth/dummy
Normal file
@ -7,6 +7,14 @@
|
||||
WEB_PORT = 42010
|
||||
HOST = "::" # You most likely want either :: for IPv6 or 0.0.0.0 for IPv4 here
|
||||
|
||||
[Login]
|
||||
|
||||
DEFAULT_PASSWORD = none
|
||||
FORCE_PASSWORD = none
|
||||
# these are only meant for Docker containers
|
||||
# on first start, set the environment variable MALOJA_DEFAULT_PASSWORD
|
||||
# if you forgot and already generated a random password, you can overwrite it with MALOJA_FORCE_PASSWORD
|
||||
|
||||
[Third Party Services]
|
||||
|
||||
# order in which to use the metadata providers
|
||||
|
@ -18,6 +18,7 @@ from doreah.logging import log
|
||||
from doreah import tsv
|
||||
from doreah import settings
|
||||
from doreah.caching import Cache, DeepCache
|
||||
from doreah.auth import authenticated_api, authenticated_api_with_alternate
|
||||
try:
|
||||
from doreah.persistence import DiskDict
|
||||
except: pass
|
||||
@ -242,6 +243,23 @@ def normalize_name(name):
|
||||
########
|
||||
########
|
||||
|
||||
# skip regular authentication if api key is present in request
|
||||
# an api key now ONLY permits scrobbling tracks, no other admin tasks
|
||||
def api_key_correct(request):
|
||||
args = request.query
|
||||
print(dict(args))
|
||||
if "key" in args:
|
||||
apikey = args["key"]
|
||||
print(args)
|
||||
del args["key"]
|
||||
print(args)
|
||||
elif "apikey" in args:
|
||||
apikey = args["apikey"]
|
||||
del args["apikey"]
|
||||
else: return False
|
||||
|
||||
return checkAPIkey(apikey)
|
||||
|
||||
|
||||
dbserver = API(delay=True,path="api")
|
||||
|
||||
@ -671,23 +689,19 @@ def trackInfo(track):
|
||||
|
||||
@dbserver.get("newscrobble")
|
||||
@dbserver.post("newscrobble")
|
||||
@authenticated_api_with_alternate(api_key_correct)
|
||||
def post_scrobble(artist:Multi,**keys):
|
||||
artists = "/".join(artist)
|
||||
title = keys.get("title")
|
||||
album = keys.get("album")
|
||||
duration = keys.get("seconds")
|
||||
apikey = keys.get("key")
|
||||
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")
|
||||
log("Incoming scrobble (native API): ARTISTS: " + str(artists) + ", TRACK: " + title,module="debug")
|
||||
(artists,title) = cla.fullclean(artists,title)
|
||||
|
||||
## this is necessary for localhost testing
|
||||
@ -720,13 +734,12 @@ def sapi(path:Multi,**keys):
|
||||
|
||||
|
||||
@dbserver.post("newrule")
|
||||
@authenticated_api
|
||||
def newrule(**keys):
|
||||
apikey = keys.pop("key",None)
|
||||
if (checkAPIkey(apikey)):
|
||||
tsv.add_entry(datadir("rules/webmade.tsv"),[k for k in keys])
|
||||
#addEntry("rules/webmade.tsv",[k for k in keys])
|
||||
global db_rulestate
|
||||
db_rulestate = False
|
||||
tsv.add_entry(datadir("rules/webmade.tsv"),[k for k in keys])
|
||||
#addEntry("rules/webmade.tsv",[k for k in keys])
|
||||
global db_rulestate
|
||||
db_rulestate = False
|
||||
|
||||
|
||||
def issues():
|
||||
@ -822,40 +835,84 @@ def check_issues():
|
||||
|
||||
|
||||
|
||||
def get_predefined_rulesets():
|
||||
validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
rulesets = []
|
||||
|
||||
for f in os.listdir(datadir("rules/predefined")):
|
||||
if f.endswith(".tsv"):
|
||||
|
||||
rawf = f.replace(".tsv","")
|
||||
valid = True
|
||||
for char in rawf:
|
||||
if char not in validchars:
|
||||
valid = False
|
||||
break # don't even show up invalid filenames
|
||||
|
||||
if not valid: continue
|
||||
if not "_" in rawf: continue
|
||||
|
||||
try:
|
||||
with open(datadir("rules/predefined",f)) as tsvfile:
|
||||
line1 = tsvfile.readline()
|
||||
line2 = tsvfile.readline()
|
||||
|
||||
if "# NAME: " in line1:
|
||||
name = line1.replace("# NAME: ","")
|
||||
else: name = rawf.split("_")[1]
|
||||
if "# DESC: " in line2:
|
||||
desc = line2.replace("# DESC: ","")
|
||||
else: desc = ""
|
||||
|
||||
author = rawf.split("_")[0]
|
||||
except:
|
||||
continue
|
||||
|
||||
ruleset = {"file":rawf}
|
||||
rulesets.append(ruleset)
|
||||
if os.path.exists(datadir("rules",f)):
|
||||
ruleset["active"] = True
|
||||
else:
|
||||
ruleset["active"] = False
|
||||
|
||||
ruleset["name"] = name
|
||||
ruleset["author"] = author
|
||||
ruleset["desc"] = desc
|
||||
|
||||
return rulesets
|
||||
|
||||
@dbserver.post("importrules")
|
||||
@authenticated_api
|
||||
def import_rulemodule(**keys):
|
||||
apikey = keys.pop("key",None)
|
||||
filename = keys.get("filename")
|
||||
remove = keys.get("remove") is not None
|
||||
validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
filename = "".join(c for c in filename if c in validchars)
|
||||
|
||||
if (checkAPIkey(apikey)):
|
||||
filename = keys.get("filename")
|
||||
remove = keys.get("remove") is not None
|
||||
validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
filename = "".join(c for c in filename if c in validchars)
|
||||
|
||||
if remove:
|
||||
log("Deactivating predefined rulefile " + filename)
|
||||
os.remove(datadir("rules/" + filename + ".tsv"))
|
||||
else:
|
||||
log("Importing predefined rulefile " + filename)
|
||||
os.symlink(datadir("rules/predefined/" + filename + ".tsv"),datadir("rules/" + filename + ".tsv"))
|
||||
if remove:
|
||||
log("Deactivating predefined rulefile " + filename)
|
||||
os.remove(datadir("rules/" + filename + ".tsv"))
|
||||
else:
|
||||
log("Importing predefined rulefile " + filename)
|
||||
os.symlink(datadir("rules/predefined/" + filename + ".tsv"),datadir("rules/" + filename + ".tsv"))
|
||||
|
||||
|
||||
|
||||
@dbserver.post("rebuild")
|
||||
@authenticated_api
|
||||
def rebuild(**keys):
|
||||
apikey = keys.pop("key",None)
|
||||
if (checkAPIkey(apikey)):
|
||||
log("Database rebuild initiated!")
|
||||
global db_rulestate
|
||||
db_rulestate = False
|
||||
sync()
|
||||
from .proccontrol.tasks.fixexisting import fix
|
||||
fix()
|
||||
global cla, coa
|
||||
cla = CleanerAgent()
|
||||
coa = CollectorAgent()
|
||||
build_db()
|
||||
invalidate_caches()
|
||||
log("Database rebuild initiated!")
|
||||
global db_rulestate
|
||||
db_rulestate = False
|
||||
sync()
|
||||
from .proccontrol.tasks.fixexisting import fix
|
||||
fix()
|
||||
global cla, coa
|
||||
cla = CleanerAgent()
|
||||
coa = CollectorAgent()
|
||||
build_db()
|
||||
invalidate_caches()
|
||||
|
||||
|
||||
|
||||
@ -896,15 +953,15 @@ def search(**keys):
|
||||
|
||||
|
||||
@dbserver.post("addpicture")
|
||||
def add_picture(b64,key,artist:Multi=[],title=None):
|
||||
if (checkAPIkey(key)):
|
||||
keys = FormsDict()
|
||||
for a in artist:
|
||||
keys.append("artist",a)
|
||||
if title is not None: keys.append("title",title)
|
||||
k_filter, _, _, _ = uri_to_internal(keys)
|
||||
if "track" in k_filter: k_filter = k_filter["track"]
|
||||
utilities.set_image(b64,**k_filter)
|
||||
@authenticated_api
|
||||
def add_picture(b64,artist:Multi=[],title=None):
|
||||
keys = FormsDict()
|
||||
for a in artist:
|
||||
keys.append("artist",a)
|
||||
if title is not None: keys.append("title",title)
|
||||
k_filter, _, _, _ = uri_to_internal(keys)
|
||||
if "track" in k_filter: k_filter = k_filter["track"]
|
||||
utilities.set_image(b64,**k_filter)
|
||||
|
||||
####
|
||||
## Server operation
|
||||
|
@ -49,6 +49,12 @@ config(
|
||||
},
|
||||
regular={
|
||||
"autostart": False
|
||||
},
|
||||
auth={
|
||||
"multiuser":False,
|
||||
"cookieprefix":"maloja",
|
||||
"stylesheets":["/style.css"],
|
||||
"dbfile":datadir("auth/auth.ddb")
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -43,7 +43,7 @@ def start():
|
||||
print("Visit your server address (Port " + str(port) + ") to see your web interface. Visit /setup to get started.")
|
||||
print("If you're installing this on your local machine, these links should get you there:")
|
||||
print("\t" + col["blue"]("http://localhost:" + str(port)))
|
||||
print("\t" + col["blue"]("http://localhost:" + str(port) + "/setup"))
|
||||
print("\t" + col["blue"]("http://localhost:" + str(port) + "/admin_setup"))
|
||||
return True
|
||||
except:
|
||||
print("Error while starting Maloja.")
|
||||
|
@ -2,6 +2,7 @@ import pkg_resources
|
||||
from distutils import dir_util
|
||||
from doreah import settings
|
||||
from doreah.io import col, ask, prompt
|
||||
from doreah import auth
|
||||
import os
|
||||
|
||||
from ..globalconf import datadir
|
||||
@ -22,7 +23,13 @@ def copy_initial_local_files():
|
||||
#shutil.copy(folder,DATA_DIR)
|
||||
dir_util.copy_tree(folder,datadir(),update=False)
|
||||
|
||||
|
||||
charset = list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
def randomstring(length=32):
|
||||
import random
|
||||
key = ""
|
||||
for i in range(length):
|
||||
key += str(random.choice(charset))
|
||||
return key
|
||||
|
||||
def setup():
|
||||
|
||||
@ -48,10 +55,7 @@ def setup():
|
||||
else:
|
||||
answer = ask("Do you want to set up a key to enable scrobbling? Your scrobble extension needs that key so that only you can scrobble tracks to your database.",default=True,skip=SKIP)
|
||||
if answer:
|
||||
import random
|
||||
key = ""
|
||||
for i in range(64):
|
||||
key += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
|
||||
key = randomstring(64)
|
||||
print("Your API Key: " + col["yellow"](key))
|
||||
with open(datadir("clients/authenticated_machines.tsv"),"w") as keyfile:
|
||||
keyfile.write(key + "\t" + "Default Generated Key")
|
||||
@ -59,6 +63,32 @@ def setup():
|
||||
pass
|
||||
|
||||
|
||||
# PASSWORD
|
||||
defaultpassword = settings.get_settings("DEFAULT_PASSWORD")
|
||||
forcepassword = settings.get_settings("FORCE_PASSWORD")
|
||||
# this is mainly meant for docker, supply password via environment variable
|
||||
|
||||
if forcepassword is not None:
|
||||
# user has specified to force the pw, nothing else matters
|
||||
auth.defaultuser.setpw(forcepassword)
|
||||
print("Password has been set.")
|
||||
elif auth.defaultuser.checkpw("admin"):
|
||||
# if the actual pw is admin, it means we've never set this up properly (eg first start after update)
|
||||
if defaultpassword is None:
|
||||
# non-docker installation or user didn't set environment variable
|
||||
defaultpassword = randomstring(32)
|
||||
newpw = prompt("Please set a password for web backend access. Leave this empty to generate a random password.",skip=SKIP,secret=True)
|
||||
if newpw is None:
|
||||
newpw = defaultpassword
|
||||
print("Generated password:",newpw)
|
||||
auth.defaultuser.setpw(newpw)
|
||||
else:
|
||||
# docker installation (or settings file, but don't do that)
|
||||
# we still 'ask' the user to set one, but for docker this will be skipped
|
||||
newpw = prompt("Please set a password for web backend access. Leave this empty to use the default password.",skip=SKIP,default=defaultpassword,secret=True)
|
||||
auth.defaultuser.setpw(newpw)
|
||||
|
||||
|
||||
if settings.get_settings("NAME") is None:
|
||||
name = prompt("Please enter your name. This will be displayed e.g. when comparing your charts to another user. Leave this empty if you would not like to specify a name right now.",default="Generic Maloja User",skip=SKIP)
|
||||
settings.update_settings(datadir("settings/settings.ini"),{"NAME":name},create_new=True)
|
||||
|
102
maloja/server.py
102
maloja/server.py
@ -4,7 +4,7 @@ from .globalconf import datadir, DATA_DIR
|
||||
|
||||
|
||||
# server stuff
|
||||
from bottle import Bottle, route, get, post, error, run, template, static_file, request, response, FormsDict, redirect, template, HTTPResponse, BaseRequest
|
||||
from bottle import Bottle, route, get, post, error, run, template, static_file, request, response, FormsDict, redirect, template, HTTPResponse, BaseRequest, abort
|
||||
import waitress
|
||||
# templating
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
@ -25,6 +25,7 @@ from doreah import settings
|
||||
from doreah.logging import log
|
||||
from doreah.timing import Clock
|
||||
from doreah.pyhp import file as pyhpfile
|
||||
from doreah import auth
|
||||
# technical
|
||||
#from importlib.machinery import SourceFileLoader
|
||||
import importlib
|
||||
@ -55,6 +56,7 @@ STATICFOLDER = pkg_resources.resource_filename(__name__,"static")
|
||||
DATAFOLDER = DATA_DIR
|
||||
|
||||
webserver = Bottle()
|
||||
auth.authapi.mount(server=webserver)
|
||||
|
||||
pthjoin = os.path.join
|
||||
|
||||
@ -92,10 +94,9 @@ def mainpage():
|
||||
def customerror(error):
|
||||
code = int(str(error).split(",")[0][1:])
|
||||
|
||||
if os.path.exists(pthjoin(WEBFOLDER,"errors",str(code) + ".pyhp")):
|
||||
return pyhpfile(pthjoin(WEBFOLDER,"errors",str(code) + ".pyhp"),{"errorcode":code})
|
||||
else:
|
||||
return pyhpfile(pthjoin(WEBFOLDER,"errors","generic.pyhp"),{"errorcode":code})
|
||||
template = jinjaenv.get_template('error.jinja')
|
||||
res = template.render(errorcode=code)
|
||||
return res
|
||||
|
||||
|
||||
|
||||
@ -158,6 +159,10 @@ def get_css():
|
||||
return css
|
||||
|
||||
|
||||
@webserver.route("/login")
|
||||
def login():
|
||||
return auth.get_login_page()
|
||||
|
||||
@webserver.route("/<name>.<ext>")
|
||||
def static(name,ext):
|
||||
assert ext in ["txt","ico","jpeg","jpg","png","less","js"]
|
||||
@ -173,6 +178,10 @@ def static(name,ext):
|
||||
return response
|
||||
|
||||
|
||||
aliases = {
|
||||
"admin": "admin_overview",
|
||||
"manual": "admin_manual"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -216,8 +225,17 @@ jinjaenv = Environment(
|
||||
jinjaenv.globals.update(JINJA_CONTEXT)
|
||||
|
||||
|
||||
@webserver.route("/<name:re:admin.*>")
|
||||
@auth.authenticated
|
||||
def static_html_private(name):
|
||||
return static_html(name)
|
||||
|
||||
@webserver.route("/<name>")
|
||||
def static_html_public(name):
|
||||
return static_html(name)
|
||||
|
||||
def static_html(name):
|
||||
if name in aliases: redirect(aliases[name])
|
||||
linkheaders = ["</style.css>; rel=preload; as=style"]
|
||||
keys = remove_identical(FormsDict.decode(request.query))
|
||||
|
||||
@ -228,7 +246,7 @@ def static_html(name):
|
||||
pyhp_pref = settings.get_settings("USE_PYHP")
|
||||
jinja_pref = settings.get_settings("USE_JINJA")
|
||||
|
||||
adminmode = request.cookies.get("adminmode") == "true" and database.checkAPIkey(request.cookies.get("apikey")) is not False
|
||||
adminmode = request.cookies.get("adminmode") == "true" and auth.check(request)
|
||||
|
||||
clock = Clock()
|
||||
clock.start()
|
||||
@ -277,48 +295,50 @@ def static_html(name):
|
||||
|
||||
# if not, use the old way
|
||||
else:
|
||||
try:
|
||||
with open(pthjoin(WEBFOLDER,name + ".html")) as htmlfile:
|
||||
html = htmlfile.read()
|
||||
|
||||
with open(pthjoin(WEBFOLDER,name + ".html")) as htmlfile:
|
||||
html = htmlfile.read()
|
||||
|
||||
# apply global substitutions
|
||||
with open(pthjoin(WEBFOLDER,"common/footer.html")) as footerfile:
|
||||
footerhtml = footerfile.read()
|
||||
with open(pthjoin(WEBFOLDER,"common/header.html")) as headerfile:
|
||||
headerhtml = headerfile.read()
|
||||
html = html.replace("</body>",footerhtml + "</body>").replace("</head>",headerhtml + "</head>")
|
||||
# apply global substitutions
|
||||
with open(pthjoin(WEBFOLDER,"common/footer.html")) as footerfile:
|
||||
footerhtml = footerfile.read()
|
||||
with open(pthjoin(WEBFOLDER,"common/header.html")) as headerfile:
|
||||
headerhtml = headerfile.read()
|
||||
html = html.replace("</body>",footerhtml + "</body>").replace("</head>",headerhtml + "</head>")
|
||||
|
||||
|
||||
# If a python file exists, it provides the replacement dict for the html file
|
||||
if os.path.exists(pthjoin(WEBFOLDER,name + ".py")):
|
||||
#txt_keys = SourceFileLoader(name,"web/" + name + ".py").load_module().replacedict(keys,DATABASE_PORT)
|
||||
try:
|
||||
module = importlib.import_module(".web." + name,package="maloja")
|
||||
txt_keys,resources = module.instructions(keys)
|
||||
except Exception as e:
|
||||
log("Error in website generation: " + str(sys.exc_info()),module="error")
|
||||
raise
|
||||
# If a python file exists, it provides the replacement dict for the html file
|
||||
if os.path.exists(pthjoin(WEBFOLDER,name + ".py")):
|
||||
#txt_keys = SourceFileLoader(name,"web/" + name + ".py").load_module().replacedict(keys,DATABASE_PORT)
|
||||
try:
|
||||
module = importlib.import_module(".web." + name,package="maloja")
|
||||
txt_keys,resources = module.instructions(keys)
|
||||
except Exception as e:
|
||||
log("Error in website generation: " + str(sys.exc_info()),module="error")
|
||||
raise
|
||||
|
||||
# add headers for server push
|
||||
for resource in resources:
|
||||
if all(ord(c) < 128 for c in resource["file"]):
|
||||
# we can only put ascii stuff in the http header
|
||||
linkheaders.append("<" + resource["file"] + ">; rel=preload; as=" + resource["type"])
|
||||
# add headers for server push
|
||||
for resource in resources:
|
||||
if all(ord(c) < 128 for c in resource["file"]):
|
||||
# we can only put ascii stuff in the http header
|
||||
linkheaders.append("<" + resource["file"] + ">; rel=preload; as=" + resource["type"])
|
||||
|
||||
# apply key substitutions
|
||||
for k in txt_keys:
|
||||
if isinstance(txt_keys[k],list):
|
||||
# if list, we replace each occurence with the next item
|
||||
for element in txt_keys[k]:
|
||||
html = html.replace(k,element,1)
|
||||
else:
|
||||
html = html.replace(k,txt_keys[k])
|
||||
# apply key substitutions
|
||||
for k in txt_keys:
|
||||
if isinstance(txt_keys[k],list):
|
||||
# if list, we replace each occurence with the next item
|
||||
for element in txt_keys[k]:
|
||||
html = html.replace(k,element,1)
|
||||
else:
|
||||
html = html.replace(k,txt_keys[k])
|
||||
|
||||
|
||||
response.set_header("Link",",".join(linkheaders))
|
||||
log("Generated page {name} in {time:.5f}s (Python+HTML)".format(name=name,time=clock.stop()),module="debug")
|
||||
return html
|
||||
#return static_file("web/" + name + ".html",root="")
|
||||
response.set_header("Link",",".join(linkheaders))
|
||||
log("Generated page {name} in {time:.5f}s (Python+HTML)".format(name=name,time=clock.stop()),module="debug")
|
||||
return html
|
||||
|
||||
except:
|
||||
abort(404, "Page does not exist")
|
||||
|
||||
|
||||
# Shortlinks
|
||||
|
149
maloja/static/js/manualscrobble.js
Normal file
149
maloja/static/js/manualscrobble.js
Normal file
@ -0,0 +1,149 @@
|
||||
var lastArtists = []
|
||||
var lastTrack = ""
|
||||
|
||||
|
||||
function addArtist(artist) {
|
||||
var newartistfield = document.getElementById("artists");
|
||||
var artistelement = document.createElement("span");
|
||||
artistelement.innerHTML = artist;
|
||||
artistelement.style = "padding:5px;";
|
||||
document.getElementById("artists_td").insertBefore(artistelement,newartistfield);
|
||||
newartistfield.placeholder = "Backspace to remove last"
|
||||
}
|
||||
|
||||
function keyDetect(event) {
|
||||
if (event.key === "Enter" || event.key === "Tab") { addEnteredArtist() }
|
||||
if (event.key === "Backspace" && document.getElementById("artists").value == "") { removeArtist() }
|
||||
}
|
||||
|
||||
function addEnteredArtist() {
|
||||
var newartistfield = document.getElementById("artists");
|
||||
var newartist = newartistfield.value.trim();
|
||||
newartistfield.value = "";
|
||||
if (newartist != "") {
|
||||
addArtist(newartist);
|
||||
}
|
||||
}
|
||||
function removeArtist() {
|
||||
var artists = document.getElementById("artists_td").getElementsByTagName("span")
|
||||
var lastartist = artists[artists.length-1]
|
||||
document.getElementById("artists_td").removeChild(lastartist);
|
||||
if (artists.length < 1) {
|
||||
document.getElementById("artists").placeholder = "Separate with Enter"
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
document.getElementById("title").value = "";
|
||||
document.getElementById("artists").value = "";
|
||||
var artists = document.getElementById("artists_td").getElementsByTagName("span")
|
||||
while (artists.length > 0) {
|
||||
removeArtist();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function scrobbleIfEnter(event) {
|
||||
if (event.key === "Enter") {
|
||||
scrobbleNew()
|
||||
}
|
||||
}
|
||||
|
||||
function scrobbleNew() {
|
||||
var artistnodes = document.getElementById("artists_td").getElementsByTagName("span");
|
||||
var artists = [];
|
||||
for (let node of artistnodes) {
|
||||
artists.push(node.innerHTML);
|
||||
}
|
||||
var title = document.getElementById("title").value;
|
||||
scrobble(artists,title);
|
||||
}
|
||||
|
||||
function scrobble(artists,title) {
|
||||
|
||||
lastArtists = artists;
|
||||
lastTrack = title;
|
||||
|
||||
|
||||
var artist = artists.join(";");
|
||||
|
||||
if (title != "" && artists.length > 0) {
|
||||
xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = scrobbledone
|
||||
xhttp.open("GET","/api/newscrobble?artist=" + encodeURIComponent(artist) +
|
||||
"&title=" + encodeURIComponent(title), true);
|
||||
xhttp.send();
|
||||
}
|
||||
|
||||
document.getElementById("title").value = "";
|
||||
document.getElementById("artists").value = "";
|
||||
var artists = document.getElementById("artists_td").getElementsByTagName("span");
|
||||
while (artists.length > 0) {
|
||||
removeArtist();
|
||||
}
|
||||
}
|
||||
|
||||
function scrobbledone() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
result = JSON.parse(this.responseText);
|
||||
txt = result["track"]["title"] + " by " + result["track"]["artists"][0];
|
||||
if (result["track"]["artists"].length > 1) {
|
||||
txt += " et al.";
|
||||
}
|
||||
document.getElementById("notification").innerHTML = "Scrobbled " + txt + "!";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function repeatLast() {
|
||||
clear();
|
||||
for (let artist of lastArtists) {
|
||||
addArtist(artist);
|
||||
}
|
||||
document.getElementById("title").value = lastTrack;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///
|
||||
// SEARCH
|
||||
///
|
||||
|
||||
function search_manualscrobbling(searchfield) {
|
||||
var txt = searchfield.value;
|
||||
if (txt == "") {
|
||||
|
||||
}
|
||||
else {
|
||||
xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = searchresult_manualscrobbling;
|
||||
xhttp.open("GET","/api/search?max=5&query=" + encodeURIComponent(txt), true);
|
||||
xhttp.send();
|
||||
}
|
||||
}
|
||||
function searchresult_manualscrobbling() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
document.getElementById("searchresults").innerHTML = "";
|
||||
result = JSON.parse(this.responseText);
|
||||
tracks = result["tracks"].slice(0,10);
|
||||
console.log(tracks);
|
||||
for (let t of tracks) {
|
||||
track = document.createElement("span");
|
||||
trackstr = t["artists"].join(", ") + " - " + t["title"];
|
||||
tracklink = t["link"];
|
||||
track.innerHTML = "<a href='" + tracklink + "'>" + trackstr + "</a>";
|
||||
row = document.createElement("tr")
|
||||
col1 = document.createElement("td")
|
||||
col1.className = "button"
|
||||
col1.innerHTML = "Scrobble!"
|
||||
col1.onclick = function(){ scrobble(t["artists"],t["title"])};
|
||||
col2 = document.createElement("td")
|
||||
row.appendChild(col1)
|
||||
row.appendChild(col2)
|
||||
col2.appendChild(track)
|
||||
document.getElementById("searchresults").appendChild(row);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
function upload(encodedentity,apikey,b64) {
|
||||
neo.xhttprequest("/api/addpicture?key=" + apikey + "&" + encodedentity,{"b64":b64},"POST")
|
||||
function upload(encodedentity,b64) {
|
||||
neo.xhttprequest("/api/addpicture?" + encodedentity,{"b64":b64},"POST")
|
||||
}
|
||||
|
@ -101,11 +101,11 @@ div.footer div:nth-child(3) {
|
||||
}
|
||||
|
||||
div.footer span a {
|
||||
padding-left:20px;
|
||||
//padding-left:20px;
|
||||
background-repeat:no-repeat;
|
||||
background-size:contain;
|
||||
background-position:left;
|
||||
background-image:url("https://github.com/favicon.ico");
|
||||
//background-image:url("https://github.com/favicon.ico");
|
||||
}
|
||||
|
||||
div.footer input {
|
||||
|
@ -22,6 +22,6 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a href="/admin"><div title="Server Administration" id="settingsicon">
|
||||
<a href="/admin_overview"><div title="Server Administration" id="settingsicon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17 12.645v-2.289c-1.17-.417-1.907-.533-2.28-1.431-.373-.9.07-1.512.6-2.625l-1.618-1.619c-1.105.525-1.723.974-2.626.6-.9-.374-1.017-1.117-1.431-2.281h-2.29c-.412 1.158-.53 1.907-1.431 2.28h-.001c-.9.374-1.51-.07-2.625-.6l-1.617 1.619c.527 1.11.973 1.724.6 2.625-.375.901-1.123 1.019-2.281 1.431v2.289c1.155.412 1.907.531 2.28 1.431.376.908-.081 1.534-.6 2.625l1.618 1.619c1.107-.525 1.724-.974 2.625-.6h.001c.9.373 1.018 1.118 1.431 2.28h2.289c.412-1.158.53-1.905 1.437-2.282h.001c.894-.372 1.501.071 2.619.602l1.618-1.619c-.525-1.107-.974-1.723-.601-2.625.374-.899 1.126-1.019 2.282-1.43zm-8.5 1.689c-1.564 0-2.833-1.269-2.833-2.834s1.269-2.834 2.833-2.834 2.833 1.269 2.833 2.834-1.269 2.834-2.833 2.834zm15.5 4.205v-1.077c-.55-.196-.897-.251-1.073-.673-.176-.424.033-.711.282-1.236l-.762-.762c-.52.248-.811.458-1.235.283-.424-.175-.479-.525-.674-1.073h-1.076c-.194.545-.25.897-.674 1.073-.424.176-.711-.033-1.235-.283l-.762.762c.248.523.458.812.282 1.236-.176.424-.528.479-1.073.673v1.077c.544.193.897.25 1.073.673.177.427-.038.722-.282 1.236l.762.762c.521-.248.812-.458 1.235-.283.424.175.479.526.674 1.073h1.076c.194-.545.25-.897.676-1.074h.001c.421-.175.706.034 1.232.284l.762-.762c-.247-.521-.458-.812-.282-1.235s.529-.481 1.073-.674zm-4 .794c-.736 0-1.333-.597-1.333-1.333s.597-1.333 1.333-1.333 1.333.597 1.333 1.333-.597 1.333-1.333 1.333z"/></svg>
|
||||
</div></a>
|
||||
|
@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Issues</title>
|
||||
<script src="/cookies.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('/favicon.png')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Possible Issues</h1><br/>
|
||||
<span>with your library</span>
|
||||
<p class="stats">KEY_ISSUES Issues</p>
|
||||
|
||||
<p>Maloja can identify possible problems with consistency or redundancy in your library. After making any changes, you should <a class="textlink" onclick='fullrebuild()'>rebuild your library</a>.<br/>
|
||||
Your API key is required to make any changes to the server: <input id='apikey' onchange='checkAPIkey()' style='width:300px;'/></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
KEY_ISSUESLIST
|
||||
|
||||
</body>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
function newrule() {
|
||||
if (apikeycorrect) {
|
||||
keys = ""
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
keys += encodeURIComponent(arguments[i]) + "&"
|
||||
}
|
||||
apikey = document.getElementById("apikey").value
|
||||
keys += "key=" + encodeURIComponent(apikey)
|
||||
console.log(keys)
|
||||
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/api/newrule?", true);
|
||||
xhttp.send(keys);
|
||||
e = arguments[0]
|
||||
line = e.parentNode
|
||||
line.parentNode.removeChild(line)
|
||||
}
|
||||
}
|
||||
|
||||
function fullrebuild() {
|
||||
if (apikeycorrect) {
|
||||
apikey = document.getElementById("apikey").value
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/api/rebuild", true);
|
||||
xhttp.send("key=" + encodeURIComponent(apikey))
|
||||
window.location = "/wait";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</html>
|
@ -1,44 +0,0 @@
|
||||
import urllib
|
||||
from .. import database
|
||||
from ..htmlgenerators import artistLink
|
||||
|
||||
def instructions(keys):
|
||||
|
||||
db_data = database.issues()
|
||||
i = 0
|
||||
|
||||
html = "<table class='list'>"
|
||||
# if db_data["inconsistent"]:
|
||||
# html += "<tr>"
|
||||
# html += "<td>The current database wasn't built with all current rules in effect. Any problem below might be a false alarm and fixing it could create redundant rules.</td>"
|
||||
# html += """<td class='button important' onclick="fullrebuild()"><div>Rebuild the database</div></td>"""
|
||||
# html += "</tr>"
|
||||
# i += 1
|
||||
for d in db_data["duplicates"]:
|
||||
html += "<tr>"
|
||||
html += "<td>'" + artistLink(d[0]) + "'"
|
||||
html += " is a possible duplicate of "
|
||||
html += "'" + artistLink(d[1]) + "'</td>"
|
||||
html += """<td class='button' onclick="newrule(this,'replaceartist','""" + d[0] + """','""" + d[1] + """')"><div>""" + d[1] + """ is correct</div></td>"""
|
||||
html += """<td class='button' onclick="newrule(this,'replaceartist','""" + d[1] + """','""" + d[0] + """')"><div>""" + d[0] + """ is correct</div></td>"""
|
||||
html += "</tr>"
|
||||
i += 1
|
||||
for c in db_data["combined"]:
|
||||
html += "<tr>"
|
||||
html += "<td>'" + artistLink(c[0]) + "' sounds like the combination of " + str(len(c[1])) + " artists: "
|
||||
for a in c[1]:
|
||||
html += "'" + artistLink(a) + "' "
|
||||
html += "</td>"
|
||||
html += """<td class='button' onclick="newrule(this,'replaceartist','""" + c[0] + """','""" + "␟".join(c[1]) + """')"><div>Fix it</div></td>"""
|
||||
html += "</tr>"
|
||||
i += 1
|
||||
for n in db_data["newartists"]:
|
||||
html += "<tr>"
|
||||
html += "<td>Is '" + n[0] + "' in '" + artistLink(n[1]) + "' an artist?</td>"
|
||||
html += """<td class='button' onclick="newrule(this,'replaceartist','""" + n[1] + """','""" + "␟".join(n[2] + [n[0]]) + """')"><div>Yes</div></td>"""
|
||||
html += "</tr>"
|
||||
i += 1
|
||||
|
||||
html += "</table>"
|
||||
|
||||
return ({"KEY_ISSUESLIST":html,"KEY_ISSUES":str(i)},[])
|
49
maloja/web/jinja/abstracts/admin.jinja
Normal file
49
maloja/web/jinja/abstracts/admin.jinja
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - Backend{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('/favicon.png')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Admin Panel</h1>
|
||||
<br/>
|
||||
<span>
|
||||
{% if page=='admin_overview' %}
|
||||
<span style="opacity:0.5;">Overview</span>
|
||||
{% else %}
|
||||
<a href="/admin_overview">Overview</a>
|
||||
{% endif %} |
|
||||
{% if page=='admin_setup' %}
|
||||
<span style="opacity:0.5;">Server Setup</span>
|
||||
{% else %}
|
||||
<a href="/admin_setup">Server Setup</a>
|
||||
{% endif %} |
|
||||
{% if page=='admin_manual' %}
|
||||
<span style="opacity:0.5;">Manual Scrobbling</span>
|
||||
{% else %}
|
||||
<a href="/admin_manual">Manual Scrobbling</a>
|
||||
{% endif %} |
|
||||
{% if page=='admin_issues' %}
|
||||
<span style="opacity:0.5;">Database Maintenance</span>
|
||||
{% else %}
|
||||
<a href="/admin_issues">Database Maintenance</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
<br/><br/>
|
||||
<span id="notification"></span>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% block maincontent %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
@ -20,6 +20,25 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('{% block imageurl %}/favicon.png{% endblock %}')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>{% block heading %}{% endblock %}</h1><br/>
|
||||
|
||||
|
||||
{% block top_info %}
|
||||
|
||||
{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -48,7 +67,7 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a href="/admin"><div title="Server Administration" id="settingsicon">
|
||||
<a href="/admin_overview"><div title="Server Administration" id="settingsicon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17 12.645v-2.289c-1.17-.417-1.907-.533-2.28-1.431-.373-.9.07-1.512.6-2.625l-1.618-1.619c-1.105.525-1.723.974-2.626.6-.9-.374-1.017-1.117-1.431-2.281h-2.29c-.412 1.158-.53 1.907-1.431 2.28h-.001c-.9.374-1.51-.07-2.625-.6l-1.617 1.619c.527 1.11.973 1.724.6 2.625-.375.901-1.123 1.019-2.281 1.431v2.289c1.155.412 1.907.531 2.28 1.431.376.908-.081 1.534-.6 2.625l1.618 1.619c1.107-.525 1.724-.974 2.625-.6h.001c.9.373 1.018 1.118 1.431 2.28h2.289c.412-1.158.53-1.905 1.437-2.282h.001c.894-.372 1.501.071 2.619.602l1.618-1.619c-.525-1.107-.974-1.723-.601-2.625.374-.899 1.126-1.019 2.282-1.43zm-8.5 1.689c-1.564 0-2.833-1.269-2.833-2.834s1.269-2.834 2.833-2.834 2.833 1.269 2.833 2.834-1.269 2.834-2.833 2.834zm15.5 4.205v-1.077c-.55-.196-.897-.251-1.073-.673-.176-.424.033-.711.282-1.236l-.762-.762c-.52.248-.811.458-1.235.283-.424-.175-.479-.525-.674-1.073h-1.076c-.194.545-.25.897-.674 1.073-.424.176-.711-.033-1.235-.283l-.762.762c.248.523.458.812.282 1.236-.176.424-.528.479-1.073.673v1.077c.544.193.897.25 1.073.673.177.427-.038.722-.282 1.236l.762.762c.521-.248.812-.458 1.235-.283.424.175.479.526.674 1.073h1.076c.194-.545.25-.897.676-1.074h.001c.421-.175.706.034 1.232.284l.762-.762c-.247-.521-.458-.812-.282-1.235s.529-.481 1.073-.674zm-4 .794c-.736 0-1.333-.597-1.333-1.333s.597-1.333 1.333-1.333 1.333.597 1.333 1.333-.597 1.333-1.333 1.333z"/></svg>
|
||||
</div></a>
|
||||
|
77
maloja/web/jinja/admin_issues.jinja
Normal file
77
maloja/web/jinja/admin_issues.jinja
Normal file
@ -0,0 +1,77 @@
|
||||
{% set page ='admin_issues' %}
|
||||
{% extends "abstracts/admin.jinja" %}
|
||||
{% block title %}Maloja - Issues{% endblock %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function newrule() {
|
||||
keys = ""
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
keys += encodeURIComponent(arguments[i]) + "&"
|
||||
}
|
||||
console.log(keys)
|
||||
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/api/newrule?", true);
|
||||
xhttp.send(keys);
|
||||
e = arguments[0]
|
||||
line = e.parentNode
|
||||
line.parentNode.removeChild(line)
|
||||
}
|
||||
|
||||
function fullrebuild() {
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/api/rebuild", true);
|
||||
xhttp.send()
|
||||
window.location = "/wait";
|
||||
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% set issuedata = dbp.issues() %}
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
|
||||
<p>Maloja can identify possible problems with consistency or redundancy in your library. After making any changes, you should <a class="textlink" onclick='fullrebuild()'>rebuild your library</a>.</p>
|
||||
|
||||
<table class="list">
|
||||
|
||||
{% if issuedata.inconsistent %}
|
||||
<tr>
|
||||
<td>The current database wasn't built with all current rules in effect. Any problem below might be a false alarm and fixing it could create redundant rules.</td>
|
||||
<td class='button important' onclick="fullrebuild()"><div>Rebuild the database</div></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% for issue in issuedata.duplicates %}
|
||||
<tr>
|
||||
<td>{{ htmlgenerators.artistLink(issue[0]) }} is a possible duplicate of {{ htmlgenerators.artistLink(issue[1]) }}</td>
|
||||
<td class='button' onclick="newrule(this,'replaceartist','{{ issue[0] }}','{{ issue[1] }}')"><div>{{ issue[1] }} is correct</div></td>
|
||||
<td class='button' onclick="newrule(this,'replaceartist','{{ issue[1] }}','{{ issue[0] }}')"><div>{{ issue[0] }} is correct</div></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% for issue in issuedata.combined %}
|
||||
<tr>
|
||||
<td>{{ artistLink(issue[0]) }} sounds like the combination of {{ len(issue[1]) }} artists:
|
||||
{{ issue[1]|join(", ") }}
|
||||
</td>
|
||||
<td class='button' onclick="newrule(this,'replaceartist','{{ issue[0] }}','{{ issue[1] | join('␟') }}')"><div>Fix it</div></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% for issue in issuedata.newartists %}
|
||||
<tr>
|
||||
<td>Is '{{ issue[0] }}' in '{{ htmlgenerators.artistLink(issue[1]) }}' an artist?</td>
|
||||
<td class='button' onclick="newrule(this,'replaceartist','{{ issue[1] }}','{{ (issue[2] + [issue[0]]) | join('␟') }}')"><div>Yes</div></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
47
maloja/web/jinja/admin_manual.jinja
Normal file
47
maloja/web/jinja/admin_manual.jinja
Normal file
@ -0,0 +1,47 @@
|
||||
{% set page ='admin_manual' %}
|
||||
{% extends "abstracts/admin.jinja" %}
|
||||
{% block title %}Maloja - Manual Scrobbling{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/manualscrobble.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
<h2>Scrobble new discovery</h2>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td style="padding-right:7px;">
|
||||
Artists:
|
||||
</td><td id="artists_td">
|
||||
<input placeholder='Separate with Enter' class='simpleinput' id='artists' onKeydown='keyDetect(event)' />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-right:7px;">
|
||||
Title:
|
||||
</td><td>
|
||||
<input placeholder='Enter to scrobble' class='simpleinput' id='title' onKeydown='scrobbleIfEnter(event)' />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
|
||||
<span class="button" onclick="scrobbleNew(event)">Scrobble!</span>
|
||||
<span class="button" onclick="repeatLast()">Last Manual Scrobble</span>
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
<h2>Search</h2>
|
||||
|
||||
<input class="simpleinput" placeholder='Search for a track' oninput='search_manualscrobbling(this)' />
|
||||
<br/><br/>
|
||||
<table id="searchresults"></table>
|
||||
|
||||
|
||||
{% endblock %}
|
77
maloja/web/jinja/admin_overview.jinja
Normal file
77
maloja/web/jinja/admin_overview.jinja
Normal file
@ -0,0 +1,77 @@
|
||||
{% set page ='admin_overview' %}
|
||||
{% extends "abstracts/admin.jinja" %}
|
||||
{% block title %}Maloja - Admin Panel{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
|
||||
function versioncompare(a,b) {
|
||||
for (var pos=0;pos<3;pos++) {
|
||||
var v1 = parseInt(a[pos]) || 0;
|
||||
var v2 = parseInt(b[pos]) || 0;
|
||||
if (v1 > v2) { return 1;}
|
||||
if (v1 < v2) { return -1;}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
neo.xhttprequest("https://pypi.org/pypi/malojaserver/json",{},"GET",json=true).then((response)=>{
|
||||
result = JSON.parse(response.responseText);
|
||||
latestvers = result.info.version.split(".");
|
||||
|
||||
neo.xhttprequest("/api/serverinfo",{},"GET",json=true).then((response)=>{
|
||||
|
||||
result = JSON.parse(response.responseText);
|
||||
thisvers = result.version;
|
||||
|
||||
document.getElementById("latestversion").innerHTML = latestvers.join(".");
|
||||
document.getElementById("currentversion").innerHTML = thisvers.join(".");
|
||||
|
||||
if (versioncompare(latestvers,thisvers) <= 0) {
|
||||
document.getElementById("currentversion").style.color = "green";
|
||||
}
|
||||
else {
|
||||
document.getElementById("currentversion").style.color = "red";
|
||||
document.getElementById("updatestatus").innerHTML = "Consider updating to take advantage of new features";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function activate() {
|
||||
neo.setCookie("adminmode","true");
|
||||
window.location.reload(true);
|
||||
}
|
||||
function deactivate() {
|
||||
neo.setCookie("adminmode","false");
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
<h2>Update</h2>
|
||||
|
||||
Currently installed Maloja version: <span id="currentversion">Loading...</span><br/>
|
||||
Latest recommended Maloja version: <span id="latestversion">Loading...</span><br/>
|
||||
<span id="updatestatus"></span>
|
||||
|
||||
|
||||
<h2>Admin Mode</h2>
|
||||
|
||||
Admin Mode allows you to manually scrobble from various places on the web interface instead of just the dedicated page.<br/><br/>
|
||||
{% if adminmode %}
|
||||
<span id="adminmodebutton" class="button" onclick="deactivate()">Deactivate</span>
|
||||
{% else %}
|
||||
<span id="adminmodebutton" class="button" onclick="activate()">Activate</span>
|
||||
{% endif %}
|
||||
|
||||
<h2>Links</h2>
|
||||
|
||||
<a class="textlink" target="_blank" rel="noopener noreferrer" href="https://github.com/krateng/maloja/issues/new">Report Issue</a><br/>
|
||||
<a class="textlink" target="_blank" rel="noopener noreferrer" href="https://github.com/krateng/maloja/blob/master/README.md">Readme</a><br/>
|
||||
<a class="textlink" target="_blank" rel="noopener noreferrer" href="https://pypi.org/project/malojaserver/">PyPi</a><br/>
|
||||
{% endblock %}
|
127
maloja/web/jinja/admin_setup.jinja
Normal file
127
maloja/web/jinja/admin_setup.jinja
Normal file
@ -0,0 +1,127 @@
|
||||
{% set page ='admin_setup' %}
|
||||
{% extends "abstracts/admin.jinja" %}
|
||||
{% block title %}Maloja - Setup{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
function replaceurls() {
|
||||
url = window.location.origin
|
||||
s = document.getElementsByName("serverurl")
|
||||
for (var i=0;i<s.length;i++) {
|
||||
s[i].innerHTML = url
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function replace() {
|
||||
replaceurls();
|
||||
}
|
||||
|
||||
function activateRuleModule(e,filename) {
|
||||
keys = "filename=" + encodeURIComponent(filename)
|
||||
console.log(keys)
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/api/importrules", true);
|
||||
xhttp.send(keys);
|
||||
|
||||
e.innerHTML = e.innerHTML.replace("Add","Remove")
|
||||
e.getAttributeNode("onclick").value = e.getAttribute("onclick").replace("activate","deactivate")
|
||||
/* Nobody ever look at this code please */
|
||||
}
|
||||
|
||||
function deactivateRuleModule(e,filename) {
|
||||
keys = "remove&filename=" + encodeURIComponent(filename)
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/api/importrules", true);
|
||||
xhttp.send(keys);
|
||||
|
||||
e.innerHTML = e.innerHTML.replace("Remove","Add")
|
||||
e.getAttributeNode("onclick").value = e.getAttribute("onclick").replace("deactivate","activate")
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded",replace);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% set rulesets = dbp.get_predefined_rulesets() %}
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
|
||||
<h2>Start Scrobbling</h2>
|
||||
|
||||
If you use Vivaldi, Brave, Iridium or any other Chromium-based browser and listen to music on Plex or YouTube Music, download the extension and simply enter the server URL as well as your API key in the relevant fields. They will turn green if the server is accessible.
|
||||
<br/><br/>
|
||||
You can also use any standard-compliant scrobbler. For GNUFM (audioscrobbler) scrobblers, enter <span class="stats"><span name="serverurl">yourserver.tld</span>/api/s/audioscrobbler</span> as your Gnukebox server and your API key as the password. For Listenbrainz scrobblers, use <span class="stats"><span name="serverurl">yourserver.tld</span>/api/s/listenbrainz</span> as the API URL and your API key as token.
|
||||
<br/><br/>
|
||||
If you use another browser or another music player, you could try to code your own extension. The API is super simple! Just send a POST HTTP request to
|
||||
|
||||
<span class="stats"><span name="serverurl">yourserver.tld</span>/api/newscrobble</span>
|
||||
|
||||
(make sure to use the public URL) with the key-value-pairs
|
||||
<br/>
|
||||
<br/>
|
||||
<table class="misc">
|
||||
<tr> <td>artist</td> <td><i>Artist String</i></td> </tr>
|
||||
<tr> <td>title</td> <td><i>Title String</i></td> </tr>
|
||||
<tr> <td>key</td> <td><i>API Key</i></td> </tr>
|
||||
<tr> <td>seconds</td> <td><i>Duration of Scrobble - optional and currently not used</i></td> </tr>
|
||||
</table>
|
||||
<br/><br/>
|
||||
Finally, you could always <a class="textlink" href="/admin_manual">manually scrobble</a>!
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h2>Import your Last.FM data</h2>
|
||||
|
||||
Switching from Last.fm? <a class="textlink" href="https://benjaminbenben.com/lastfm-to-csv/">Download all your data</a> and run the command <span class="stats">maloja import <i>(the file you just downloaded)</i></span>.
|
||||
<br/><br/>
|
||||
|
||||
<h2>Set up some rules</h2>
|
||||
|
||||
After you've scrobbled for a bit, you might want to check the <a class="textlink" href="/admin_issues">Issues page</a> to see if you need to set up some rules. You can also manually add rules in your server's "rules" directory - just add your own .tsv file and read the instructions on how to declare a rule.
|
||||
<br/><br/>
|
||||
|
||||
You can also set up some predefined rulesets right away!
|
||||
<br/>
|
||||
|
||||
<br/><br/>
|
||||
<table class='misc'>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Module</th>
|
||||
<th>Author</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for rs in rulesets %}
|
||||
<tr>
|
||||
{% if rs.active %}
|
||||
<td class='interaction' onclick=deactivateRuleModule(this,'{{ rs.file }}')><a class='textlink'>Remove:</a></td>
|
||||
{% else %}
|
||||
<td class='interaction' onclick=activateRuleModule(this,'{{ rs.file }}')><a class='textlink'>Add:</a></td>
|
||||
{% endif %}
|
||||
<td>{{ rs.name }}</td>
|
||||
<td>{{ rs.author }}</td>
|
||||
<td>{{ rs.desc }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h2>Say thanks</h2>
|
||||
|
||||
Donations are never required, but always appreciated. If you really like Maloja, you can fund my next Buttergipfel via
|
||||
<a class="textlink" target="_blank" rel="noopener noreferrer" href="https://paypal.me/krateng">PayPal</a>, <a class="textlink" href="bitcoin:1krat8JMniJBTiHftMfR1LtF3Y1w5DAxx">Bitcoin</a> or <a class="textlink" target="_blank" rel="noopener noreferrer" href="https://flattr.com/@Krateng">Flattr</a>.
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h2>View your stats</h2>
|
||||
|
||||
Done! Visit <a class="textlink" href="/"><span name="serverurl">yourserver.tld</span></a> (or your public / proxy URL) to look at your overview page. Almost everything is clickable!
|
||||
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{% extends "base.jinja" %}
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - {{ artist }}{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@ -36,7 +36,7 @@
|
||||
<td class="image">
|
||||
{% if adminmode %}
|
||||
<div
|
||||
class="changeable-image" data-uploader="b64=>upload('{encodedartist}','{apikey}',b64)"
|
||||
class="changeable-image" data-uploader="b64=>upload('{{ encodedartist }}',b64)"
|
||||
style="background-image:url('{{ utilities.getArtistImage(artist=artist,fast=True) }}');"
|
||||
></div>
|
||||
{% else %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends "base.jinja" %}
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - {{ artist }}{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends "base.jinja" %}
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - Track Charts{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
18
maloja/web/jinja/error.jinja
Normal file
18
maloja/web/jinja/error.jinja
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
|
||||
{% block content %}
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('/favicon.png')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Error {{ errorcode }}</h1><br/>
|
||||
|
||||
|
||||
<p>That did not work. Don't ask me why.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
@ -1,11 +1,11 @@
|
||||
{% extends "base.jinja" %}
|
||||
{% extends "abstracts/base.jinja" %}
|
||||
{% block title %}Maloja - {{ track.title }}{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/rangeselect.js"></script>
|
||||
<script>
|
||||
function scrobble(encodedtrack,apikey) {
|
||||
neo.xhttprequest('/api/newscrobble?' + encodedtrack + "key=" + apikey).then(response=>{window.location.reload()});
|
||||
function scrobble(encodedtrack) {
|
||||
neo.xhttprequest('/api/newscrobble?' + encodedtrack).then(response=>{window.location.reload()});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -30,7 +30,7 @@
|
||||
<td class="image">
|
||||
{% if adminmode %}
|
||||
<div
|
||||
class="changeable-image" data-uploader="b64=>upload('{encodedartist}','{apikey}',b64)"
|
||||
class="changeable-image" data-uploader="b64=>upload('{{ encodedtrack }}',b64)"
|
||||
style="background-image:url('{{ utilities.getTrackImage(artists=track.artists,title=track.title,fast=True) }}');"
|
||||
></div>
|
||||
{% else %}
|
||||
@ -45,7 +45,10 @@
|
||||
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
|
||||
<br/>
|
||||
|
||||
<p class="stats"><a href="/scrobbles?{{ encodedtrack }}">{{ info['scrobbles'] }} Scrobbles</a></p>
|
||||
<p class="stats">
|
||||
{% if adminmode %}<span onclick="scrobble('{{ encodedtrack }}')" class="button">Scrobble now</span>{% endif %}
|
||||
<a href="/scrobbles?{{ encodedtrack }}">{{ info['scrobbles'] }} Scrobbles</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
@ -1,199 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja</title>
|
||||
<script src="/cookies.js"></script>
|
||||
|
||||
<script>
|
||||
function keyDetect(event) {
|
||||
if (event.key === "Enter" || event.key === "Tab") { addArtist() }
|
||||
if (event.key === "Backspace" && document.getElementById("artists").value == "") { removeArtist() }
|
||||
}
|
||||
|
||||
function addArtist() {
|
||||
element = document.getElementById("artists");
|
||||
newartist = element.value.trim();
|
||||
element.value = "";
|
||||
if (newartist != "") {
|
||||
artist = document.createElement("span");
|
||||
artist.innerHTML = newartist;
|
||||
artist.style = "padding:5px;";
|
||||
document.getElementById("artists_td").insertBefore(artist,element);
|
||||
|
||||
element.placeholder = "Backspace to remove last"
|
||||
}
|
||||
}
|
||||
function removeArtist() {
|
||||
artists = document.getElementById("artists_td").getElementsByTagName("span")
|
||||
lastartist = artists[artists.length-1]
|
||||
document.getElementById("artists_td").removeChild(lastartist);
|
||||
if (artists.length < 1) {
|
||||
document.getElementById("artists").placeholder = "Separate with Enter"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function scrobbleIfEnter(event) {
|
||||
if (event.key === "Enter") {
|
||||
scrobbleNew()
|
||||
}
|
||||
}
|
||||
|
||||
function scrobbleNew() {
|
||||
artistnodes = document.getElementById("artists_td").getElementsByTagName("span");
|
||||
artists = [];
|
||||
for (let node of artistnodes) {
|
||||
artists.push(node.innerHTML);
|
||||
}
|
||||
title = document.getElementById("title").value;
|
||||
scrobble(artists,title);
|
||||
}
|
||||
|
||||
function scrobble(artists,title) {
|
||||
|
||||
|
||||
artist = artists.join(";");
|
||||
key = APIkey();
|
||||
|
||||
if (title != "" && artists.length > 0) {
|
||||
xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = scrobbledone
|
||||
xhttp.open("GET","/api/newscrobble?artist=" + encodeURIComponent(artist) +
|
||||
"&title=" + encodeURIComponent(title) +
|
||||
"&key=" + encodeURIComponent(key), true);
|
||||
xhttp.send();
|
||||
}
|
||||
|
||||
document.getElementById("title").value = "";
|
||||
document.getElementById("artists").value = "";
|
||||
parent = document.getElementById("artists_td");
|
||||
artists = document.getElementById("artists_td").getElementsByTagName("span")
|
||||
while (artists.length > 0) {
|
||||
removeArtist();
|
||||
}
|
||||
}
|
||||
|
||||
function scrobbledone() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
result = JSON.parse(this.responseText);
|
||||
txt = result["track"]["title"] + " by " + result["track"]["artists"][0];
|
||||
if (result["track"]["artists"].length > 1) {
|
||||
txt += " et al.";
|
||||
}
|
||||
document.getElementById("scrobbleresult").innerHTML = "Scrobbled " + txt + "!";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
///
|
||||
// SEARCH
|
||||
///
|
||||
|
||||
function search_manualscrobbling(searchfield) {
|
||||
txt = searchfield.value;
|
||||
if (txt == "") {
|
||||
|
||||
}
|
||||
else {
|
||||
xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = searchresult_manualscrobbling;
|
||||
xhttp.open("GET","/api/search?max=5&query=" + encodeURIComponent(txt), true);
|
||||
xhttp.send();
|
||||
}
|
||||
}
|
||||
function searchresult_manualscrobbling() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
document.getElementById("searchresults").innerHTML = "";
|
||||
result = JSON.parse(this.responseText);
|
||||
tracks = result["tracks"].slice(0,10);
|
||||
console.log(tracks);
|
||||
for (let t of tracks) {
|
||||
track = document.createElement("span");
|
||||
trackstr = t["artists"].join(", ") + " - " + t["title"];
|
||||
tracklink = t["link"];
|
||||
track.innerHTML = "<a href='" + tracklink + "'>" + trackstr + "</a>";
|
||||
row = document.createElement("tr")
|
||||
col1 = document.createElement("td")
|
||||
col1.className = "button"
|
||||
col1.innerHTML = "Scrobble!"
|
||||
col1.onclick = function(){ scrobble(t["artists"],t["title"])};
|
||||
col2 = document.createElement("td")
|
||||
row.appendChild(col1)
|
||||
row.appendChild(col2)
|
||||
col2.appendChild(track)
|
||||
document.getElementById("searchresults").appendChild(row);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
<body>
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('/favicon.png')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Manual Scrobbling</h1><br/>
|
||||
<br/><br/>
|
||||
API Key: <input id='apikey' onchange='checkAPIkey()' style='width:300px;'/><br/><br/>
|
||||
<span id="scrobbleresult"></span>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h1>Scrobble new discovery</h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td style="padding-right:7px;">
|
||||
Artists:
|
||||
</td><td id="artists_td">
|
||||
<input placeholder='Separate with Enter' class='simpleinput' id='artists' onKeydown='keyDetect(event)' />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-right:7px;">
|
||||
Title:
|
||||
</td><td>
|
||||
<input placeholder='Enter to scrobble' class='simpleinput' id='title' onKeydown='scrobbleIfEnter(event)' />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
|
||||
<span class="button" onclick="scrobbleNew(event)">Scrobble!</span>
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
<h1>Search</h1>
|
||||
|
||||
<input class="simpleinput" placeholder='Search for a track' oninput='search_manualscrobbling(this)' />
|
||||
<br/><br/>
|
||||
<table id="searchresults"></table>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,119 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja</title>
|
||||
<script src="/cookies.js"></script>
|
||||
|
||||
<pyhp include="common/header.html" />
|
||||
|
||||
<script>
|
||||
|
||||
function versioncompare(a,b) {
|
||||
for (var pos=0;pos<3;pos++) {
|
||||
var v1 = parseInt(a[pos]) || 0;
|
||||
var v2 = parseInt(b[pos]) || 0;
|
||||
if (v1 > v2) { return 1;}
|
||||
if (v1 < v2) { return -1;}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
neo.xhttprequest("https://pypi.org/pypi/malojaserver/json",{},"GET",json=true).then((response)=>{
|
||||
result = JSON.parse(response.responseText);
|
||||
latestvers = result.info.version.split(".");
|
||||
|
||||
neo.xhttprequest("/api/serverinfo",{},"GET",json=true).then((response)=>{
|
||||
|
||||
result = JSON.parse(response.responseText);
|
||||
thisvers = result.version;
|
||||
|
||||
document.getElementById("latestversion").innerHTML = latestvers.join(".");
|
||||
document.getElementById("currentversion").innerHTML = thisvers.join(".");
|
||||
|
||||
if (versioncompare(latestvers,thisvers) <= 0) {
|
||||
document.getElementById("currentversion").style.color = "green";
|
||||
}
|
||||
else {
|
||||
document.getElementById("currentversion").style.color = "red";
|
||||
document.getElementById("updatestatus").innerHTML = "Consider updating to take advantage of new features";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function activate() {
|
||||
neo.setCookie("adminmode","true");
|
||||
window.location.reload(true);
|
||||
}
|
||||
function deactivate() {
|
||||
neo.setCookie("adminmode","false");
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
function buttonlock() {
|
||||
button = document.getElementById("adminmodebutton")
|
||||
if (apikeycorrect) {
|
||||
button.classList.remove("locked");
|
||||
if (button.innerHTML == "Activate") { button.onclick = activate; }
|
||||
else { button.onclick = deactivate; }
|
||||
// ugh
|
||||
}
|
||||
else {
|
||||
button.classList.add("locked");
|
||||
button.onclick = null;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("load",function(){checkAPIkey(buttonlock)});
|
||||
// we do this twice, but this one ensures that the button is correctly locked / unlocked after the api key has been checked
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('/favicon.png')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Admin Panel</h1><br/>
|
||||
<br/><br/>
|
||||
API Key: <input id='apikey' onchange='checkAPIkey(buttonlock);' style='width:300px;'/><br/><br/>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Update</h2>
|
||||
|
||||
Currently installed Maloja version: <span id="currentversion">Loading...</span><br/>
|
||||
Latest recommended Maloja version: <span id="latestversion">Loading...</span><br/>
|
||||
<span id="updatestatus"></span>
|
||||
|
||||
|
||||
<h2>Admin Mode</h2>
|
||||
|
||||
Admin Mode allows you to manually scrobble from various places on the web interface instead of just the dedicated page.<br/><br/>
|
||||
<pyhp if="adminmode"><span id="adminmodebutton" class="button locked">Deactivate</span></pyhp>
|
||||
<pyhp if="not adminmode"><span id="adminmodebutton" class="button locked">Activate</span></pyhp>
|
||||
|
||||
<h2>Links</h2>
|
||||
|
||||
<a class="textlink" href="/setup">Server Setup</a><br/>
|
||||
<a class="textlink" href="/manual">Manual Scrobbling</a><br/>
|
||||
<a class="textlink" href="/issues">Database Maintenance</a>
|
||||
|
||||
<h2>External</h2>
|
||||
|
||||
<a class="textlink" target="_blank" rel="noopener noreferrer" href="https://github.com/krateng/maloja/issues/new">Report Issue</a><br/>
|
||||
|
||||
<pyhp include="common/footer.html" />
|
||||
</body>
|
||||
</html>
|
@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Error <pyhp echo="errorcode" /></title>
|
||||
<pyhp include="../common/header.html" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('/favicon.png')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Error <pyhp echo="errorcode" /></h1><br/>
|
||||
|
||||
|
||||
<p>That did not work. Don't ask me why.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,133 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Maloja - Setup</title>
|
||||
<script src="/cookies.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
function replaceurls() {
|
||||
url = window.location.origin
|
||||
s = document.getElementsByName("serverurl")
|
||||
for (var i=0;i<s.length;i++) {
|
||||
s[i].innerHTML = url
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function replace() {
|
||||
replaceurls();
|
||||
}
|
||||
|
||||
function activateRuleModule(e,filename) {
|
||||
if (apikeycorrect) {
|
||||
keys = "filename=" + encodeURIComponent(filename)
|
||||
apikey = document.getElementById("apikey").value
|
||||
keys += "&key=" + encodeURIComponent(apikey)
|
||||
console.log(keys)
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/api/importrules", true);
|
||||
xhttp.send(keys);
|
||||
|
||||
e.innerHTML = e.innerHTML.replace("Add","Remove")
|
||||
e.getAttributeNode("onclick").value = e.getAttribute("onclick").replace("activate","deactivate")
|
||||
/* Nobody ever look at this code please */
|
||||
}
|
||||
}
|
||||
|
||||
function deactivateRuleModule(e,filename) {
|
||||
if (apikeycorrect) {
|
||||
keys = "remove&filename=" + encodeURIComponent(filename)
|
||||
apikey = document.getElementById("apikey").value
|
||||
keys += "&key=" + encodeURIComponent(apikey)
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST","/api/importrules", true);
|
||||
xhttp.send(keys);
|
||||
|
||||
e.innerHTML = e.innerHTML.replace("Remove","Add")
|
||||
e.getAttributeNode("onclick").value = e.getAttribute("onclick").replace("deactivate","activate")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body onload="replace()">
|
||||
<table class="top_info">
|
||||
<tr>
|
||||
<td class="image">
|
||||
<div style="background-image:url('/favicon.png')"></div>
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1>Maloja</h1><br/>
|
||||
|
||||
<p class="desc">Welcome to your own Maloja server!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Start Scrobbling</h2>
|
||||
|
||||
If you use Vivaldi, Brave, Iridium or any other Chromium-based browser and listen to music on Plex or YouTube Music, download the extension and simply enter the server URL as well as your API key in the relevant fields. They will turn green if the server is accessible.
|
||||
<br/><br/>
|
||||
You can also use any standard-compliant scrobbler. For GNUFM (audioscrobbler) scrobblers, enter <span class="stats"><span name="serverurl">yourserver.tld</span>/api/s/audioscrobbler</span> as your Gnukebox server and your API key as the password. For Listenbrainz scrobblers, use <span class="stats"><span name="serverurl">yourserver.tld</span>/api/s/listenbrainz</span> as the API URL and your API key as token.
|
||||
<br/><br/>
|
||||
If you use another browser or another music player, you could try to code your own extension. The API is super simple! Just send a POST HTTP request to
|
||||
|
||||
<span class="stats"><span name="serverurl">yourserver.tld</span>/api/newscrobble</span>
|
||||
|
||||
(make sure to use the public URL) with the key-value-pairs
|
||||
<br/>
|
||||
<br/>
|
||||
<table class="misc">
|
||||
<tr> <td>artist</td> <td><i>Artist String</i></td> </tr>
|
||||
<tr> <td>title</td> <td><i>Title String</i></td> </tr>
|
||||
<tr> <td>key</td> <td><i>API Key</i></td> </tr>
|
||||
<tr> <td>seconds</td> <td><i>Duration of Scrobble - optional and currently not used</i></td> </tr>
|
||||
</table>
|
||||
<br/><br/>
|
||||
Finally, you could always <a class="textlink" href="/manual">manually scrobble</a>!
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h2>Import your Last.FM data</h2>
|
||||
|
||||
Switching from Last.fm? <a class="textlink" href="https://benjaminbenben.com/lastfm-to-csv/">Download all your data</a> and run the command <span class="stats">maloja import <i>(the file you just downloaded)</i></span>.
|
||||
<br/><br/>
|
||||
|
||||
<h2>Set up some rules</h2>
|
||||
|
||||
After you've scrobbled for a bit, you might want to check the <a class="textlink" href="/issues">Issues page</a> to see if you need to set up some rules. You can also manually add rules in your server's "rules" directory - just add your own .tsv file and read the instructions on how to declare a rule.
|
||||
<br/><br/>
|
||||
|
||||
You can also set up some predefined rulesets right away! Enter your API key and click the buttons.
|
||||
<br/>
|
||||
API Key:
|
||||
<input id='apikey' onchange='checkAPIkey()' style='width:300px;'/>
|
||||
|
||||
<br/><br/>
|
||||
KEY_PREDEFINED_RULESETS
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h2>Say thanks</h2>
|
||||
|
||||
Coding open source projects is fun, but not really monetizable. If you like Maloja, I would appreciate a small donation via
|
||||
<a class="textlink" target="_blank" rel="noopener noreferrer" href="https://paypal.me/krateng">PayPal</a>, <a class="textlink" href="bitcoin:1krat8JMniJBTiHftMfR1LtF3Y1w5DAxx">Bitcoin</a> or <a class="textlink" href="https://flattr.com/@Krateng">Flattr</a>.
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<h2>View your stats</h2>
|
||||
|
||||
Done! Visit <a class="textlink" href="/"><span name="serverurl">yourserver.tld</span></a> (or your public / proxy URL) to look at your overview page. Almost everything is clickable!
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
@ -1,57 +0,0 @@
|
||||
import os
|
||||
from ..globalconf import datadir
|
||||
|
||||
def instructions(keys):
|
||||
|
||||
html = "<table class='misc'>"
|
||||
|
||||
html += "<tr><th></th><th>Module</th><th>Author</th><th>Description</th></tr>"
|
||||
|
||||
|
||||
validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
for f in os.listdir(datadir("rules/predefined")):
|
||||
if f.endswith(".tsv"):
|
||||
|
||||
rawf = f.replace(".tsv","")
|
||||
valid = True
|
||||
for char in rawf:
|
||||
if char not in validchars:
|
||||
valid = False
|
||||
break # don't even show up invalid filenames
|
||||
|
||||
if not valid: continue
|
||||
if not "_" in rawf: continue
|
||||
|
||||
try:
|
||||
with open(datadir("rules/predefined",f)) as tsvfile:
|
||||
line1 = tsvfile.readline()
|
||||
line2 = tsvfile.readline()
|
||||
|
||||
if "# NAME: " in line1:
|
||||
name = line1.replace("# NAME: ","")
|
||||
else: name = rawf.split("_")[1]
|
||||
if "# DESC: " in line2:
|
||||
desc = line2.replace("# DESC: ","")
|
||||
else: desc = ""
|
||||
|
||||
author = rawf.split("_")[0]
|
||||
except:
|
||||
continue
|
||||
|
||||
html += "<tr>"
|
||||
|
||||
if os.path.exists(datadir("rules",f)):
|
||||
html += "<td class='interaction' onclick=deactivateRuleModule(this,'" + rawf + "')><a class='textlink'>Remove:</a></td>"
|
||||
else:
|
||||
html += "<td class='interaction' onclick=activateRuleModule(this,'" + rawf + "')><a class='textlink'>Add:</a></td>"
|
||||
html += "<td>" + name + "</td>"
|
||||
html += "<td>" + author + "</td>"
|
||||
html += "<td>" + desc + "</td>"
|
||||
|
||||
html += "</tr>"
|
||||
html += "</table>"
|
||||
|
||||
|
||||
pushresources = []
|
||||
replace = {"KEY_PREDEFINED_RULESETS":html}
|
||||
return (replace,pushresources)
|
Loading…
x
Reference in New Issue
Block a user