diff --git a/README.md b/README.md index 8a8cfcd..7ecacfb 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/maloja/__pkginfo__.py b/maloja/__pkginfo__.py index 7a604ad..ca4fa3e 100644 --- a/maloja/__pkginfo__.py +++ b/maloja/__pkginfo__.py @@ -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", diff --git a/maloja/data_files/auth/dummy b/maloja/data_files/auth/dummy new file mode 100644 index 0000000..e69de29 diff --git a/maloja/data_files/settings/default.ini b/maloja/data_files/settings/default.ini index 86e9c2f..f5ff205 100644 --- a/maloja/data_files/settings/default.ini +++ b/maloja/data_files/settings/default.ini @@ -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 diff --git a/maloja/database.py b/maloja/database.py index 13b0f12..5a2fd89 100644 --- a/maloja/database.py +++ b/maloja/database.py @@ -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 diff --git a/maloja/globalconf.py b/maloja/globalconf.py index 5ed516f..bba0938 100644 --- a/maloja/globalconf.py +++ b/maloja/globalconf.py @@ -49,6 +49,12 @@ config( }, regular={ "autostart": False + }, + auth={ + "multiuser":False, + "cookieprefix":"maloja", + "stylesheets":["/style.css"], + "dbfile":datadir("auth/auth.ddb") } ) diff --git a/maloja/proccontrol/control.py b/maloja/proccontrol/control.py index d0cf53e..12c4342 100644 --- a/maloja/proccontrol/control.py +++ b/maloja/proccontrol/control.py @@ -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.") diff --git a/maloja/proccontrol/setup.py b/maloja/proccontrol/setup.py index ad6752a..7f1343c 100644 --- a/maloja/proccontrol/setup.py +++ b/maloja/proccontrol/setup.py @@ -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) diff --git a/maloja/server.py b/maloja/server.py index ff2c73e..58608d4 100755 --- a/maloja/server.py +++ b/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("/.") 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("/") +@auth.authenticated +def static_html_private(name): + return static_html(name) + @webserver.route("/") +def static_html_public(name): + return static_html(name) + def static_html(name): + if name in aliases: redirect(aliases[name]) linkheaders = ["; 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("",footerhtml + "").replace("",headerhtml + "") + # 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("",footerhtml + "").replace("",headerhtml + "") - # 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 diff --git a/maloja/static/js/manualscrobble.js b/maloja/static/js/manualscrobble.js new file mode 100644 index 0000000..03ce272 --- /dev/null +++ b/maloja/static/js/manualscrobble.js @@ -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 = "" + trackstr + ""; + 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); + } + + + } +} diff --git a/maloja/static/js/upload.js b/maloja/static/js/upload.js index 103ea12..797543a 100644 --- a/maloja/static/js/upload.js +++ b/maloja/static/js/upload.js @@ -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") } diff --git a/maloja/static/less/maloja.less b/maloja/static/less/maloja.less index 9a24ef2..6e75625 100644 --- a/maloja/static/less/maloja.less +++ b/maloja/static/less/maloja.less @@ -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 { diff --git a/maloja/web/common/footer.html b/maloja/web/common/footer.html index 7de3865..4f4a96f 100644 --- a/maloja/web/common/footer.html +++ b/maloja/web/common/footer.html @@ -22,6 +22,6 @@ -
+
diff --git a/maloja/web/issues.html b/maloja/web/issues.html deleted file mode 100644 index 1a8bb9e..0000000 --- a/maloja/web/issues.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - Maloja - Issues - - - - - - - - - -
-
-
-

Possible Issues


- with your library -

KEY_ISSUES Issues

- -

Maloja can identify possible problems with consistency or redundancy in your library. After making any changes, you should rebuild your library.
- Your API key is required to make any changes to the server:

-
- - KEY_ISSUESLIST - - - - - diff --git a/maloja/web/issues.py b/maloja/web/issues.py deleted file mode 100644 index b5033d0..0000000 --- a/maloja/web/issues.py +++ /dev/null @@ -1,44 +0,0 @@ -import urllib -from .. import database -from ..htmlgenerators import artistLink - -def instructions(keys): - - db_data = database.issues() - i = 0 - - html = "" -# if db_data["inconsistent"]: -# html += "" -# html += "" -# html += """""" -# html += "" -# i += 1 - for d in db_data["duplicates"]: - html += "" - html += "" - html += """""" - html += """""" - html += "" - i += 1 - for c in db_data["combined"]: - html += "" - html += "" - html += """""" - html += "" - i += 1 - for n in db_data["newartists"]: - html += "" - html += "" - html += """""" - html += "" - i += 1 - - html += "
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.
Rebuild the database
'" + artistLink(d[0]) + "'" - html += " is a possible duplicate of " - html += "'" + artistLink(d[1]) + "'
""" + d[1] + """ is correct
""" + d[0] + """ is correct
'" + artistLink(c[0]) + "' sounds like the combination of " + str(len(c[1])) + " artists: " - for a in c[1]: - html += "'" + artistLink(a) + "' " - html += "
Fix it
Is '" + n[0] + "' in '" + artistLink(n[1]) + "' an artist?
Yes
" - - return ({"KEY_ISSUESLIST":html,"KEY_ISSUES":str(i)},[]) diff --git a/maloja/web/jinja/abstracts/admin.jinja b/maloja/web/jinja/abstracts/admin.jinja new file mode 100644 index 0000000..76c95ad --- /dev/null +++ b/maloja/web/jinja/abstracts/admin.jinja @@ -0,0 +1,49 @@ +{% extends "abstracts/base.jinja" %} +{% block title %}Maloja - Backend{% endblock %} + + +{% block content %} + + + + + + + + + +
+
+
+

Admin Panel

+
+ + {% if page=='admin_overview' %} + Overview + {% else %} + Overview + {% endif %} | + {% if page=='admin_setup' %} + Server Setup + {% else %} + Server Setup + {% endif %} | + {% if page=='admin_manual' %} + Manual Scrobbling + {% else %} + Manual Scrobbling + {% endif %} | + {% if page=='admin_issues' %} + Database Maintenance + {% else %} + Database Maintenance + {% endif %} + +

+ + +
+ + {% block maincontent %} + {% endblock %} +{% endblock %} diff --git a/maloja/web/jinja/base.jinja b/maloja/web/jinja/abstracts/base.jinja similarity index 85% rename from maloja/web/jinja/base.jinja rename to maloja/web/jinja/abstracts/base.jinja index 58d5aef..924a989 100644 --- a/maloja/web/jinja/base.jinja +++ b/maloja/web/jinja/abstracts/base.jinja @@ -20,6 +20,25 @@ {% block content %} + + + + + +
+
+
+

{% block heading %}{% endblock %}


+ + + {% block top_info %} + + {% endblock %} +
+ + {% block maincontent %} + + {% endblock %} {% endblock %} @@ -48,7 +67,7 @@
-
+
diff --git a/maloja/web/jinja/admin_issues.jinja b/maloja/web/jinja/admin_issues.jinja new file mode 100644 index 0000000..8dcabfb --- /dev/null +++ b/maloja/web/jinja/admin_issues.jinja @@ -0,0 +1,77 @@ +{% set page ='admin_issues' %} +{% extends "abstracts/admin.jinja" %} +{% block title %}Maloja - Issues{% endblock %} + + +{% block scripts %} + +{% endblock %} + +{% set issuedata = dbp.issues() %} + +{% block maincontent %} + + +

Maloja can identify possible problems with consistency or redundancy in your library. After making any changes, you should rebuild your library.

+ + + +{% if issuedata.inconsistent %} + + + + +{% endif %} + +{% for issue in issuedata.duplicates %} + + + + + +{% endfor %} + +{% for issue in issuedata.combined %} + + + + +{% endfor %} + +{% for issue in issuedata.newartists %} + + + + +{% endfor %} + +
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.
Rebuild the database
{{ htmlgenerators.artistLink(issue[0]) }} is a possible duplicate of {{ htmlgenerators.artistLink(issue[1]) }}
{{ issue[1] }} is correct
{{ issue[0] }} is correct
{{ artistLink(issue[0]) }} sounds like the combination of {{ len(issue[1]) }} artists: + {{ issue[1]|join(", ") }} +
Fix it
Is '{{ issue[0] }}' in '{{ htmlgenerators.artistLink(issue[1]) }}' an artist?
Yes
+ +{% endblock %} diff --git a/maloja/web/jinja/admin_manual.jinja b/maloja/web/jinja/admin_manual.jinja new file mode 100644 index 0000000..6815bd7 --- /dev/null +++ b/maloja/web/jinja/admin_manual.jinja @@ -0,0 +1,47 @@ +{% set page ='admin_manual' %} +{% extends "abstracts/admin.jinja" %} +{% block title %}Maloja - Manual Scrobbling{% endblock %} + +{% block scripts %} + +{% endblock %} + + +{% block maincontent %} + +

Scrobble new discovery

+ + + + + + + + +
+ Artists: + + +
+ Title: + + +
+ +
+ + Scrobble! + Last Manual Scrobble + +
+ + + +

Search

+ + +

+
+ + +{% endblock %} diff --git a/maloja/web/jinja/admin_overview.jinja b/maloja/web/jinja/admin_overview.jinja new file mode 100644 index 0000000..5c0d285 --- /dev/null +++ b/maloja/web/jinja/admin_overview.jinja @@ -0,0 +1,77 @@ +{% set page ='admin_overview' %} +{% extends "abstracts/admin.jinja" %} +{% block title %}Maloja - Admin Panel{% endblock %} + +{% block scripts %} + +{% endblock %} + + + {% block maincontent %} + +

Update

+ + Currently installed Maloja version: Loading...
+ Latest recommended Maloja version: Loading...
+ + + +

Admin Mode

+ + Admin Mode allows you to manually scrobble from various places on the web interface instead of just the dedicated page.

+ {% if adminmode %} + Deactivate + {% else %} + Activate + {% endif %} + +

Links

+ + Report Issue
+ Readme
+ PyPi
+ {% endblock %} diff --git a/maloja/web/jinja/admin_setup.jinja b/maloja/web/jinja/admin_setup.jinja new file mode 100644 index 0000000..b7c46c9 --- /dev/null +++ b/maloja/web/jinja/admin_setup.jinja @@ -0,0 +1,127 @@ +{% set page ='admin_setup' %} +{% extends "abstracts/admin.jinja" %} +{% block title %}Maloja - Setup{% endblock %} + +{% block scripts %} + + +{% endblock %} + +{% set rulesets = dbp.get_predefined_rulesets() %} + +{% block maincontent %} + + +

Start Scrobbling

+ + 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. +

+ You can also use any standard-compliant scrobbler. For GNUFM (audioscrobbler) scrobblers, enter yourserver.tld/api/s/audioscrobbler as your Gnukebox server and your API key as the password. For Listenbrainz scrobblers, use yourserver.tld/api/s/listenbrainz as the API URL and your API key as token. +

+ 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 + + yourserver.tld/api/newscrobble + + (make sure to use the public URL) with the key-value-pairs +
+
+ + + + + +
artist Artist String
title Title String
key API Key
seconds Duration of Scrobble - optional and currently not used
+

+ Finally, you could always manually scrobble! + +

+ +

Import your Last.FM data

+ + Switching from Last.fm? Download all your data and run the command maloja import (the file you just downloaded). +

+ +

Set up some rules

+ + After you've scrobbled for a bit, you might want to check the Issues page 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. +

+ + You can also set up some predefined rulesets right away! +
+ +

+ + + + + + + + {% for rs in rulesets %} + + {% if rs.active %} + + {% else %} + + {% endif %} + + + + + {% endfor %} +
ModuleAuthorDescription
Remove:Add:{{ rs.name }}{{ rs.author }}{{ rs.desc }}
+ +

+ +

Say thanks

+ + Donations are never required, but always appreciated. If you really like Maloja, you can fund my next Buttergipfel via + PayPal, Bitcoin or Flattr. + +

+ +

View your stats

+ + Done! Visit yourserver.tld (or your public / proxy URL) to look at your overview page. Almost everything is clickable! + +{% endblock %} diff --git a/maloja/web/jinja/artist.jinja b/maloja/web/jinja/artist.jinja index acc6cad..a1c8701 100644 --- a/maloja/web/jinja/artist.jinja +++ b/maloja/web/jinja/artist.jinja @@ -1,4 +1,4 @@ -{% extends "base.jinja" %} +{% extends "abstracts/base.jinja" %} {% block title %}Maloja - {{ artist }}{% endblock %} {% block scripts %} @@ -36,7 +36,7 @@ {% if adminmode %}
{% else %} diff --git a/maloja/web/jinja/charts_artists.jinja b/maloja/web/jinja/charts_artists.jinja index 4866901..5648ab7 100644 --- a/maloja/web/jinja/charts_artists.jinja +++ b/maloja/web/jinja/charts_artists.jinja @@ -1,4 +1,4 @@ -{% extends "base.jinja" %} +{% extends "abstracts/base.jinja" %} {% block title %}Maloja - {{ artist }}{% endblock %} {% block scripts %} diff --git a/maloja/web/jinja/charts_tracks.jinja b/maloja/web/jinja/charts_tracks.jinja index b25b11e..cb31f59 100644 --- a/maloja/web/jinja/charts_tracks.jinja +++ b/maloja/web/jinja/charts_tracks.jinja @@ -1,4 +1,4 @@ -{% extends "base.jinja" %} +{% extends "abstracts/base.jinja" %} {% block title %}Maloja - Track Charts{% endblock %} {% block scripts %} diff --git a/maloja/web/jinja/error.jinja b/maloja/web/jinja/error.jinja new file mode 100644 index 0000000..3fe843b --- /dev/null +++ b/maloja/web/jinja/error.jinja @@ -0,0 +1,18 @@ +{% extends "abstracts/base.jinja" %} + +{% block content %} + + + + + +
+
+
+

Error {{ errorcode }}


+ + +

That did not work. Don't ask me why.

+
+ +{% endblock %} diff --git a/maloja/web/jinja/track.jinja b/maloja/web/jinja/track.jinja index 0dcefae..d8edd89 100644 --- a/maloja/web/jinja/track.jinja +++ b/maloja/web/jinja/track.jinja @@ -1,11 +1,11 @@ -{% extends "base.jinja" %} +{% extends "abstracts/base.jinja" %} {% block title %}Maloja - {{ track.title }}{% endblock %} {% block scripts %} {% endblock %} @@ -30,7 +30,7 @@ {% if adminmode %}
{% else %} @@ -45,7 +45,10 @@ #{{ info.position }}
-

{{ info['scrobbles'] }} Scrobbles

+

+ {% if adminmode %}Scrobble now{% endif %} + {{ info['scrobbles'] }} Scrobbles +

diff --git a/maloja/web/manual.html b/maloja/web/manual.html deleted file mode 100644 index e880d5c..0000000 --- a/maloja/web/manual.html +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - Maloja - - - - - - - - - - - - - - - - -
-
-
-

Manual Scrobbling


-

- API Key:

- - -
- -

Scrobble new discovery

- - - - - - - - -
- Artists: - - -
- Title: - - -
- -
- - Scrobble! - -
- - - -

Search

- - -

-
- - - - - - - - - diff --git a/maloja/web/pyhp/admin.pyhp b/maloja/web/pyhp/admin.pyhp deleted file mode 100644 index 61ff121..0000000 --- a/maloja/web/pyhp/admin.pyhp +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - Maloja - - - - - - - - - - - - - - - - - -
-
-
-

Admin Panel


-

- API Key:

- -
- -

Update

- - Currently installed Maloja version: Loading...
- Latest recommended Maloja version: Loading...
- - - -

Admin Mode

- - Admin Mode allows you to manually scrobble from various places on the web interface instead of just the dedicated page.

- Deactivate - Activate - -

Links

- - Server Setup
- Manual Scrobbling
- Database Maintenance - -

External

- - Report Issue
- - - - diff --git a/maloja/web/pyhp/errors/generic.pyhp b/maloja/web/pyhp/errors/generic.pyhp deleted file mode 100644 index aafa2e1..0000000 --- a/maloja/web/pyhp/errors/generic.pyhp +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - Maloja - Error <pyhp echo="errorcode" /> - - - - - - - - - -
-
-
-

Error


- - -

That did not work. Don't ask me why.

-
- - - - - diff --git a/maloja/web/setup.html b/maloja/web/setup.html deleted file mode 100644 index fed262d..0000000 --- a/maloja/web/setup.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - Maloja - Setup - - - - - - - - - - - - - - -
-
-
-

Maloja


- -

Welcome to your own Maloja server!

-
- -

Start Scrobbling

- - 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. -

- You can also use any standard-compliant scrobbler. For GNUFM (audioscrobbler) scrobblers, enter yourserver.tld/api/s/audioscrobbler as your Gnukebox server and your API key as the password. For Listenbrainz scrobblers, use yourserver.tld/api/s/listenbrainz as the API URL and your API key as token. -

- 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 - - yourserver.tld/api/newscrobble - - (make sure to use the public URL) with the key-value-pairs -
-
- - - - - -
artist Artist String
title Title String
key API Key
seconds Duration of Scrobble - optional and currently not used
-

- Finally, you could always manually scrobble! - -

- -

Import your Last.FM data

- - Switching from Last.fm? Download all your data and run the command maloja import (the file you just downloaded). -

- -

Set up some rules

- - After you've scrobbled for a bit, you might want to check the Issues page 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. -

- - You can also set up some predefined rulesets right away! Enter your API key and click the buttons. -
- API Key: - - -

- KEY_PREDEFINED_RULESETS - -

- -

Say thanks

- - Coding open source projects is fun, but not really monetizable. If you like Maloja, I would appreciate a small donation via - PayPal, Bitcoin or Flattr. - -

- -

View your stats

- - Done! Visit yourserver.tld (or your public / proxy URL) to look at your overview page. Almost everything is clickable! - - - - - diff --git a/maloja/web/setup.py b/maloja/web/setup.py deleted file mode 100644 index 596a3c1..0000000 --- a/maloja/web/setup.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -from ..globalconf import datadir - -def instructions(keys): - - html = "" - - html += "" - - - 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 += "" - - if os.path.exists(datadir("rules",f)): - html += "" - else: - html += "" - html += "" - html += "" - html += "" - - html += "" - html += "
ModuleAuthorDescription
Remove:Add:" + name + "" + author + "" + desc + "
" - - - pushresources = [] - replace = {"KEY_PREDEFINED_RULESETS":html} - return (replace,pushresources)