mirror of https://github.com/krateng/maloja.git
749 lines
18 KiB
Python
749 lines
18 KiB
Python
import os
|
|
import math
|
|
import traceback
|
|
|
|
from bottle import response, static_file, request, FormsDict
|
|
|
|
from doreah.logging import log
|
|
from doreah.auth import authenticated_api, authenticated_api_with_alternate, authenticated_function
|
|
|
|
# nimrodel API
|
|
from nimrodel import EAPI as API
|
|
from nimrodel import Multi
|
|
|
|
|
|
from .. import database
|
|
from ..pkg_global.conf import malojaconfig, data_dir
|
|
|
|
|
|
|
|
from ..__pkginfo__ import VERSION
|
|
from ..malojauri import uri_to_internal, compose_querystring, internal_to_uri
|
|
from .. import images
|
|
from ._apikeys import apikeystore, api_key_correct
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api = API(delay=True)
|
|
api.__apipath__ = "mlj_1"
|
|
|
|
|
|
|
|
|
|
errors = {
|
|
database.exceptions.MissingScrobbleParameters: lambda e: (400,{
|
|
"status":"failure",
|
|
"error":{
|
|
'type':'missing_scrobble_data',
|
|
'value':e.params,
|
|
'desc':"The scrobble is missing needed parameters."
|
|
}
|
|
}),
|
|
database.exceptions.MissingEntityParameter: lambda e: (400,{
|
|
"status":"error",
|
|
"error":{
|
|
'type':'missing_entity_parameter',
|
|
'value':None,
|
|
'desc':"This API call is not valid without an entity (track or artist)."
|
|
}
|
|
}),
|
|
database.exceptions.EntityExists: lambda e: (409,{
|
|
"status":"failure",
|
|
"error":{
|
|
'type':'entity_exists',
|
|
'value':e.entitydict,
|
|
'desc':"This entity already exists in the database. Consider merging instead."
|
|
}
|
|
}),
|
|
database.exceptions.DatabaseNotBuilt: lambda e: (503,{
|
|
"status":"error",
|
|
"error":{
|
|
'type':'server_not_ready',
|
|
'value':'db_upgrade',
|
|
'desc':"The database is being upgraded. Please try again later."
|
|
}
|
|
}),
|
|
images.MalformedB64: lambda e: (400,{
|
|
"status":"failure",
|
|
"error":{
|
|
'type':'malformed_b64',
|
|
'value':None,
|
|
'desc':"The provided base 64 string is not valid."
|
|
}
|
|
}),
|
|
# for http errors, use their status code
|
|
Exception: lambda e: ((e.status_code if hasattr(e,'statuscode') else 500),{
|
|
"status":"failure",
|
|
"error":{
|
|
'type':'unknown_error',
|
|
'value':e.__repr__(),
|
|
'desc':"The server has encountered an exception."
|
|
}
|
|
})
|
|
}
|
|
|
|
def catch_exceptions(func):
|
|
def protector(*args,**kwargs):
|
|
try:
|
|
return func(*args,**kwargs)
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
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):
|
|
def decorator(func):
|
|
timeformats = "Possible formats include '2022', '2022/08', '2022/08/01', '2022/W42', 'today', 'thismonth', 'monday', 'august'"
|
|
|
|
if filterkeys:
|
|
func.__doc__ += f"""
|
|
:param string title: Track title
|
|
:param string artist: Track artist. Can be specified multiple times.
|
|
:param bool associated: Whether to include associated artists.
|
|
"""
|
|
if limitkeys:
|
|
func.__doc__ += f"""
|
|
:param string from: Start of the desired time range. Can also be called since or start. {timeformats}
|
|
:param string until: End of the desired range. Can also be called to or end. {timeformats}
|
|
:param string in: Desired range. Can also be called within or during. {timeformats}
|
|
"""
|
|
if delimitkeys:
|
|
func.__doc__ += """
|
|
:param string step: Step, e.g. month or week.
|
|
:param int stepn: Number of base type units per step
|
|
:param int trail: How many preceding steps should be factored in.
|
|
:param bool cumulative: Instead of a fixed trail length, use all history up to this point.
|
|
"""
|
|
if amountkeys:
|
|
func.__doc__ += """
|
|
:param int page: Page to show
|
|
:param int perpage: Entries per page.
|
|
:param int max: Legacy. Show first page with this many entries.
|
|
"""
|
|
return func
|
|
return decorator
|
|
|
|
|
|
|
|
@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
|
|
always respond with 200.
|
|
|
|
:param string key: An API key to be tested. Optional.
|
|
:return: status (String), error (String)
|
|
:rtype: Dictionary
|
|
"""
|
|
response.set_header("Access-Control-Allow-Origin","*")
|
|
if key is not None and not apikeystore.check_key(key):
|
|
response.status = 403
|
|
return {
|
|
"status":"error",
|
|
"error":"Wrong API key"
|
|
}
|
|
|
|
else:
|
|
response.status = 200
|
|
return {
|
|
"status":"ok"
|
|
}
|
|
|
|
|
|
@api.get("serverinfo")
|
|
@catch_exceptions
|
|
def server_info():
|
|
"""Returns basic information about the server.
|
|
|
|
:return: name (String), version (Tuple), versionstring (String), db_status (Mapping). Additional keys can be added at any point, but will not be removed within API version.
|
|
:rtype: Dictionary
|
|
"""
|
|
|
|
|
|
response.set_header("Access-Control-Allow-Origin","*")
|
|
|
|
return {
|
|
"name":malojaconfig["NAME"],
|
|
"version":VERSION.split("."),
|
|
"versionstring":VERSION,
|
|
"db_status":database.dbstatus
|
|
}
|
|
|
|
|
|
## API ENDPOINTS THAT CLOSELY MATCH ONE DATABASE FUNCTION
|
|
|
|
|
|
@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.
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary
|
|
"""
|
|
k_filter, k_time, _, k_amount, _ = uri_to_internal(keys,api=True)
|
|
ckeys = {**k_filter, **k_time, **k_amount}
|
|
|
|
result = database.get_scrobbles(**ckeys)
|
|
|
|
offset = (k_amount.get('page') * k_amount.get('perpage')) if k_amount.get('perpage') is not math.inf else 0
|
|
result = result[offset:]
|
|
if k_amount.get('perpage') is not math.inf: result = result[:k_amount.get('perpage')]
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":result
|
|
}
|
|
|
|
|
|
@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.
|
|
|
|
:return: amount (Integer)
|
|
:rtype: Dictionary
|
|
"""
|
|
k_filter, k_time, _, k_amount, _ = uri_to_internal(keys)
|
|
ckeys = {**k_filter, **k_time, **k_amount}
|
|
|
|
result = database.get_scrobbles_num(**ckeys)
|
|
|
|
return {
|
|
"status":"ok",
|
|
"amount":result
|
|
}
|
|
|
|
|
|
|
|
@api.get("tracks")
|
|
@catch_exceptions
|
|
@add_common_args_to_docstring(filterkeys=True)
|
|
def get_tracks_external(**keys):
|
|
"""Returns all tracks (optionally of an artist).
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary
|
|
"""
|
|
k_filter, _, _, _, _ = uri_to_internal(keys,forceArtist=True)
|
|
ckeys = {**k_filter}
|
|
|
|
result = database.get_tracks(**ckeys)
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":result
|
|
}
|
|
|
|
|
|
|
|
@api.get("artists")
|
|
@catch_exceptions
|
|
@add_common_args_to_docstring()
|
|
def get_artists_external():
|
|
"""Returns all artists.
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary"""
|
|
result = database.get_artists()
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":result
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@api.get("charts/artists")
|
|
@catch_exceptions
|
|
@add_common_args_to_docstring(limitkeys=True)
|
|
def get_charts_artists_external(**keys):
|
|
"""Returns artist charts
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary"""
|
|
_, k_time, _, _, _ = uri_to_internal(keys)
|
|
ckeys = {**k_time}
|
|
|
|
result = database.get_charts_artists(**ckeys)
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":result
|
|
}
|
|
|
|
|
|
|
|
@api.get("charts/tracks")
|
|
@catch_exceptions
|
|
@add_common_args_to_docstring(filterkeys=True,limitkeys=True)
|
|
def get_charts_tracks_external(**keys):
|
|
"""Returns track charts
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary"""
|
|
k_filter, k_time, _, _, _ = uri_to_internal(keys,forceArtist=True)
|
|
ckeys = {**k_filter, **k_time}
|
|
|
|
result = database.get_charts_tracks(**ckeys)
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":result
|
|
}
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary"""
|
|
k_filter, k_time, k_internal, k_amount, _ = uri_to_internal(keys)
|
|
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
|
|
|
|
results = database.get_pulse(**ckeys)
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":results
|
|
}
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary"""
|
|
k_filter, k_time, k_internal, k_amount, _ = uri_to_internal(keys)
|
|
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
|
|
|
|
results = database.get_performance(**ckeys)
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":results
|
|
}
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary"""
|
|
_, k_time, k_internal, _, _ = uri_to_internal(keys)
|
|
ckeys = {**k_time, **k_internal}
|
|
|
|
results = database.get_top_artists(**ckeys)
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":results
|
|
}
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
:return: list (List)
|
|
:rtype: Dictionary"""
|
|
_, k_time, k_internal, _, _ = uri_to_internal(keys)
|
|
ckeys = {**k_time, **k_internal}
|
|
|
|
# IMPLEMENT THIS FOR TOP TRACKS OF ARTIST AS WELL?
|
|
|
|
results = database.get_top_tracks(**ckeys)
|
|
|
|
return {
|
|
"status":"ok",
|
|
"list":results
|
|
}
|
|
|
|
|
|
|
|
|
|
@api.get("artistinfo")
|
|
@catch_exceptions
|
|
@add_common_args_to_docstring(filterkeys=True)
|
|
def artist_info_external(**keys):
|
|
"""Returns information about an artist
|
|
|
|
:return: artist (String), scrobbles (Integer), position (Integer), associated (List), medals (Mapping), topweeks (Integer)
|
|
:rtype: Dictionary"""
|
|
k_filter, _, _, _, _ = uri_to_internal(keys,forceArtist=True)
|
|
ckeys = {**k_filter}
|
|
|
|
return database.artist_info(**ckeys)
|
|
|
|
|
|
|
|
@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
|
|
|
|
:return: track (Mapping), scrobbles (Integer), position (Integer), medals (Mapping), certification (String), topweeks (Integer)
|
|
:rtype: Dictionary"""
|
|
# transform into a multidict so we can use our nomral uri_to_internal function
|
|
keys = FormsDict(keys)
|
|
for a in artist:
|
|
keys.append("artist",a)
|
|
k_filter, _, _, _, _ = uri_to_internal(keys,forceTrack=True)
|
|
ckeys = {**k_filter}
|
|
|
|
return database.track_info(**ckeys)
|
|
|
|
|
|
@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=[],
|
|
title:str="",
|
|
album:str=None,
|
|
albumartists:list=[],
|
|
duration:int=None,
|
|
length:int=None,
|
|
time:int=None,
|
|
nofix=None,
|
|
auth_result=None,
|
|
**extra_kwargs):
|
|
"""Submit a new scrobble.
|
|
|
|
:param string artist: Artist. Can be submitted multiple times as query argument for multiple artists.
|
|
:param list artists: List of artists.
|
|
:param string title: Title of the track.
|
|
:param string album: Name of the album. Optional.
|
|
:param list albumartists: Album artists. Optional.
|
|
:param int duration: Actual listened duration of the scrobble in seconds. Optional.
|
|
:param int length: Total length of the track in seconds. Optional.
|
|
:param int time: UNIX timestamp of the scrobble. Optional, not needed if scrobble is at time of request.
|
|
:param flag nofix: Skip server-side metadata parsing. Optional.
|
|
|
|
:return: status (String), track (Mapping)
|
|
:rtype: Dictionary
|
|
"""
|
|
|
|
rawscrobble = {
|
|
'track_artists':(artist or []) + artists,
|
|
'track_title':title,
|
|
'album_name':album,
|
|
'album_artists':albumartists,
|
|
'scrobble_duration':duration,
|
|
'track_length':length,
|
|
'scrobble_time':time
|
|
}
|
|
|
|
# for logging purposes, don't pass values that we didn't actually supply
|
|
rawscrobble = {k:rawscrobble[k] for k in rawscrobble if rawscrobble[k]}
|
|
|
|
|
|
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']
|
|
},
|
|
'desc':f"Scrobbled {result['track']['title']} by {', '.join(result['track']['artists'])}"
|
|
}
|
|
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("addpicture")
|
|
@authenticated_function(alternate=api_key_correct,api=True)
|
|
@catch_exceptions
|
|
def add_picture(b64,artist:Multi=[],title=None):
|
|
"""Uploads a new image for an artist or track.
|
|
|
|
param string b64: Base 64 representation of the image
|
|
param string artist: Artist name. Can be supplied multiple times for tracks with multiple artists.
|
|
param string title: Title of the track. Optional.
|
|
|
|
"""
|
|
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"]
|
|
url = images.set_image(b64,**k_filter)
|
|
|
|
return {
|
|
'status': 'success',
|
|
'url': url
|
|
}
|
|
|
|
|
|
|
|
@api.post("importrules")
|
|
@authenticated_function(api=True)
|
|
@catch_exceptions
|
|
def import_rulemodule(**keys):
|
|
"""Internal Use Only"""
|
|
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(data_dir['rules'](filename + ".tsv"))
|
|
else:
|
|
log("Importing predefined rulefile " + filename)
|
|
os.symlink(data_dir['rules']("predefined/" + filename + ".tsv"),data_dir['rules'](filename + ".tsv"))
|
|
|
|
|
|
|
|
@api.post("rebuild")
|
|
@authenticated_function(api=True)
|
|
@catch_exceptions
|
|
def rebuild(**keys):
|
|
"""Internal Use Only"""
|
|
log("Database rebuild initiated!")
|
|
database.sync()
|
|
dbstatus['rebuildinprogress'] = True
|
|
from ..proccontrol.tasks.fixexisting import fix
|
|
fix()
|
|
global cla
|
|
cla = CleanerAgent()
|
|
database.build_db()
|
|
database.invalidate_caches()
|
|
|
|
|
|
|
|
|
|
@api.get("search")
|
|
@catch_exceptions
|
|
def search(**keys):
|
|
"""Internal Use Only"""
|
|
query = keys.get("query")
|
|
max_ = keys.get("max")
|
|
if max_ is not None: max_ = int(max_)
|
|
query = query.lower()
|
|
|
|
artists = database.db_search(query,type="ARTIST")
|
|
tracks = database.db_search(query,type="TRACK")
|
|
|
|
|
|
|
|
# if the string begins with the query it's a better match, if a word in it begins with it, still good
|
|
# also, shorter is better (because longer titles would be easier to further specify)
|
|
artists.sort(key=lambda x: ((0 if x.lower().startswith(query) else 1 if " " + query in x.lower() else 2),len(x)))
|
|
tracks.sort(key=lambda x: ((0 if x["title"].lower().startswith(query) else 1 if " " + query in x["title"].lower() else 2),len(x["title"])))
|
|
|
|
# add links
|
|
artists_result = []
|
|
for a in artists:
|
|
result = {
|
|
'artist': a,
|
|
'link': "/artist?" + compose_querystring(internal_to_uri({"artist": a})),
|
|
'image': images.get_artist_image(a)
|
|
}
|
|
artists_result.append(result)
|
|
|
|
tracks_result = []
|
|
for t in tracks:
|
|
result = {
|
|
'track': t,
|
|
'link': "/track?" + compose_querystring(internal_to_uri({"track":t})),
|
|
'image': images.get_track_image(t)
|
|
}
|
|
tracks_result.append(result)
|
|
|
|
return {"artists":artists_result[:max_],"tracks":tracks_result[:max_]}
|
|
|
|
|
|
@api.post("newrule")
|
|
@authenticated_function(api=True)
|
|
@catch_exceptions
|
|
def newrule(**keys):
|
|
"""Internal Use Only"""
|
|
pass
|
|
# TODO after implementing new rule system
|
|
#tsv.add_entry(data_dir['rules']("webmade.tsv"),[k for k in keys])
|
|
#addEntry("rules/webmade.tsv",[k for k in 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
|
|
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
|
|
import tempfile
|
|
|
|
tmpfolder = tempfile.gettempdir()
|
|
archivefile = backup(tmpfolder)
|
|
|
|
return static_file(os.path.basename(archivefile),root=tmpfolder)
|
|
|
|
@api.get("export")
|
|
@authenticated_function(api=True)
|
|
@catch_exceptions
|
|
def get_export(**keys):
|
|
"""Internal Use Only"""
|
|
from ..proccontrol.tasks.export import export
|
|
import tempfile
|
|
|
|
tmpfolder = tempfile.gettempdir()
|
|
resultfile = export(tmpfolder)
|
|
|
|
return static_file(os.path.basename(resultfile),root=tmpfolder)
|
|
|
|
|
|
@api.post("delete_scrobble")
|
|
@authenticated_function(api=True)
|
|
@catch_exceptions
|
|
def delete_scrobble(timestamp):
|
|
"""Internal Use Only"""
|
|
result = database.remove_scrobble(timestamp)
|
|
return {
|
|
"status":"success",
|
|
"desc":f"Scrobble was deleted!"
|
|
}
|
|
|
|
|
|
@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)
|
|
@catch_exceptions
|
|
def reparse_scrobble(timestamp):
|
|
"""Internal Use Only"""
|
|
result = database.reparse_scrobble(timestamp)
|
|
if result:
|
|
return {
|
|
"status":"success",
|
|
"desc":f"Scrobble was reparsed!",
|
|
"scrobble":result
|
|
}
|
|
else:
|
|
return {
|
|
"status":"no_operation",
|
|
"desc":"The scrobble was not changed."
|
|
}
|