mirror of
https://github.com/krateng/maloja.git
synced 2023-08-10 21:12:55 +03:00
Merge branch 'feature-webedit' into feature/reparse-scrobble
This commit is contained in:
commit
495627f3f7
4
dev/releases/branch.yml
Normal file
4
dev/releases/branch.yml
Normal file
@ -0,0 +1,4 @@
|
||||
- "[Architecture] Cleaned up legacy process control"
|
||||
- "[Architecture] Added proper exception framework to native API"
|
||||
- "[Feature] Implemented track title and artist name editing from web interface"
|
||||
- "[Feature] Implemented track and artist merging from web interface"
|
@ -1,4 +1,4 @@
|
||||
# monkey patching
|
||||
from . import monkey
|
||||
from .pkg_global import monkey
|
||||
# configuration before all else
|
||||
from . import globalconf
|
||||
from .pkg_global import conf
|
||||
|
@ -1,4 +1,163 @@
|
||||
# make the package itself runnable with python -m maloja
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
from .proccontrol.control import main
|
||||
main()
|
||||
from setproctitle import setproctitle
|
||||
from ipaddress import ip_address
|
||||
|
||||
from doreah.control import mainfunction
|
||||
from doreah.io import col
|
||||
from doreah.logging import log
|
||||
|
||||
from . import __pkginfo__ as pkginfo
|
||||
from .pkg_global import conf
|
||||
from .proccontrol import tasks
|
||||
from .setup import setup
|
||||
from .dev import generate
|
||||
|
||||
|
||||
|
||||
def print_header_info():
|
||||
print()
|
||||
#print("#####")
|
||||
print(col['yellow']("Maloja"),f"v{pkginfo.VERSION}")
|
||||
print(pkginfo.HOMEPAGE)
|
||||
#print("#####")
|
||||
print()
|
||||
|
||||
|
||||
|
||||
def get_instance():
|
||||
try:
|
||||
return int(subprocess.check_output(["pidof","maloja"]))
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_instance_supervisor():
|
||||
try:
|
||||
return int(subprocess.check_output(["pidof","maloja_supervisor"]))
|
||||
except:
|
||||
return None
|
||||
|
||||
def restart():
|
||||
stop()
|
||||
start()
|
||||
|
||||
|
||||
def start():
|
||||
if get_instance_supervisor() is not None:
|
||||
print("Maloja is already running.")
|
||||
else:
|
||||
print_header_info()
|
||||
setup()
|
||||
try:
|
||||
#p = subprocess.Popen(["python3","-m","maloja.server"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
sp = subprocess.Popen(["python3","-m","maloja","supervisor"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
print(col["green"]("Maloja started!"))
|
||||
|
||||
port = conf.malojaconfig["PORT"]
|
||||
|
||||
print("Visit your server address (Port " + str(port) + ") to see your web interface. Visit /admin_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) + "/admin_setup"))
|
||||
return True
|
||||
except:
|
||||
print("Error while starting Maloja.")
|
||||
return False
|
||||
|
||||
|
||||
def stop():
|
||||
|
||||
pid_sv = get_instance_supervisor()
|
||||
if pid_sv is not None:
|
||||
os.kill(pid_sv,signal.SIGTERM)
|
||||
|
||||
pid = get_instance()
|
||||
if pid is not None:
|
||||
os.kill(pid,signal.SIGTERM)
|
||||
|
||||
if pid is None and pid_sv is None:
|
||||
return False
|
||||
|
||||
print("Maloja stopped!")
|
||||
return True
|
||||
|
||||
def onlysetup():
|
||||
print_header_info()
|
||||
setup()
|
||||
print("Setup complete!")
|
||||
|
||||
def run_server():
|
||||
print_header_info()
|
||||
setup()
|
||||
setproctitle("maloja")
|
||||
from . import server
|
||||
server.run_server()
|
||||
|
||||
def run_supervisor():
|
||||
setproctitle("maloja_supervisor")
|
||||
while True:
|
||||
log("Maloja is not running, starting...",module="supervisor")
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
["python3", "-m", "maloja","run"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except Exception as e:
|
||||
log("Error starting Maloja: " + str(e),module="supervisor")
|
||||
else:
|
||||
try:
|
||||
process.wait()
|
||||
except Exception as e:
|
||||
log("Maloja crashed: " + str(e),module="supervisor")
|
||||
|
||||
def debug():
|
||||
os.environ["MALOJA_DEV_MODE"] = 'true'
|
||||
conf.malojaconfig.load_environment()
|
||||
direct()
|
||||
|
||||
def print_info():
|
||||
print_header_info()
|
||||
print(col['lightblue']("Configuration Directory:"),conf.dir_settings['config'])
|
||||
print(col['lightblue']("Data Directory: "),conf.dir_settings['state'])
|
||||
print(col['lightblue']("Log Directory: "),conf.dir_settings['logs'])
|
||||
print(col['lightblue']("Network: "),f"IPv{ip_address(conf.malojaconfig['host']).version}, Port {conf.malojaconfig['port']}")
|
||||
print(col['lightblue']("Timezone: "),f"UTC{conf.malojaconfig['timezone']:+d}")
|
||||
print()
|
||||
print()
|
||||
|
||||
@mainfunction({"l":"level","v":"version","V":"version"},flags=['version','include_images'],shield=True)
|
||||
def main(*args,**kwargs):
|
||||
|
||||
actions = {
|
||||
# server
|
||||
"start":start,
|
||||
"restart":restart,
|
||||
"stop":stop,
|
||||
"run":run_server,
|
||||
"supervisor":run_supervisor,
|
||||
"debug":debug,
|
||||
"setup":onlysetup,
|
||||
# admin scripts
|
||||
"import":tasks.import_scrobbles, # maloja import /x/y.csv
|
||||
"backup":tasks.backup, # maloja backup --targetfolder /x/y --include_images
|
||||
"generate":generate.generate_scrobbles, # maloja generate 400
|
||||
"export":tasks.export, # maloja export
|
||||
# aux
|
||||
"info":print_info
|
||||
}
|
||||
|
||||
if "version" in kwargs:
|
||||
print(info.VERSION)
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
action, *args = args
|
||||
action = actions[action]
|
||||
except (ValueError, KeyError):
|
||||
print("Valid commands: " + " ".join(a for a in actions))
|
||||
return False
|
||||
|
||||
return action(*args,**kwargs)
|
||||
|
@ -4,7 +4,7 @@
|
||||
from doreah.keystore import KeyStore
|
||||
from doreah.logging import log
|
||||
|
||||
from ..globalconf import data_dir
|
||||
from ..pkg_global.conf import data_dir
|
||||
|
||||
apikeystore = KeyStore(file=data_dir['clients']("apikeys.yml"),save_endpoint="/apis/mlj_1/apikeys")
|
||||
|
||||
|
@ -4,7 +4,7 @@ from .. import database
|
||||
import datetime
|
||||
from ._apikeys import apikeystore
|
||||
|
||||
from ..globalconf import malojaconfig
|
||||
from ..pkg_global.conf import malojaconfig
|
||||
|
||||
|
||||
class Listenbrainz(APIHandler):
|
||||
|
@ -12,7 +12,7 @@ from nimrodel import Multi
|
||||
|
||||
|
||||
from .. import database
|
||||
from ..globalconf import malojaconfig, data_dir
|
||||
from ..pkg_global.conf import malojaconfig, data_dir
|
||||
|
||||
|
||||
|
||||
@ -44,7 +44,15 @@ errors = {
|
||||
"error":{
|
||||
'type':'missing_scrobble_data',
|
||||
'value':e.params,
|
||||
'desc':"A scrobble requires these parameters."
|
||||
'desc':"The scrobble is missing needed parameters."
|
||||
}
|
||||
}),
|
||||
database.exceptions.EntityExists: lambda e: (409,{
|
||||
"status":"failure",
|
||||
"error":{
|
||||
'type':'entity_exists',
|
||||
'value':e.entitydict,
|
||||
'desc':"This entity already exists in the database."
|
||||
}
|
||||
}),
|
||||
Exception: lambda e: (500,{
|
||||
@ -57,6 +65,20 @@ errors = {
|
||||
})
|
||||
}
|
||||
|
||||
def catch_exceptions(func):
|
||||
def protector(*args,**kwargs):
|
||||
try:
|
||||
return func(*args,**kwargs)
|
||||
except Exception as e:
|
||||
for etype in errors:
|
||||
if isinstance(e,etype):
|
||||
errorhandling = errors[etype](e)
|
||||
response.status = errorhandling[0]
|
||||
return errorhandling[1]
|
||||
|
||||
protector.__doc__ = func.__doc__
|
||||
protector.__annotations__ = func.__annotations__
|
||||
return protector
|
||||
|
||||
|
||||
def add_common_args_to_docstring(filterkeys=False,limitkeys=False,delimitkeys=False,amountkeys=False):
|
||||
@ -94,6 +116,7 @@ def add_common_args_to_docstring(filterkeys=False,limitkeys=False,delimitkeys=Fa
|
||||
|
||||
|
||||
@api.get("test")
|
||||
@catch_exceptions
|
||||
def test_server(key=None):
|
||||
"""Pings the server. If an API key is supplied, the server will respond with 200
|
||||
if the key is correct and 403 if it isn't. If no key is supplied, the server will
|
||||
@ -119,6 +142,7 @@ def test_server(key=None):
|
||||
|
||||
|
||||
@api.get("serverinfo")
|
||||
@catch_exceptions
|
||||
def server_info():
|
||||
"""Returns basic information about the server.
|
||||
|
||||
@ -141,6 +165,7 @@ def server_info():
|
||||
|
||||
|
||||
@api.get("scrobbles")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True,amountkeys=True)
|
||||
def get_scrobbles_external(**keys):
|
||||
"""Returns a list of scrobbles.
|
||||
@ -163,6 +188,7 @@ def get_scrobbles_external(**keys):
|
||||
|
||||
|
||||
@api.get("numscrobbles")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True,amountkeys=True)
|
||||
def get_scrobbles_num_external(**keys):
|
||||
"""Returns amount of scrobbles.
|
||||
@ -182,6 +208,7 @@ def get_scrobbles_num_external(**keys):
|
||||
|
||||
|
||||
@api.get("tracks")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True)
|
||||
def get_tracks_external(**keys):
|
||||
"""Returns all tracks (optionally of an artist).
|
||||
@ -201,6 +228,7 @@ def get_tracks_external(**keys):
|
||||
|
||||
|
||||
@api.get("artists")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring()
|
||||
def get_artists_external():
|
||||
"""Returns all artists.
|
||||
@ -218,6 +246,7 @@ def get_artists_external():
|
||||
|
||||
|
||||
@api.get("charts/artists")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(limitkeys=True)
|
||||
def get_charts_artists_external(**keys):
|
||||
"""Returns artist charts
|
||||
@ -236,6 +265,7 @@ def get_charts_artists_external(**keys):
|
||||
|
||||
|
||||
@api.get("charts/tracks")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True)
|
||||
def get_charts_tracks_external(**keys):
|
||||
"""Returns track charts
|
||||
@ -255,6 +285,7 @@ def get_charts_tracks_external(**keys):
|
||||
|
||||
|
||||
@api.get("pulse")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True,delimitkeys=True,amountkeys=True)
|
||||
def get_pulse_external(**keys):
|
||||
"""Returns amounts of scrobbles in specified time frames
|
||||
@ -274,6 +305,7 @@ def get_pulse_external(**keys):
|
||||
|
||||
|
||||
@api.get("performance")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True,limitkeys=True,delimitkeys=True,amountkeys=True)
|
||||
def get_performance_external(**keys):
|
||||
"""Returns artist's or track's rank in specified time frames
|
||||
@ -293,6 +325,7 @@ def get_performance_external(**keys):
|
||||
|
||||
|
||||
@api.get("top/artists")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(limitkeys=True,delimitkeys=True)
|
||||
def get_top_artists_external(**keys):
|
||||
"""Returns respective number 1 artists in specified time frames
|
||||
@ -312,6 +345,7 @@ def get_top_artists_external(**keys):
|
||||
|
||||
|
||||
@api.get("top/tracks")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(limitkeys=True,delimitkeys=True)
|
||||
def get_top_tracks_external(**keys):
|
||||
"""Returns respective number 1 tracks in specified time frames
|
||||
@ -333,6 +367,7 @@ def get_top_tracks_external(**keys):
|
||||
|
||||
|
||||
@api.get("artistinfo")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True)
|
||||
def artist_info_external(**keys):
|
||||
"""Returns information about an artist
|
||||
@ -347,6 +382,7 @@ def artist_info_external(**keys):
|
||||
|
||||
|
||||
@api.get("trackinfo")
|
||||
@catch_exceptions
|
||||
@add_common_args_to_docstring(filterkeys=True)
|
||||
def track_info_external(artist:Multi[str],**keys):
|
||||
"""Returns information about a track
|
||||
@ -365,6 +401,7 @@ def track_info_external(artist:Multi[str],**keys):
|
||||
|
||||
@api.post("newscrobble")
|
||||
@authenticated_function(alternate=api_key_correct,api=True,pass_auth_result_as='auth_result')
|
||||
@catch_exceptions
|
||||
def post_scrobble(
|
||||
artist:Multi=None,
|
||||
artists:list=[],
|
||||
@ -406,46 +443,40 @@ def post_scrobble(
|
||||
# for logging purposes, don't pass values that we didn't actually supply
|
||||
rawscrobble = {k:rawscrobble[k] for k in rawscrobble if rawscrobble[k]}
|
||||
|
||||
try:
|
||||
result = database.incoming_scrobble(
|
||||
rawscrobble,
|
||||
client='browser' if auth_result.get('doreah_native_auth_check') else auth_result.get('client'),
|
||||
api='native/v1',
|
||||
fix=(nofix is None)
|
||||
)
|
||||
|
||||
responsedict = {
|
||||
'status': 'success',
|
||||
'track': {
|
||||
'artists':result['track']['artists'],
|
||||
'title':result['track']['title']
|
||||
}
|
||||
result = database.incoming_scrobble(
|
||||
rawscrobble,
|
||||
client='browser' if auth_result.get('doreah_native_auth_check') else auth_result.get('client'),
|
||||
api='native/v1',
|
||||
fix=(nofix is None)
|
||||
)
|
||||
|
||||
responsedict = {
|
||||
'status': 'success',
|
||||
'track': {
|
||||
'artists':result['track']['artists'],
|
||||
'title':result['track']['title']
|
||||
}
|
||||
if extra_kwargs:
|
||||
responsedict['warnings'] = [
|
||||
{'type':'invalid_keyword_ignored','value':k,
|
||||
'desc':"This key was not recognized by the server and has been discarded."}
|
||||
for k in extra_kwargs
|
||||
]
|
||||
if artist and artists:
|
||||
responsedict['warnings'] = [
|
||||
{'type':'mixed_schema','value':['artist','artists'],
|
||||
'desc':"These two fields are meant as alternative methods to submit information. Use of both is discouraged, but works at the moment."}
|
||||
]
|
||||
return responsedict
|
||||
except Exception as e:
|
||||
for etype in errors:
|
||||
if isinstance(e,etype):
|
||||
errorhandling = errors[etype](e)
|
||||
response.status = errorhandling[0]
|
||||
return errorhandling[1]
|
||||
|
||||
}
|
||||
if extra_kwargs:
|
||||
responsedict['warnings'] = [
|
||||
{'type':'invalid_keyword_ignored','value':k,
|
||||
'desc':"This key was not recognized by the server and has been discarded."}
|
||||
for k in extra_kwargs
|
||||
]
|
||||
if artist and artists:
|
||||
responsedict['warnings'] = [
|
||||
{'type':'mixed_schema','value':['artist','artists'],
|
||||
'desc':"These two fields are meant as alternative methods to submit information. Use of both is discouraged, but works at the moment."}
|
||||
]
|
||||
return responsedict
|
||||
|
||||
|
||||
|
||||
|
||||
@api.post("importrules")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def import_rulemodule(**keys):
|
||||
"""Internal Use Only"""
|
||||
filename = keys.get("filename")
|
||||
@ -464,6 +495,7 @@ def import_rulemodule(**keys):
|
||||
|
||||
@api.post("rebuild")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def rebuild(**keys):
|
||||
"""Internal Use Only"""
|
||||
log("Database rebuild initiated!")
|
||||
@ -480,6 +512,7 @@ def rebuild(**keys):
|
||||
|
||||
|
||||
@api.get("search")
|
||||
@catch_exceptions
|
||||
def search(**keys):
|
||||
"""Internal Use Only"""
|
||||
query = keys.get("query")
|
||||
@ -519,6 +552,7 @@ def search(**keys):
|
||||
|
||||
@api.post("addpicture")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def add_picture(b64,artist:Multi=[],title=None):
|
||||
"""Internal Use Only"""
|
||||
keys = FormsDict()
|
||||
@ -532,6 +566,7 @@ def add_picture(b64,artist:Multi=[],title=None):
|
||||
|
||||
@api.post("newrule")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def newrule(**keys):
|
||||
"""Internal Use Only"""
|
||||
pass
|
||||
@ -542,18 +577,21 @@ def newrule(**keys):
|
||||
|
||||
@api.post("settings")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def set_settings(**keys):
|
||||
"""Internal Use Only"""
|
||||
malojaconfig.update(keys)
|
||||
|
||||
@api.post("apikeys")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def set_apikeys(**keys):
|
||||
"""Internal Use Only"""
|
||||
apikeystore.update(keys)
|
||||
|
||||
@api.post("import")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def import_scrobbles(identifier):
|
||||
"""Internal Use Only"""
|
||||
from ..thirdparty import import_scrobbles
|
||||
@ -561,6 +599,7 @@ def import_scrobbles(identifier):
|
||||
|
||||
@api.get("backup")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def get_backup(**keys):
|
||||
"""Internal Use Only"""
|
||||
from ..proccontrol.tasks.backup import backup
|
||||
@ -573,6 +612,7 @@ def get_backup(**keys):
|
||||
|
||||
@api.get("export")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def get_export(**keys):
|
||||
"""Internal Use Only"""
|
||||
from ..proccontrol.tasks.export import export
|
||||
@ -586,12 +626,58 @@ def get_export(**keys):
|
||||
|
||||
@api.post("delete_scrobble")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def delete_scrobble(timestamp):
|
||||
"""Internal Use Only"""
|
||||
database.remove_scrobble(timestamp)
|
||||
result = database.remove_scrobble(timestamp)
|
||||
return {
|
||||
"status":"success"
|
||||
}
|
||||
|
||||
|
||||
@api.post("edit_artist")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def edit_artist(id,name):
|
||||
"""Internal Use Only"""
|
||||
result = database.edit_artist(id,name)
|
||||
return {
|
||||
"status":"success"
|
||||
}
|
||||
|
||||
@api.post("edit_track")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def edit_track(id,title):
|
||||
"""Internal Use Only"""
|
||||
result = database.edit_track(id,{'title':title})
|
||||
return {
|
||||
"status":"success"
|
||||
}
|
||||
|
||||
|
||||
@api.post("merge_tracks")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def merge_tracks(target_id,source_ids):
|
||||
"""Internal Use Only"""
|
||||
result = database.merge_tracks(target_id,source_ids)
|
||||
return {
|
||||
"status":"success"
|
||||
}
|
||||
|
||||
@api.post("merge_artists")
|
||||
@authenticated_function(api=True)
|
||||
@catch_exceptions
|
||||
def merge_artists(target_id,source_ids):
|
||||
"""Internal Use Only"""
|
||||
result = database.merge_artists(target_id,source_ids)
|
||||
return {
|
||||
"status":"success"
|
||||
}
|
||||
|
||||
@api.post("reparse_scrobble")
|
||||
@authenticated_function(api=True)
|
||||
def reparse_scrobble(timestamp):
|
||||
"""Internal Use Only"""
|
||||
database.reparse_scrobble(timestamp)
|
||||
database.reparse_scrobble(timestamp)
|
@ -2,7 +2,7 @@ import re
|
||||
import os
|
||||
import csv
|
||||
|
||||
from .globalconf import data_dir, malojaconfig
|
||||
from .pkg_global.conf import data_dir, malojaconfig
|
||||
|
||||
# need to do this as a class so it can retain loaded settings from file
|
||||
# apparently this is not true
|
||||
|
@ -7,7 +7,7 @@ from .. import images
|
||||
from ..malojatime import register_scrobbletime, time_stamps, ranges, alltime
|
||||
from ..malojauri import uri_to_internal, internal_to_uri, compose_querystring
|
||||
from ..thirdparty import proxy_scrobble_all
|
||||
from ..globalconf import data_dir, malojaconfig
|
||||
from ..pkg_global.conf import data_dir, malojaconfig
|
||||
from ..apis import apikeystore
|
||||
#db
|
||||
from . import sqldb
|
||||
@ -167,9 +167,47 @@ def remove_scrobble(timestamp):
|
||||
result = sqldb.delete_scrobble(timestamp)
|
||||
dbcache.invalidate_caches(timestamp)
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def edit_artist(id,artistinfo):
|
||||
artist = sqldb.get_artist(id)
|
||||
log(f"Renaming {artist} to {artistinfo}")
|
||||
result = sqldb.edit_artist(id,artistinfo)
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def edit_track(id,trackinfo):
|
||||
track = sqldb.get_track(id)
|
||||
log(f"Renaming {track['title']} to {trackinfo['title']}")
|
||||
result = sqldb.edit_track(id,trackinfo)
|
||||
dbcache.invalidate_entity_cache()
|
||||
dbcache.invalidate_caches()
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def merge_artists(target_id,source_ids):
|
||||
sources = [sqldb.get_artist(id) for id in source_ids]
|
||||
target = sqldb.get_artist(target_id)
|
||||
log(f"Merging {sources} into {target}")
|
||||
result = sqldb.merge_artists(target_id,source_ids)
|
||||
dbcache.invalidate_entity_cache()
|
||||
|
||||
return result
|
||||
|
||||
@waitfordb
|
||||
def merge_tracks(target_id,source_ids):
|
||||
sources = [sqldb.get_track(id) for id in source_ids]
|
||||
target = sqldb.get_track(target_id)
|
||||
log(f"Merging {sources} into {target}")
|
||||
result = sqldb.merge_tracks(target_id,source_ids)
|
||||
dbcache.invalidate_entity_cache()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@ -304,7 +342,8 @@ def artist_info(dbconn=None,**keys):
|
||||
|
||||
artist = keys.get('artist')
|
||||
|
||||
artist = sqldb.get_artist(sqldb.get_artist_id(artist,dbconn=dbconn),dbconn=dbconn)
|
||||
artist_id = sqldb.get_artist_id(artist,dbconn=dbconn)
|
||||
artist = sqldb.get_artist(artist_id,dbconn=dbconn)
|
||||
alltimecharts = get_charts_artists(timerange=alltime(),dbconn=dbconn)
|
||||
scrobbles = get_scrobbles_num(artist=artist,timerange=alltime(),dbconn=dbconn)
|
||||
#we cant take the scrobble number from the charts because that includes all countas scrobbles
|
||||
@ -318,11 +357,12 @@ def artist_info(dbconn=None,**keys):
|
||||
"position":position,
|
||||
"associated":others,
|
||||
"medals":{
|
||||
"gold": [year for year in cached.medals_artists if artist in cached.medals_artists[year]['gold']],
|
||||
"silver": [year for year in cached.medals_artists if artist in cached.medals_artists[year]['silver']],
|
||||
"bronze": [year for year in cached.medals_artists if artist in cached.medals_artists[year]['bronze']],
|
||||
"gold": [year for year in cached.medals_artists if artist_id in cached.medals_artists[year]['gold']],
|
||||
"silver": [year for year in cached.medals_artists if artist_id in cached.medals_artists[year]['silver']],
|
||||
"bronze": [year for year in cached.medals_artists if artist_id in cached.medals_artists[year]['bronze']],
|
||||
},
|
||||
"topweeks":len([e for e in cached.weekly_topartists if e == artist])
|
||||
"topweeks":len([e for e in cached.weekly_topartists if e == artist_id]),
|
||||
"id":artist_id
|
||||
}
|
||||
except:
|
||||
# if the artist isnt in the charts, they are not being credited and we
|
||||
@ -340,7 +380,8 @@ def track_info(dbconn=None,**keys):
|
||||
|
||||
track = keys.get('track')
|
||||
|
||||
track = sqldb.get_track(sqldb.get_track_id(track,dbconn=dbconn),dbconn=dbconn)
|
||||
track_id = sqldb.get_track_id(track,dbconn=dbconn)
|
||||
track = sqldb.get_track(track_id,dbconn=dbconn)
|
||||
alltimecharts = get_charts_tracks(timerange=alltime(),dbconn=dbconn)
|
||||
#scrobbles = get_scrobbles_num(track=track,timerange=alltime())
|
||||
|
||||
@ -359,12 +400,13 @@ def track_info(dbconn=None,**keys):
|
||||
"scrobbles":scrobbles,
|
||||
"position":position,
|
||||
"medals":{
|
||||
"gold": [year for year in cached.medals_tracks if track in cached.medals_tracks[year]['gold']],
|
||||
"silver": [year for year in cached.medals_tracks if track in cached.medals_tracks[year]['silver']],
|
||||
"bronze": [year for year in cached.medals_tracks if track in cached.medals_tracks[year]['bronze']],
|
||||
"gold": [year for year in cached.medals_tracks if track_id in cached.medals_tracks[year]['gold']],
|
||||
"silver": [year for year in cached.medals_tracks if track_id in cached.medals_tracks[year]['silver']],
|
||||
"bronze": [year for year in cached.medals_tracks if track_id in cached.medals_tracks[year]['bronze']],
|
||||
},
|
||||
"certification":cert,
|
||||
"topweeks":len([e for e in cached.weekly_toptracks if e == track])
|
||||
"topweeks":len([e for e in cached.weekly_toptracks if e == track_id]),
|
||||
"id":track_id
|
||||
}
|
||||
|
||||
|
||||
|
@ -8,7 +8,7 @@ import csv
|
||||
import os
|
||||
|
||||
from . import sqldb
|
||||
from ..globalconf import data_dir
|
||||
from ..pkg_global.conf import data_dir
|
||||
|
||||
|
||||
def load_associated_rules():
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
from doreah.regular import runyearly, rundaily
|
||||
from .. import database
|
||||
from . import sqldb
|
||||
from .. import malojatime as mjt
|
||||
|
||||
|
||||
@ -24,27 +25,29 @@ def update_medals():
|
||||
medals_artists.clear()
|
||||
medals_tracks.clear()
|
||||
|
||||
for year in mjt.ranges(step="year"):
|
||||
if year == mjt.thisyear(): break
|
||||
with sqldb.engine.begin() as conn:
|
||||
for year in mjt.ranges(step="year"):
|
||||
if year == mjt.thisyear(): break
|
||||
|
||||
charts_artists = database.get_charts_artists(timerange=year)
|
||||
charts_tracks = database.get_charts_tracks(timerange=year)
|
||||
charts_artists = sqldb.count_scrobbles_by_artist(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=conn)
|
||||
charts_tracks = sqldb.count_scrobbles_by_track(since=year.first_stamp(),to=year.last_stamp(),resolve_ids=False,dbconn=conn)
|
||||
|
||||
entry_artists = {'gold':[],'silver':[],'bronze':[]}
|
||||
entry_tracks = {'gold':[],'silver':[],'bronze':[]}
|
||||
medals_artists[year.desc()] = entry_artists
|
||||
medals_tracks[year.desc()] = entry_tracks
|
||||
entry_artists = {'gold':[],'silver':[],'bronze':[]}
|
||||
entry_tracks = {'gold':[],'silver':[],'bronze':[]}
|
||||
medals_artists[year.desc()] = entry_artists
|
||||
medals_tracks[year.desc()] = entry_tracks
|
||||
|
||||
for entry in charts_artists:
|
||||
if entry['rank'] == 1: entry_artists['gold'].append(entry['artist_id'])
|
||||
elif entry['rank'] == 2: entry_artists['silver'].append(entry['artist_id'])
|
||||
elif entry['rank'] == 3: entry_artists['bronze'].append(entry['artist_id'])
|
||||
else: break
|
||||
for entry in charts_tracks:
|
||||
if entry['rank'] == 1: entry_tracks['gold'].append(entry['track_id'])
|
||||
elif entry['rank'] == 2: entry_tracks['silver'].append(entry['track_id'])
|
||||
elif entry['rank'] == 3: entry_tracks['bronze'].append(entry['track_id'])
|
||||
else: break
|
||||
|
||||
for entry in charts_artists:
|
||||
if entry['rank'] == 1: entry_artists['gold'].append(entry['artist'])
|
||||
elif entry['rank'] == 2: entry_artists['silver'].append(entry['artist'])
|
||||
elif entry['rank'] == 3: entry_artists['bronze'].append(entry['artist'])
|
||||
else: break
|
||||
for entry in charts_tracks:
|
||||
if entry['rank'] == 1: entry_tracks['gold'].append(entry['track'])
|
||||
elif entry['rank'] == 2: entry_tracks['silver'].append(entry['track'])
|
||||
elif entry['rank'] == 3: entry_tracks['bronze'].append(entry['track'])
|
||||
else: break
|
||||
|
||||
|
||||
|
||||
@ -55,15 +58,17 @@ def update_weekly():
|
||||
weekly_topartists.clear()
|
||||
weekly_toptracks.clear()
|
||||
|
||||
for week in mjt.ranges(step="week"):
|
||||
if week == mjt.thisweek(): break
|
||||
with sqldb.engine.begin() as conn:
|
||||
for week in mjt.ranges(step="week"):
|
||||
if week == mjt.thisweek(): break
|
||||
|
||||
charts_artists = database.get_charts_artists(timerange=week)
|
||||
charts_tracks = database.get_charts_tracks(timerange=week)
|
||||
|
||||
for entry in charts_artists:
|
||||
if entry['rank'] == 1: weekly_topartists.append(entry['artist'])
|
||||
else: break
|
||||
for entry in charts_tracks:
|
||||
if entry['rank'] == 1: weekly_toptracks.append(entry['track'])
|
||||
else: break
|
||||
charts_artists = sqldb.count_scrobbles_by_artist(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=conn)
|
||||
charts_tracks = sqldb.count_scrobbles_by_track(since=week.first_stamp(),to=week.last_stamp(),resolve_ids=False,dbconn=conn)
|
||||
|
||||
for entry in charts_artists:
|
||||
if entry['rank'] == 1: weekly_topartists.append(entry['artist_id'])
|
||||
else: break
|
||||
for entry in charts_tracks:
|
||||
if entry['rank'] == 1: weekly_toptracks.append(entry['track_id'])
|
||||
else: break
|
||||
|
@ -8,7 +8,7 @@ import json
|
||||
from doreah.regular import runhourly
|
||||
from doreah.logging import log
|
||||
|
||||
from ..globalconf import malojaconfig
|
||||
from ..pkg_global.conf import malojaconfig
|
||||
|
||||
HIGH_NUMBER = 1000000
|
||||
CACHE_SIZE = 10000
|
||||
@ -94,12 +94,12 @@ def cached_wrapper_individual(inner_func):
|
||||
|
||||
return outer_func
|
||||
|
||||
def invalidate_caches(scrobbletime):
|
||||
def invalidate_caches(scrobbletime=None):
|
||||
if malojaconfig['USE_GLOBAL_CACHE']:
|
||||
cleared, kept = 0, 0
|
||||
for k in cache.keys():
|
||||
# VERY BIG TODO: differentiate between None as in 'unlimited timerange' and None as in 'time doesnt matter here'!
|
||||
if (k[3] is None or scrobbletime >= k[3]) and (k[4] is None or scrobbletime <= k[4]):
|
||||
if scrobbletime is None or (k[3] is None or scrobbletime >= k[3]) and (k[4] is None or scrobbletime <= k[4]):
|
||||
cleared += 1
|
||||
del cache[k]
|
||||
else:
|
||||
|
10
maloja/database/exceptions.py
Normal file
10
maloja/database/exceptions.py
Normal file
@ -0,0 +1,10 @@
|
||||
class EntityExists(Exception):
|
||||
def __init__(self,entitydict):
|
||||
self.entitydict = entitydict
|
||||
|
||||
|
||||
class TrackExists(EntityExists):
|
||||
pass
|
||||
|
||||
class ArtistExists(EntityExists):
|
||||
pass
|
@ -3,7 +3,7 @@ from . sqldb import engine
|
||||
|
||||
from .dbcache import serialize
|
||||
|
||||
from ..globalconf import malojaconfig
|
||||
from ..pkg_global.conf import malojaconfig
|
||||
|
||||
from doreah.logging import log
|
||||
|
||||
|
@ -5,8 +5,9 @@ import math
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
|
||||
from ..globalconf import data_dir
|
||||
from ..pkg_global.conf import data_dir
|
||||
from .dbcache import cached_wrapper, cached_wrapper_individual
|
||||
from . import exceptions as exc
|
||||
|
||||
from doreah.logging import log
|
||||
from doreah.regular import runhourly, runmonthly
|
||||
@ -275,7 +276,9 @@ def delete_scrobble(scrobble_id,dbconn=None):
|
||||
DB['scrobbles'].c.timestamp == scrobble_id
|
||||
)
|
||||
|
||||
dbconn.execute(op)
|
||||
result = dbconn.execute(op)
|
||||
|
||||
return True
|
||||
|
||||
@connection_provider
|
||||
def update_scrobble_track_id(scrobble_id, track_id, dbconn=None):
|
||||
@ -292,7 +295,7 @@ def update_scrobble_track_id(scrobble_id, track_id, dbconn=None):
|
||||
|
||||
@cached_wrapper
|
||||
@connection_provider
|
||||
def get_track_id(trackdict,dbconn=None):
|
||||
def get_track_id(trackdict,create_new=True,dbconn=None):
|
||||
ntitle = normalize_name(trackdict['title'])
|
||||
artist_ids = [get_artist_id(a) for a in trackdict['artists']]
|
||||
artist_ids = list(set(artist_ids))
|
||||
@ -322,6 +325,8 @@ def get_track_id(trackdict,dbconn=None):
|
||||
#print("ID for",trackdict['title'],"was",row[0])
|
||||
return row.id
|
||||
|
||||
if not create_new: return None
|
||||
|
||||
|
||||
op = DB['tracks'].insert().values(
|
||||
**track_dict_to_db(trackdict)
|
||||
@ -365,6 +370,78 @@ def get_artist_id(artistname,create_new=True,dbconn=None):
|
||||
return result.inserted_primary_key[0]
|
||||
|
||||
|
||||
### Edit existing
|
||||
|
||||
@connection_provider
|
||||
def edit_artist(id,artistupdatedict,dbconn=None):
|
||||
|
||||
artist = get_artist(id)
|
||||
changedartist = artistupdatedict # well
|
||||
|
||||
dbentry = artist_dict_to_db(artistupdatedict)
|
||||
|
||||
existing_artist_id = get_artist_id(changedartist,create_new=False,dbconn=dbconn)
|
||||
if existing_artist_id not in (None,id):
|
||||
raise exc.ArtistExists(changedartist)
|
||||
|
||||
op = DB['artists'].update().where(
|
||||
DB['artists'].c.id==id
|
||||
).values(
|
||||
**dbentry
|
||||
)
|
||||
result = dbconn.execute(op)
|
||||
|
||||
return True
|
||||
|
||||
@connection_provider
|
||||
def edit_track(id,trackupdatedict,dbconn=None):
|
||||
|
||||
track = get_track(id)
|
||||
changedtrack = {**track,**trackupdatedict}
|
||||
|
||||
dbentry = track_dict_to_db(trackupdatedict)
|
||||
|
||||
existing_track_id = get_track_id(changedtrack,create_new=False,dbconn=dbconn)
|
||||
if existing_track_id not in (None,id):
|
||||
raise exc.TrackExists(changedtrack)
|
||||
|
||||
op = DB['tracks'].update().where(
|
||||
DB['tracks'].c.id==id
|
||||
).values(
|
||||
**dbentry
|
||||
)
|
||||
result = dbconn.execute(op)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
### Merge
|
||||
|
||||
@connection_provider
|
||||
def merge_tracks(target_id,source_ids,dbconn=None):
|
||||
|
||||
op = DB['scrobbles'].update().where(
|
||||
DB['scrobbles'].c.track_id.in_(source_ids)
|
||||
).values(
|
||||
track_id=target_id
|
||||
)
|
||||
result = dbconn.execute(op)
|
||||
clean_db()
|
||||
|
||||
return True
|
||||
|
||||
@connection_provider
|
||||
def merge_artists(target_id,source_ids,dbconn=None):
|
||||
|
||||
op = DB['trackartists'].update().where(
|
||||
DB['trackartists'].c.artist_id.in_(source_ids)
|
||||
).values(
|
||||
artist_id=target_id
|
||||
)
|
||||
result = dbconn.execute(op)
|
||||
clean_db()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@ -377,7 +454,7 @@ def get_scrobbles_of_artist(artist,since=None,to=None,resolve_references=True,db
|
||||
if since is None: since=0
|
||||
if to is None: to=now()
|
||||
|
||||
artist_id = get_artist_id(artist)
|
||||
artist_id = get_artist_id(artist,dbconn=dbconn)
|
||||
|
||||
jointable = sql.join(DB['scrobbles'],DB['trackartists'],DB['scrobbles'].c.track_id == DB['trackartists'].c.track_id)
|
||||
|
||||
@ -400,7 +477,7 @@ def get_scrobbles_of_track(track,since=None,to=None,resolve_references=True,dbco
|
||||
if since is None: since=0
|
||||
if to is None: to=now()
|
||||
|
||||
track_id = get_track_id(track)
|
||||
track_id = get_track_id(track,dbconn=dbconn)
|
||||
|
||||
op = DB['scrobbles'].select().where(
|
||||
DB['scrobbles'].c.timestamp<=to,
|
||||
@ -466,7 +543,7 @@ def get_artists_of_track(track_id,resolve_references=True,dbconn=None):
|
||||
@connection_provider
|
||||
def get_tracks_of_artist(artist,dbconn=None):
|
||||
|
||||
artist_id = get_artist_id(artist)
|
||||
artist_id = get_artist_id(artist,dbconn=dbconn)
|
||||
|
||||
op = sql.join(DB['tracks'],DB['trackartists']).select().where(
|
||||
DB['trackartists'].c.artist_id==artist_id
|
||||
@ -497,7 +574,7 @@ def get_tracks(dbconn=None):
|
||||
|
||||
@cached_wrapper
|
||||
@connection_provider
|
||||
def count_scrobbles_by_artist(since,to,dbconn=None):
|
||||
def count_scrobbles_by_artist(since,to,resolve_ids=True,dbconn=None):
|
||||
jointable = sql.join(
|
||||
DB['scrobbles'],
|
||||
DB['trackartists'],
|
||||
@ -525,16 +602,18 @@ def count_scrobbles_by_artist(since,to,dbconn=None):
|
||||
).order_by(sql.desc('count'))
|
||||
result = dbconn.execute(op).all()
|
||||
|
||||
|
||||
counts = [row.count for row in result]
|
||||
artists = get_artists_map([row.artist_id for row in result])
|
||||
result = [{'scrobbles':row.count,'artist':artists[row.artist_id]} for row in result]
|
||||
if resolve_ids:
|
||||
counts = [row.count for row in result]
|
||||
artists = get_artists_map([row.artist_id for row in result])
|
||||
result = [{'scrobbles':row.count,'artist':artists[row.artist_id]} for row in result]
|
||||
else:
|
||||
result = [{'scrobbles':row.count,'artist_id':row.artist_id} for row in result]
|
||||
result = rank(result,key='scrobbles')
|
||||
return result
|
||||
|
||||
@cached_wrapper
|
||||
@connection_provider
|
||||
def count_scrobbles_by_track(since,to,dbconn=None):
|
||||
def count_scrobbles_by_track(since,to,resolve_ids=True,dbconn=None):
|
||||
|
||||
|
||||
op = sql.select(
|
||||
@ -546,10 +625,12 @@ def count_scrobbles_by_track(since,to,dbconn=None):
|
||||
).group_by(DB['scrobbles'].c.track_id).order_by(sql.desc('count'))
|
||||
result = dbconn.execute(op).all()
|
||||
|
||||
|
||||
counts = [row.count for row in result]
|
||||
tracks = get_tracks_map([row.track_id for row in result])
|
||||
result = [{'scrobbles':row.count,'track':tracks[row.track_id]} for row in result]
|
||||
if resolve_ids:
|
||||
counts = [row.count for row in result]
|
||||
tracks = get_tracks_map([row.track_id for row in result])
|
||||
result = [{'scrobbles':row.count,'track':tracks[row.track_id]} for row in result]
|
||||
else:
|
||||
result = [{'scrobbles':row.count,'track_id':row.track_id} for row in result]
|
||||
result = rank(result,key='scrobbles')
|
||||
return result
|
||||
|
||||
@ -557,7 +638,7 @@ def count_scrobbles_by_track(since,to,dbconn=None):
|
||||
@connection_provider
|
||||
def count_scrobbles_by_track_of_artist(since,to,artist,dbconn=None):
|
||||
|
||||
artist_id = get_artist_id(artist)
|
||||
artist_id = get_artist_id(artist,dbconn=dbconn)
|
||||
|
||||
jointable = sql.join(
|
||||
DB['scrobbles'],
|
||||
@ -577,7 +658,7 @@ def count_scrobbles_by_track_of_artist(since,to,artist,dbconn=None):
|
||||
|
||||
|
||||
counts = [row.count for row in result]
|
||||
tracks = get_tracks_map([row.track_id for row in result])
|
||||
tracks = get_tracks_map([row.track_id for row in result],dbconn=dbconn)
|
||||
result = [{'scrobbles':row.count,'track':tracks[row.track_id]} for row in result]
|
||||
result = rank(result,key='scrobbles')
|
||||
return result
|
||||
@ -659,7 +740,7 @@ def get_associated_artists(*artists,dbconn=None):
|
||||
@cached_wrapper
|
||||
@connection_provider
|
||||
def get_credited_artists(*artists,dbconn=None):
|
||||
artist_ids = [get_artist_id(a) for a in artists]
|
||||
artist_ids = [get_artist_id(a,dbconn=dbconn) for a in artists]
|
||||
|
||||
jointable = sql.join(
|
||||
DB['associated_artists'],
|
||||
|
2
maloja/dev/__init__.py
Normal file
2
maloja/dev/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
### Subpackage that takes care of all things that concern the server process itself,
|
||||
### e.g. analytics
|
@ -1,5 +1,6 @@
|
||||
import random
|
||||
import datetime
|
||||
|
||||
from doreah.io import ask
|
||||
|
||||
|
||||
@ -66,10 +67,10 @@ def generate_track():
|
||||
|
||||
|
||||
|
||||
def generate(n=200):
|
||||
def generate_scrobbles(n=200):
|
||||
|
||||
from ..database.sqldb import add_scrobbles
|
||||
|
||||
from ...database.sqldb import add_scrobbles
|
||||
|
||||
n = int(n)
|
||||
|
||||
if ask("Generate random scrobbles?",default=False):
|
@ -2,11 +2,10 @@ import os
|
||||
|
||||
import cProfile, pstats
|
||||
|
||||
|
||||
from doreah.logging import log
|
||||
from doreah.timing import Clock
|
||||
|
||||
from ..globalconf import data_dir
|
||||
from ..pkg_global.conf import data_dir
|
||||
|
||||
|
||||
profiler = cProfile.Profile()
|
@ -1,4 +1,4 @@
|
||||
from .globalconf import data_dir, malojaconfig
|
||||
from .pkg_global.conf import data_dir, malojaconfig
|
||||
from . import thirdparty
|
||||
from . import database
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
from . import filters
|
||||
from ..globalconf import malojaconfig
|
||||
from ..pkg_global.conf import malojaconfig
|
||||
|
||||
from .. import database, malojatime, images, malojauri, thirdparty, __pkginfo__
|
||||
from ..database import jinjaview
|
||||
|
@ -3,7 +3,7 @@ from calendar import monthrange
|
||||
from os.path import commonprefix
|
||||
import math
|
||||
|
||||
from .globalconf import malojaconfig
|
||||
from .pkg_global.conf import malojaconfig
|
||||
|
||||
|
||||
OFFSET = malojaconfig["TIMEZONE"]
|
||||
|
@ -3,7 +3,7 @@ from doreah.configuration import Configuration
|
||||
from doreah.configuration import types as tp
|
||||
|
||||
|
||||
from .__pkginfo__ import VERSION
|
||||
from ..__pkginfo__ import VERSION
|
||||
|
||||
|
||||
|
@ -1,140 +0,0 @@
|
||||
import subprocess
|
||||
from doreah import settings
|
||||
from doreah.control import mainfunction
|
||||
from doreah.io import col
|
||||
import os
|
||||
import signal
|
||||
from ipaddress import ip_address
|
||||
|
||||
from .setup import setup
|
||||
from . import tasks
|
||||
from .. import __pkginfo__ as info
|
||||
from .. import globalconf
|
||||
|
||||
|
||||
|
||||
def print_header_info():
|
||||
print()
|
||||
#print("#####")
|
||||
print(col['yellow']("Maloja"),"v" + info.VERSION)
|
||||
print(info.HOMEPAGE)
|
||||
#print("#####")
|
||||
print()
|
||||
|
||||
|
||||
|
||||
def getInstance():
|
||||
try:
|
||||
output = subprocess.check_output(["pidof","Maloja"])
|
||||
return int(output)
|
||||
except:
|
||||
return None
|
||||
|
||||
def getInstanceSupervisor():
|
||||
try:
|
||||
output = subprocess.check_output(["pidof","maloja_supervisor"])
|
||||
return int(output)
|
||||
except:
|
||||
return None
|
||||
|
||||
def restart():
|
||||
stop()
|
||||
start()
|
||||
|
||||
def start():
|
||||
if getInstanceSupervisor() is not None:
|
||||
print("Maloja is already running.")
|
||||
else:
|
||||
print_header_info()
|
||||
setup()
|
||||
try:
|
||||
#p = subprocess.Popen(["python3","-m","maloja.server"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
sp = subprocess.Popen(["python3","-m","maloja.proccontrol.supervisor"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
print(col["green"]("Maloja started!"))
|
||||
|
||||
port = globalconf.malojaconfig["PORT"]
|
||||
|
||||
print("Visit your server address (Port " + str(port) + ") to see your web interface. Visit /admin_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) + "/admin_setup"))
|
||||
return True
|
||||
except:
|
||||
print("Error while starting Maloja.")
|
||||
return False
|
||||
|
||||
|
||||
def stop():
|
||||
|
||||
pid_sv = getInstanceSupervisor()
|
||||
if pid_sv is not None:
|
||||
os.kill(pid_sv,signal.SIGTERM)
|
||||
|
||||
pid = getInstance()
|
||||
if pid is not None:
|
||||
os.kill(pid,signal.SIGTERM)
|
||||
|
||||
if pid is None and pid_sv is None:
|
||||
return False
|
||||
|
||||
print("Maloja stopped!")
|
||||
return True
|
||||
|
||||
def onlysetup():
|
||||
print_header_info()
|
||||
setup()
|
||||
print("Setup complete!")
|
||||
|
||||
def direct():
|
||||
print_header_info()
|
||||
setup()
|
||||
from .. import server
|
||||
server.run_server()
|
||||
|
||||
def debug():
|
||||
os.environ["MALOJA_DEV_MODE"] = 'true'
|
||||
globalconf.malojaconfig.load_environment()
|
||||
direct()
|
||||
|
||||
def print_info():
|
||||
print_header_info()
|
||||
print(col['lightblue']("Configuration Directory:"),globalconf.dir_settings['config'])
|
||||
print(col['lightblue']("Data Directory: "),globalconf.dir_settings['state'])
|
||||
print(col['lightblue']("Log Directory: "),globalconf.dir_settings['logs'])
|
||||
print(col['lightblue']("Network: "),f"IPv{ip_address(globalconf.malojaconfig['host']).version}, Port {globalconf.malojaconfig['port']}")
|
||||
print(col['lightblue']("Timezone: "),f"UTC{globalconf.malojaconfig['timezone']:+d}")
|
||||
print()
|
||||
print()
|
||||
|
||||
@mainfunction({"l":"level","v":"version","V":"version"},flags=['version','include_images'],shield=True)
|
||||
def main(*args,**kwargs):
|
||||
|
||||
actions = {
|
||||
# server
|
||||
"start":start,
|
||||
"restart":restart,
|
||||
"stop":stop,
|
||||
"run":direct,
|
||||
"debug":debug,
|
||||
"setup":onlysetup,
|
||||
# admin scripts
|
||||
"import":tasks.import_scrobbles, # maloja import /x/y.csv
|
||||
"backup":tasks.backup, # maloja backup --targetfolder /x/y --include_images
|
||||
"generate":tasks.generate, # maloja generate 400
|
||||
"export":tasks.export, # maloja export
|
||||
# aux
|
||||
"info":print_info
|
||||
}
|
||||
|
||||
if "version" in kwargs:
|
||||
print(info.VERSION)
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
action, *args = args
|
||||
action = actions[action]
|
||||
except (ValueError, KeyError):
|
||||
print("Valid commands: " + " ".join(a for a in actions))
|
||||
return False
|
||||
|
||||
return action(*args,**kwargs)
|
@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
from ..globalconf import malojaconfig
|
||||
|
||||
import subprocess
|
||||
import setproctitle
|
||||
import signal
|
||||
from doreah.logging import log
|
||||
|
||||
|
||||
from .control import getInstance
|
||||
|
||||
|
||||
setproctitle.setproctitle("maloja_supervisor")
|
||||
|
||||
def start():
|
||||
try:
|
||||
return subprocess.Popen(
|
||||
["python3", "-m", "maloja","run"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except e:
|
||||
log("Error starting Maloja: " + str(e),module="supervisor")
|
||||
|
||||
|
||||
|
||||
while True:
|
||||
log("Maloja is not running, starting...",module="supervisor")
|
||||
process = start()
|
||||
|
||||
process.wait()
|
@ -1,4 +1,3 @@
|
||||
from .import_scrobbles import import_scrobbles
|
||||
from .backup import backup
|
||||
from .generate import generate
|
||||
from .export import export # read that line out loud
|
||||
|
@ -2,7 +2,7 @@ import tarfile
|
||||
import time
|
||||
import glob
|
||||
import os
|
||||
from ...globalconf import dir_settings
|
||||
from ...pkg_global.conf import dir_settings
|
||||
from pathlib import PurePath
|
||||
|
||||
from doreah.logging import log
|
||||
|
@ -4,7 +4,7 @@ import json, csv
|
||||
from doreah.io import col, ask, prompt
|
||||
|
||||
from ...cleanup import *
|
||||
from ...globalconf import data_dir
|
||||
from ...pkg_global.conf import data_dir
|
||||
|
||||
|
||||
c = CleanerAgent()
|
||||
|
@ -2,7 +2,6 @@
|
||||
import sys
|
||||
import os
|
||||
from threading import Thread
|
||||
import setproctitle
|
||||
from importlib import resources
|
||||
from css_html_js_minify import html_minify, css_minify
|
||||
import datauri
|
||||
@ -22,12 +21,12 @@ from . import database
|
||||
from .database.jinjaview import JinjaDBConnection
|
||||
from .images import resolve_track_image, resolve_artist_image
|
||||
from .malojauri import uri_to_internal, remove_identical
|
||||
from .globalconf import malojaconfig, data_dir
|
||||
from .pkg_global.conf import malojaconfig, data_dir
|
||||
from .jinjaenv.context import jinja_environment
|
||||
from .apis import init_apis, apikeystore
|
||||
|
||||
|
||||
from .proccontrol.profiler import profile
|
||||
from .dev.profiler import profile
|
||||
|
||||
|
||||
######
|
||||
@ -43,8 +42,6 @@ BaseRequest.MEMFILE_MAX = 15 * 1024 * 1024
|
||||
|
||||
webserver = Bottle()
|
||||
|
||||
#rename process, this is now required for the daemon manager to work
|
||||
setproctitle.setproctitle("Maloja")
|
||||
|
||||
|
||||
######
|
||||
|
@ -1,10 +1,12 @@
|
||||
from importlib import resources
|
||||
from distutils import dir_util
|
||||
from doreah.io import col, ask, prompt
|
||||
from doreah import auth
|
||||
import os
|
||||
|
||||
from ..globalconf import data_dir, dir_settings, malojaconfig
|
||||
from importlib import resources
|
||||
from distutils import dir_util
|
||||
|
||||
from doreah.io import col, ask, prompt
|
||||
from doreah import auth
|
||||
|
||||
from .pkg_global.conf import data_dir, dir_settings, malojaconfig
|
||||
|
||||
|
||||
|
||||
@ -48,7 +50,7 @@ def setup():
|
||||
|
||||
|
||||
# OWN API KEY
|
||||
from ..apis import apikeystore
|
||||
from .apis import apikeystore
|
||||
if len(apikeystore) == 0:
|
||||
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:
|
2
maloja/thirdparty/__init__.py
vendored
2
maloja/thirdparty/__init__.py
vendored
@ -13,7 +13,7 @@ import base64
|
||||
from doreah.logging import log
|
||||
from threading import BoundedSemaphore
|
||||
|
||||
from ..globalconf import malojaconfig
|
||||
from ..pkg_global.conf import malojaconfig
|
||||
from .. import database
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ import csv
|
||||
from doreah.logging import log
|
||||
from doreah.io import col
|
||||
|
||||
from .globalconf import data_dir, dir_settings
|
||||
from .pkg_global.conf import data_dir, dir_settings
|
||||
from .apis import _apikeys
|
||||
|
||||
|
||||
|
@ -50,9 +50,7 @@
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
<div id="notification_area">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="footer">
|
||||
@ -84,9 +82,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/admin_overview"><div title="Server Administration" id="settingsicon" class="clickable_icon">
|
||||
<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>
|
||||
<div id="icon_bar">
|
||||
{% block icon_bar %}{% endblock %}
|
||||
{% include 'icons/settings.jinja' %}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="notification_area">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -66,6 +66,8 @@
|
||||
<ul>
|
||||
<li>manually scrobble from track pages</li>
|
||||
<li>delete scrobbles</li>
|
||||
<li>edit tracks and artists</li>
|
||||
<li>merge tracks and artists</li>
|
||||
<li>upload artist and track art by dropping a file on the existing image on an artist or track page</li>
|
||||
<li>see more detailed error pages</li>
|
||||
</ul>
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/rangeselect.js"></script>
|
||||
<script src="/edit.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% set artist = filterkeys.artist %}
|
||||
@ -26,10 +27,23 @@
|
||||
|
||||
{% set encodedartist = mlj_uri.uriencode({'artist':artist}) %}
|
||||
|
||||
{% block icon_bar %}
|
||||
{% if adminmode %}
|
||||
{% include 'icons/edit.jinja' %}
|
||||
{% include 'icons/merge.jinja' %}
|
||||
{% include 'icons/merge_mark.jinja' %}
|
||||
{% include 'icons/merge_cancel.jinja' %}
|
||||
<script>showValidMergeIcons();</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<script>
|
||||
const entity_id = {{ info.id }};
|
||||
const entity_type = 'artist';
|
||||
const entity_name = {{ artist | tojson }};
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@ -47,7 +61,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text">
|
||||
<h1 class="headerwithextra">{{ info.artist }}</h1>
|
||||
<h1 id="main_entity_name" class="headerwithextra">{{ info.artist }}</h1>
|
||||
{% if competes %}<span class="rank"><a href="/charts_artists?max=100">#{{ info.position }}</a></span>{% endif %}
|
||||
<br/>
|
||||
{% if competes and included %}
|
||||
@ -56,7 +70,9 @@
|
||||
<span>Competing under {{ links.link(credited) }} (#{{ info.position }})</span>
|
||||
{% endif %}
|
||||
|
||||
<p class="stats"><a href="{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}">{{ info['scrobbles'] }} Scrobbles</a></p>
|
||||
<p class="stats">
|
||||
<a href="{{ mlj_uri.create_uri("/scrobbles",filterkeys) }}">{{ info['scrobbles'] }} Scrobbles</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@ -72,6 +88,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<h2><a href='{{ mlj_uri.create_uri("/charts_tracks",filterkeys) }}'>Top Tracks</a></h2>
|
||||
|
||||
|
||||
|
11
maloja/web/jinja/icons/edit.jinja
Normal file
11
maloja/web/jinja/icons/edit.jinja
Normal file
@ -0,0 +1,11 @@
|
||||
<div title="Edit" id="editicon" class="clickable_icon" onclick="editEntity()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 59.985 59.985" style="enable-background:new 0 0 59.985 59.985;" xml:space="preserve">
|
||||
<path d="M5.243,44.844L42.378,7.708l9.899,9.899L15.141,54.742L5.243,44.844z"/>
|
||||
<path d="M56.521,13.364l1.414-1.414c1.322-1.322,2.05-3.079,2.05-4.949s-0.728-3.627-2.05-4.949S54.855,0,52.985,0
|
||||
s-3.627,0.729-4.95,2.051l-1.414,1.414L56.521,13.364z"/>
|
||||
<path d="M4.099,46.527L0.051,58.669c-0.12,0.359-0.026,0.756,0.242,1.023c0.19,0.19,0.446,0.293,0.707,0.293
|
||||
c0.106,0,0.212-0.017,0.316-0.052l12.141-4.047L4.099,46.527z"/>
|
||||
<path d="M43.793,6.294l1.415-1.415l9.899,9.899l-1.415,1.415L43.793,6.294z"/>
|
||||
</svg>
|
||||
</div>
|
8
maloja/web/jinja/icons/merge.jinja
Normal file
8
maloja/web/jinja/icons/merge.jinja
Normal file
@ -0,0 +1,8 @@
|
||||
<div title="Merge" id="mergeicon" class="clickable_icon hide" onclick="merge()">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M7.105 8.79A3.001 3.001 0 0 0 10 11h4a5.001 5.001 0 0 1 4.927 4.146A3.001 3.001 0 0 1 18 21a3 3 0 0 1-1.105-5.79A3.001 3.001 0 0 0 14 13h-4a4.978 4.978 0 0 1-3-1v3.17a3.001 3.001 0 1 1-2 0V8.83a3.001 3.001 0 1 1 2.105-.04z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
10
maloja/web/jinja/icons/merge_cancel.jinja
Normal file
10
maloja/web/jinja/icons/merge_cancel.jinja
Normal file
@ -0,0 +1,10 @@
|
||||
<div title="Cancel merge" id="mergecancelicon" class="clickable_icon hide" onclick="cancelMerge()">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 47.095 47.095" style="enable-background:new 0 0 47.095 47.095;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M45.363,36.234l-13.158-13.16l12.21-12.21c2.31-2.307,2.31-6.049,0-8.358c-2.308-2.308-6.05-2.307-8.356,0l-12.212,12.21
|
||||
L11.038,1.906c-2.309-2.308-6.051-2.308-8.358,0c-2.307,2.309-2.307,6.049,0,8.358l12.81,12.81L1.732,36.831
|
||||
c-2.309,2.31-2.309,6.05,0,8.359c2.308,2.307,6.049,2.307,8.356,0l13.759-13.758l13.16,13.16c2.308,2.308,6.049,2.308,8.356,0
|
||||
C47.673,42.282,47.672,38.54,45.363,36.234z"/>
|
||||
</g>
|
||||
</div>
|
5
maloja/web/jinja/icons/merge_mark.jinja
Normal file
5
maloja/web/jinja/icons/merge_mark.jinja
Normal file
@ -0,0 +1,5 @@
|
||||
<div title="Mark for merging" id="mergemarkicon" class="clickable_icon hide" onclick="markForMerge()">
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M10,0 L10,2.60002 C12.2108812,3.04881281 13.8920863,4.95644867 13.9950026,7.27443311 L14,7.5 L14,11.2676 C14.5978,11.6134 15,12.2597 15,13 C15,14.1046 14.1046,15 13,15 C11.8954,15 11,14.1046 11,13 C11,12.3166462 11.342703,11.713387 11.8656124,11.3526403 L12,11.2676 L12,7.5 C12,6.259091 11.246593,5.19415145 10.1722389,4.73766702 L10,4.67071 L10,7 L6,3.5 L10,0 Z M3,1 C4.10457,1 5,1.89543 5,3 C5,3.68333538 4.65729704,4.28663574 4.13438762,4.6473967 L4,4.73244 L4,11.2676 C4.5978,11.6134 5,12.2597 5,13 C5,14.1046 4.10457,15 3,15 C1.89543,15 1,14.1046 1,13 C1,12.3166462 1.34270296,11.713387 1.86561238,11.3526403 L2,11.2676 L2,4.73244 C1.4022,4.38663 1,3.74028 1,3 C1,1.89543 1.89543,1 3,1 Z"/>
|
||||
</svg>
|
||||
</div>
|
7
maloja/web/jinja/icons/settings.jinja
Normal file
7
maloja/web/jinja/icons/settings.jinja
Normal file
@ -0,0 +1,7 @@
|
||||
<a class='hidelink' href="/admin_overview">
|
||||
<div title="Server Administration" id="settingsicon" class="clickable_icon" style="margin-left:25px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" 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>
|
@ -5,6 +5,7 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/rangeselect.js"></script>
|
||||
<script src="/edit.js"></script>
|
||||
<script>
|
||||
function scrobble(encodedtrack) {
|
||||
neo.xhttprequest('/apis/mlj_1/newscrobble?nofix&' + encodedtrack,data={},method="POST").then(response=>{window.location.reload()});
|
||||
@ -21,8 +22,24 @@
|
||||
{% set encodedtrack = mlj_uri.uriencode({'track':track}) %}
|
||||
|
||||
|
||||
{% block icon_bar %}
|
||||
{% if adminmode %}
|
||||
{% include 'icons/edit.jinja' %}
|
||||
{% include 'icons/merge.jinja' %}
|
||||
{% include 'icons/merge_mark.jinja' %}
|
||||
{% include 'icons/merge_cancel.jinja' %}
|
||||
<script>showValidMergeIcons();</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script>
|
||||
const entity_id = {{ info.id }};
|
||||
const entity_type = 'track';
|
||||
const entity_name = {{ track.title | tojson }};
|
||||
</script>
|
||||
|
||||
|
||||
{% import 'partials/awards_track.jinja' as awards %}
|
||||
|
||||
@ -42,7 +59,7 @@
|
||||
</td>
|
||||
<td class="text">
|
||||
<span>{{ links.links(track.artists) }}</span><br/>
|
||||
<h1 class="headerwithextra">{{ info.track.title }}</h1>
|
||||
<h1 id="main_entity_name" class="headerwithextra">{{ info.track.title }}</h1>
|
||||
{{ awards.certs(track) }}
|
||||
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
|
||||
<br/>
|
||||
|
@ -156,5 +156,5 @@ input:focus {
|
||||
|
||||
|
||||
.hide {
|
||||
display:none;
|
||||
display:none !important;
|
||||
}
|
||||
|
@ -55,8 +55,18 @@ div.header h1 {
|
||||
settings icon
|
||||
**/
|
||||
|
||||
div.clickable_icon {
|
||||
div#icon_bar {
|
||||
position:fixed;
|
||||
right:30px;
|
||||
top:30px;
|
||||
}
|
||||
|
||||
div#icon_bar div.clickable_icon {
|
||||
display: inline-block;
|
||||
height:26px;
|
||||
width:26px;
|
||||
}
|
||||
div.clickable_icon {
|
||||
fill: var(--text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -67,11 +77,6 @@ div.clickable_icon.danger:hover {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
div#settingsicon {
|
||||
position:fixed;
|
||||
right:30px;
|
||||
top:30px;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
@ -1,14 +1,191 @@
|
||||
// JS for all web interface editing / deletion of scrobble data
|
||||
|
||||
// HELPERS
|
||||
function selectAll(e) {
|
||||
// https://stackoverflow.com/a/6150060/6651341
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(e);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
// DELETION
|
||||
function toggleDeleteConfirm(element) {
|
||||
element.parentElement.parentElement.classList.toggle('active');
|
||||
}
|
||||
|
||||
function deleteScrobble(id,element) {
|
||||
element.parentElement.parentElement.parentElement.classList.add('removed');
|
||||
var callback_func = function(req){
|
||||
if (req.status == 200) {
|
||||
element.parentElement.parentElement.parentElement.classList.add('removed');
|
||||
notifyCallback(req);
|
||||
}
|
||||
else {
|
||||
notifyCallback(req);
|
||||
}
|
||||
};
|
||||
|
||||
neo.xhttpreq("/apis/mlj_1/delete_scrobble",data={'timestamp':id},method="POST",callback=(()=>null),json=true);
|
||||
neo.xhttpreq("/apis/mlj_1/delete_scrobble",data={'timestamp':id},method="POST",callback=callback_func,json=true);
|
||||
}
|
||||
|
||||
|
||||
// EDIT NAME
|
||||
function editEntity() {
|
||||
|
||||
var namefield = document.getElementById('main_entity_name');
|
||||
namefield.contentEditable = "plaintext-only";
|
||||
|
||||
namefield.addEventListener('keydown',function(e){
|
||||
// dont allow new lines, done on enter
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
namefield.blur(); // this leads to below
|
||||
}
|
||||
// cancel on esc
|
||||
else if (e.key === "Escape" || e.key === "Esc") {
|
||||
e.preventDefault();
|
||||
namefield.textContent = entity_name;
|
||||
namefield.blur();
|
||||
}
|
||||
})
|
||||
|
||||
// emergency, not pretty because it will move cursor
|
||||
namefield.addEventListener('input',function(e){
|
||||
if (namefield.textContent.includes("\n")) {
|
||||
namefield.textContent = namefield.textContent.replace("\n","");
|
||||
}
|
||||
})
|
||||
|
||||
// manually clicking away OR enter
|
||||
namefield.addEventListener('blur',function(e){
|
||||
doneEditing();
|
||||
})
|
||||
|
||||
namefield.focus();
|
||||
selectAll(namefield);
|
||||
}
|
||||
|
||||
function doneEditing() {
|
||||
window.getSelection().removeAllRanges();
|
||||
var namefield = document.getElementById('main_entity_name');
|
||||
namefield.contentEditable = "false";
|
||||
newname = namefield.textContent;
|
||||
|
||||
if (newname != entity_name) {
|
||||
var searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (entity_type == 'artist') {
|
||||
var endpoint = "/apis/mlj_1/edit_artist";
|
||||
searchParams.set("artist", newname);
|
||||
var payload = {'id':entity_id,'name':newname};
|
||||
}
|
||||
else if (entity_type == 'track') {
|
||||
var endpoint = "/apis/mlj_1/edit_track";
|
||||
searchParams.set("title", newname);
|
||||
var payload = {'id':entity_id,'title':newname}
|
||||
}
|
||||
|
||||
callback_func = function(req){
|
||||
if (req.status == 200) {
|
||||
window.location = "?" + searchParams.toString();
|
||||
}
|
||||
else {
|
||||
notifyCallback(req);
|
||||
namefield.textContent = entity_name;
|
||||
}
|
||||
};
|
||||
|
||||
neo.xhttpreq(
|
||||
endpoint,
|
||||
data=payload,
|
||||
method="POST",
|
||||
callback=callback_func,
|
||||
json=true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// MERGING
|
||||
|
||||
function showValidMergeIcons() {
|
||||
const lcst = window.sessionStorage;
|
||||
var key = "marked_for_merge_" + entity_type;
|
||||
var current_stored = (lcst.getItem(key) || '').split(",");
|
||||
current_stored = current_stored.filter((x)=>x).map((x)=>parseInt(x));
|
||||
|
||||
var mergeicon = document.getElementById('mergeicon');
|
||||
var mergemarkicon = document.getElementById('mergemarkicon');
|
||||
var mergecancelicon = document.getElementById('mergecancelicon');
|
||||
|
||||
mergeicon.classList.add('hide');
|
||||
mergemarkicon.classList.add('hide');
|
||||
mergecancelicon.classList.add('hide');
|
||||
|
||||
if (current_stored.length == 0) {
|
||||
mergemarkicon.classList.remove('hide');
|
||||
}
|
||||
else {
|
||||
mergecancelicon.classList.remove('hide');
|
||||
|
||||
if (current_stored.includes(entity_id)) {
|
||||
|
||||
}
|
||||
else {
|
||||
mergemarkicon.classList.remove('hide');
|
||||
mergeicon.classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function markForMerge() {
|
||||
const lcst = window.sessionStorage;
|
||||
var key = "marked_for_merge_" + entity_type;
|
||||
var current_stored = (lcst.getItem(key) || '').split(",");
|
||||
current_stored = current_stored.filter((x)=>x).map((x)=>parseInt(x));
|
||||
current_stored.push(entity_id);
|
||||
current_stored = [...new Set(current_stored)];
|
||||
lcst.setItem(key,current_stored); //this already formats it correctly
|
||||
notify("Success","Marked " + entity_name + " for merge, currently " + current_stored.length + " marked!")
|
||||
showValidMergeIcons();
|
||||
}
|
||||
|
||||
function merge() {
|
||||
const lcst = window.sessionStorage;
|
||||
var key = "marked_for_merge_" + entity_type;
|
||||
var current_stored = lcst.getItem(key).split(",");
|
||||
current_stored = current_stored.filter((x)=>x).map((x)=>parseInt(x));
|
||||
|
||||
callback_func = function(req){
|
||||
if (req.status == 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
else {
|
||||
notifyCallback(req);
|
||||
}
|
||||
};
|
||||
|
||||
neo.xhttpreq(
|
||||
"/apis/mlj_1/merge_" + entity_type + "s",
|
||||
data={
|
||||
'source_ids':current_stored,
|
||||
'target_id':entity_id
|
||||
},
|
||||
method="POST",
|
||||
callback=callback_func,
|
||||
json=true
|
||||
);
|
||||
|
||||
lcst.removeItem(key);
|
||||
}
|
||||
|
||||
function cancelMerge() {
|
||||
const lcst = window.sessionStorage;
|
||||
var key = "marked_for_merge_" + entity_type;
|
||||
lcst.setItem(key,[]);
|
||||
showValidMergeIcons();
|
||||
}
|
||||
|
||||
function toggleReparseConfirm(element) {
|
||||
|
@ -69,8 +69,9 @@ function scrobble(artists,title) {
|
||||
"title":title
|
||||
}
|
||||
|
||||
|
||||
if (title != "" && artists.length > 0) {
|
||||
neo.xhttpreq("/apis/mlj_1/newscrobble",data=payload,method="POST",callback=scrobbledone,json=true)
|
||||
neo.xhttpreq("/apis/mlj_1/newscrobble",data=payload,method="POST",callback=notifyCallback,json=true)
|
||||
}
|
||||
|
||||
document.getElementById("title").value = "";
|
||||
|
@ -6,7 +6,7 @@ const colors = {
|
||||
}
|
||||
|
||||
const notification_template = info => `
|
||||
<div class="notification" style="background-color:${colors[type]};">
|
||||
<div class="notification" style="background-color:${colors[info.notification_type]};">
|
||||
<b>${info.title}</b><br/>
|
||||
<span>${info.body}</span>
|
||||
|
||||
@ -20,11 +20,11 @@ function htmlToElement(html) {
|
||||
return template.content.firstChild;
|
||||
}
|
||||
|
||||
function notify(title,msg,type='info',reload=false) {
|
||||
function notify(title,msg,notification_type='info',reload=false) {
|
||||
info = {
|
||||
'title':title,
|
||||
'body':msg,
|
||||
'type':type
|
||||
'notification_type':notification_type
|
||||
}
|
||||
|
||||
var element = htmlToElement(notification_template(info));
|
||||
@ -33,3 +33,22 @@ function notify(title,msg,type='info',reload=false) {
|
||||
|
||||
setTimeout(function(e){e.remove();},7000,element);
|
||||
}
|
||||
|
||||
function notifyCallback(request) {
|
||||
var body = request.response;
|
||||
var status = request.status;
|
||||
|
||||
if (status == 200) {
|
||||
var notification_type = 'info';
|
||||
var title = "Success!";
|
||||
var msg = "Scrobbled " + body.track.title + " by " + body.track.artists.join(", ");
|
||||
}
|
||||
else {
|
||||
var notification_type = 'warning';
|
||||
var title = "Error: " + body.error.type;
|
||||
var msg = body.error.desc || "";
|
||||
}
|
||||
|
||||
|
||||
notify(title,msg,notification_type);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ full = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
maloja = "maloja.proccontrol.control:main"
|
||||
maloja = "maloja.__main__:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["flit_core >=3.2,<4"]
|
||||
|
Loading…
Reference in New Issue
Block a user