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

Compare commits

...

13 Commits

Author SHA1 Message Date
krateng
1d9247fc72 Version bump 2022-04-14 20:09:01 +02:00
krateng
c91cae9de1 Added info about API endpoint return values, fix GH-114 2022-04-14 20:02:02 +02:00
krateng
1a977d9c0c Moved all native API endpoints to new auth handling 2022-04-14 19:36:50 +02:00
krateng
62a654bfbf Added more docstrings 2022-04-14 19:34:42 +02:00
krateng
16d8ed0575 Fixed nofix argument for scrobbling 2022-04-14 17:44:52 +02:00
krateng
7c1d45f4af Fixed mistake in API testing 2022-04-14 17:32:27 +02:00
krateng
65fd57dceb Explicit arguments for native scrobble endpoint 2022-04-14 17:29:10 +02:00
krateng
29f722e3d3 Added time format info to docstrings 2022-04-14 17:00:45 +02:00
krateng
e6bb844ff9 Added some docstrings to native API endpoints, GH-114 2022-04-14 16:14:31 +02:00
krateng
bcb1d36b4a Exit codes for main function, fix GH-113 2022-04-14 15:10:15 +02:00
krateng
9d8752d052 Fixed proper recognition of artist and track entities, fix GH-111 2022-04-14 14:49:59 +02:00
krateng
741246a7c1
Merge pull request #110 from da2x/patch-1
Set Referrer-Policy to same-origin
2022-04-14 14:35:55 +02:00
Daniel Aleksandersen
c076518d76
Set Referrer-Policy to same-origin
Remove the Referer (sic) HTTP request header from external requests (e.g. to
the image CDNs).

The charset directive must be included in the first TCP packet. It should
be set at the very top of the document. Grouping document mode metas
and descriptive metadata in separate groups.
2022-04-14 13:02:21 +02:00
9 changed files with 164 additions and 53 deletions

View File

@ -249,7 +249,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.artist2}}\"\n}"
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.title1}}\"\n}"
},
"url": {
"raw": "{{url}}/api/newscrobble",
@ -904,4 +904,4 @@
"value": ""
}
]
}
}

View File

@ -4,7 +4,7 @@
# you know what f*ck it
# this is hardcoded for now because of that damn project / package name discrepancy
# i'll fix it one day
VERSION = "3.0.2"
VERSION = "3.0.3"
HOMEPAGE = "https://github.com/krateng/maloja"

View File

@ -37,6 +37,38 @@ api.__apipath__ = "mlj_1"
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")
@ -46,11 +78,13 @@ def test_server(key=None):
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 {"error":"Wrong API key"}
return {"status":"error","error":"Wrong API key"}
else:
response.status = 200
@ -59,6 +93,11 @@ def test_server(key=None):
@api.get("serverinfo")
def server_info():
"""Returns basic information about the server.
:return: name (String), version (Tuple), versionstring (String), db_status (String). 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","*")
@ -76,7 +115,13 @@ def server_info():
@api.get("scrobbles")
@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}
@ -89,19 +134,14 @@ def get_scrobbles_external(**keys):
return {"list":result}
# info for comparison
@api.get("info")
def info_external(**keys):
response.set_header("Access-Control-Allow-Origin","*")
response.set_header("Content-Type","application/json")
return info()
@api.get("numscrobbles")
@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}
@ -111,7 +151,13 @@ def get_scrobbles_num_external(**keys):
@api.get("tracks")
@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}
@ -121,7 +167,12 @@ def get_tracks_external(**keys):
@api.get("artists")
@add_common_args_to_docstring()
def get_artists_external():
"""Returns all artists.
:return: list (List)
:rtype: Dictionary"""
result = database.get_artists()
return {"list":result}
@ -130,7 +181,12 @@ def get_artists_external():
@api.get("charts/artists")
@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}
@ -140,7 +196,12 @@ def get_charts_artists_external(**keys):
@api.get("charts/tracks")
@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}
@ -151,7 +212,12 @@ def get_charts_tracks_external(**keys):
@api.get("pulse")
@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}
@ -162,7 +228,12 @@ def get_pulse_external(**keys):
@api.get("performance")
@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}
@ -173,7 +244,12 @@ def get_performance_external(**keys):
@api.get("top/artists")
@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}
@ -184,7 +260,12 @@ def get_top_artists_external(**keys):
@api.get("top/tracks")
@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}
@ -197,7 +278,12 @@ def get_top_tracks_external(**keys):
@api.get("artistinfo")
@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}
@ -206,7 +292,12 @@ def artist_info_external(**keys):
@api.get("trackinfo")
@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:
@ -217,35 +308,43 @@ def track_info_external(artist:Multi[str],**keys):
return database.track_info(**ckeys)
@api.get("compare")
def compare_external(**keys):
return database.compare(keys["remote"])
@api.post("newscrobble")
@authenticated_function(alternate=api_key_correct,api=True,pass_auth_result_as='auth_result')
def post_scrobble(artist:Multi=None,auth_result=None,**keys):
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):
"""Submit a new scrobble.
:param string artist: Artist. Can be submitted multiple times as query argument for multiple artists.
:param string artists: List of artists. Overwritten by artist parameter.
:param list artists: List of artists. Overwritten by artist parameter.
:param string title: Title of the track.
:param string album: Name of the album. Optional.
:param string albumartists: Album artists. 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 boolean nofix: Skip server-side metadata parsing. Optional.
:param flag nofix: Skip server-side metadata parsing. Optional.
:return: status (String), track (Mapping)
:rtype: Dictionary
"""
rawscrobble = {
'track_artists':artist if artist is not None else keys.get("artists"),
'track_title':keys.get('title'),
'album_name':keys.get('album'),
'album_artists':keys.get('albumartists'),
'scrobble_duration':keys.get('duration'),
'track_length':keys.get('length'),
'scrobble_time':int(keys.get('time')) if (keys.get('time') is not None) else None
'track_artists':artist if artist is not None else 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
@ -255,7 +354,7 @@ def post_scrobble(artist:Multi=None,auth_result=None,**keys):
rawscrobble,
client='browser' if auth_result.get('doreah_native_auth_check') else auth_result.get('client'),
api='native/v1',
fix=(keys.get("nofix") is None)
fix=(nofix is None)
)
if result:
@ -273,8 +372,9 @@ def post_scrobble(artist:Multi=None,auth_result=None,**keys):
@api.post("importrules")
@authenticated_api
@authenticated_function(api=True)
def import_rulemodule(**keys):
"""Internal Use Only"""
filename = keys.get("filename")
remove = keys.get("remove") is not None
validchars = "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
@ -290,8 +390,9 @@ def import_rulemodule(**keys):
@api.post("rebuild")
@authenticated_api
@authenticated_function(api=True)
def rebuild(**keys):
"""Internal Use Only"""
log("Database rebuild initiated!")
database.sync()
dbstatus['rebuildinprogress'] = True
@ -307,6 +408,7 @@ def rebuild(**keys):
@api.get("search")
def search(**keys):
"""Internal Use Only"""
query = keys.get("query")
max_ = keys.get("max")
if max_ is not None: max_ = int(max_)
@ -343,8 +445,9 @@ def search(**keys):
@api.post("addpicture")
@authenticated_api
@authenticated_function(api=True)
def add_picture(b64,artist:Multi=[],title=None):
"""Internal Use Only"""
keys = FormsDict()
for a in artist:
keys.append("artist",a)
@ -355,8 +458,9 @@ def add_picture(b64,artist:Multi=[],title=None):
@api.post("newrule")
@authenticated_api
@authenticated_function(api=True)
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])
@ -364,24 +468,28 @@ def newrule(**keys):
@api.post("settings")
@authenticated_api
@authenticated_function(api=True)
def set_settings(**keys):
"""Internal Use Only"""
malojaconfig.update(keys)
@api.post("apikeys")
@authenticated_api
@authenticated_function(api=True)
def set_apikeys(**keys):
"""Internal Use Only"""
apikeystore.update(keys)
@api.post("import")
@authenticated_api
@authenticated_function(api=True)
def import_scrobbles(identifier):
"""Internal Use Only"""
from ..thirdparty import import_scrobbles
import_scrobbles(identifier)
@api.get("backup")
@authenticated_api
@authenticated_function(api=True)
def get_backup(**keys):
"""Internal Use Only"""
from ..proccontrol.tasks.backup import backup
import tempfile
@ -391,8 +499,9 @@ def get_backup(**keys):
return static_file(os.path.basename(archivefile),root=tmpfolder)
@api.get("export")
@authenticated_api
@authenticated_function(api=True)
def get_export(**keys):
"""Internal Use Only"""
from ..proccontrol.tasks.export import export
import tempfile
@ -403,6 +512,7 @@ def get_export(**keys):
@api.post("delete_scrobble")
@authenticated_api
@authenticated_function(api=True)
def delete_scrobble(timestamp):
"""Internal Use Only"""
database.remove_scrobble(timestamp)

View File

@ -128,14 +128,13 @@ def main(*args,**kwargs):
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
return False
return action(*args,**kwargs)
return True

View File

@ -3,8 +3,10 @@
<html>
<head>
<title>{% block title %}{% endblock %}</title>
<meta charset="UTF-8" />
<meta name="referrer" content="same-origin" />
<title>{% block title %}{% endblock %}</title>
<meta name="description" content='Maloja is a self-hosted music scrobble server.' />
<meta name="color-scheme" content="dark" />

View File

@ -2,14 +2,14 @@
{% import 'snippets/links.jinja' as links %}
{% if 'artists' in entity %}
{% if entity is mapping and 'artists' in entity %}
{% set img = images.get_track_image(entity) %}
{% else %}
{% set img = images.get_artist_image(entity) %}
{% endif %}
<td class='icon'><div style="background-image:url('{{ img }}')"></div></td>
{% if "artists" in entity %}
{% if entity is mapping and 'artists' in entity %}
{% if settings['TRACK_SEARCH_PROVIDER'] %}
<td class='searchProvider'>{{ links.link_search(entity) }}</td>
{% endif %}

View File

@ -1,5 +1,5 @@
{% macro link(entity) -%}
{% if 'artists' in entity %}
{% if entity is mapping and 'artists' in entity %}
{% set name = entity.title %}
{% else %}
{% set name = entity %}
@ -17,7 +17,7 @@
{% macro url(entity) %}
{% if 'artists' in entity -%}
{% if entity is mapping and 'artists' in entity -%}
{{ mlj_uri.create_uri("/track",{'track':entity}) }}
{%- else -%}
{{ mlj_uri.create_uri("/artist",{'artist':entity}) }}

View File

@ -1,6 +1,6 @@
[project]
name = "malojaserver"
version = "3.0.2"
version = "3.0.3"
description = "Self-hosted music scrobble database"
readme = "./README.md"
requires-python = ">=3.6"
@ -21,7 +21,7 @@ classifiers = [
dependencies = [
"bottle>=0.12.16",
"waitress>=1.3",
"doreah>=1.9.0, <2",
"doreah>=1.9.1, <2",
"nimrodel>=0.8.0",
"setproctitle>=1.1.10",
#"pyvips>=2.1.16",

View File

@ -1,6 +1,6 @@
bottle>=0.12.16
waitress>=1.3
doreah>=1.9.0, <2
doreah>=1.9.1, <2
nimrodel>=0.8.0
setproctitle>=1.1.10
jinja2>=2.11