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

Compare commits

...

23 Commits

Author SHA1 Message Date
krateng
b31e778d95 Made incomplete merging process a bit less permanent 2022-04-17 20:23:49 +02:00
krateng
6e8cbe6a57 Added callback notifications to edit functions 2022-04-17 20:18:44 +02:00
krateng
45ea7499b2 Added some return values to database 2022-04-17 20:18:26 +02:00
krateng
77c4dac7be Merge branch 'master' into feature-webedit 2022-04-17 19:30:39 +02:00
krateng
61526fdc89 Added basic notification system to web interface 2022-04-17 19:30:27 +02:00
krateng
ea6d70a650 Implemented experimental merging server-side 2022-04-17 17:38:38 +02:00
krateng
57e66fdafd Added client logic for merging 2022-04-17 17:24:23 +02:00
krateng
0d985ff706 Reorganized admin mode icons 2022-04-17 16:46:02 +02:00
krateng
27a9543da9 Added merge icons 2022-04-17 16:16:05 +02:00
krateng
977385a700 Fixed editing with special characters 2022-04-17 15:37:08 +02:00
krateng
c8522bd473 Updated changelog 2022-04-17 15:31:12 +02:00
krateng
83e3157ad1 Can now cancel editing 2022-04-17 15:15:29 +02:00
krateng
0525ff400b Merge branch 'feature-restructure' into feature-webedit 2022-04-17 04:45:51 +02:00
krateng
13856a2347 Merge branch 'master' into feature-restructure 2022-04-17 04:44:28 +02:00
krateng
e9bf65da34 Completed old tag information 2022-04-16 22:08:54 +02:00
krateng
5bf66ab270 Added release documentation 2022-04-16 20:40:37 +02:00
krateng
206ebd58ea Version bump 2022-04-16 18:47:07 +02:00
krateng
a642c274e3
Merge pull request #116 from ICTman1076/patch-4
Add artist to specialsymbols
2022-04-16 18:46:33 +02:00
ICTman1076
8ba973ed91
Add artist to specialsymbols 2022-04-16 16:35:21 +00:00
krateng
ca726c774a Removed duplicate track artist entries, fix GH-115 2022-04-16 18:19:25 +02:00
krateng
33bbe61ece Small fixes 2022-04-16 16:21:24 +02:00
krateng
15f815ffe9 Improved native API error feedback 2022-04-16 15:59:42 +02:00
krateng
6601920f69 Fixed entrypoint 2022-04-16 02:17:43 +02:00
40 changed files with 798 additions and 94 deletions

3
dev/releases/1.0.yml Normal file
View File

@ -0,0 +1,3 @@
minor_release_name: "Yura"
'1.0':
commit: "1fac2ca965fdbe40c85a88559d5b736f4829e7b0"

3
dev/releases/1.1.yml Normal file
View File

@ -0,0 +1,3 @@
minor_release_name: "Solar"
'1.1':
commit: "5603ca9eb137516e604e9e3e83e273a70ef32f65"

3
dev/releases/1.2.yml Normal file
View File

@ -0,0 +1,3 @@
minor_release_name: "Jeonghwa"
'1.2':
commit: "d46d2be2bf27ef40ddd9f0c077f86dcf0214adbb"

3
dev/releases/1.3.yml Normal file
View File

@ -0,0 +1,3 @@
minor_release_name: "IU"
'1.3':
commit: "0bf1790a7cc0174b84f8c25dade6b221b13d65e9"

3
dev/releases/1.4.yml Normal file
View File

@ -0,0 +1,3 @@
minor_release_name: "Chungha"
'1.4':
commit: "981c0e4ae2ad1bff5a0778b6fa34916b0c4d4f4a"

3
dev/releases/1.5.yml Normal file
View File

@ -0,0 +1,3 @@
minor_release_name: "Seulgi"
'1.5':
commit: "e282789153ec3df133474a56e8d922a73795b72a"

5
dev/releases/2.0.yml Normal file
View File

@ -0,0 +1,5 @@
minor_release_name: "Irene"
'2.0':
commit: "55621ef4efdf61c3092d42565e897dfbaa0244c8"
notes:
- "[Architecture] Refactored into Python Package"

5
dev/releases/2.1.yml Normal file
View File

@ -0,0 +1,5 @@
minor_release_name: "Jennie"
2.1.0:
commit: "b87379ed986640788201f1ff52826413067e5ffb"
2.1.4:
commit: "c95ce17451cb19b4775a819f82a532d3a3a6231b"

17
dev/releases/2.10.yml Normal file
View File

@ -0,0 +1,17 @@
minor_release_name: "Yeri"
2.10.0:
commit: "ce9d882856be8f6caca14ab7e5b9f13d6c31940b"
2.10.1:
commit: "f555ee9d9fc485c6a241f4a8fa88bd68527ed2e2"
2.10.2:
commit: "9a6617b4b1117a6e53d818aadcda886c831e16db"
2.10.3:
commit: "5b7d1fd8e9f70a8bf9c5bfbe4aca5b796578e114"
2.10.4:
commit: "3cf0dd9767fe62702b2f5c4f0267a234338a972b"
2.10.5:
commit: "034bd064f11ef18ebbfcb25bd8acac8eacce1324"
2.10.6:
commit: "f62fd254dd44deca50a860f3a966390ae9c3662c"
2.10.7:
commit: "212fbf368e38281f45a0d8dd63dc051dbd8cd8cf"

3
dev/releases/2.11.yml Normal file
View File

@ -0,0 +1,3 @@
minor_release_name: "Akali"
2.11.0:
commit: "218313f80c160f90b28d99236a062ef62db7260d"

41
dev/releases/2.12.yml Normal file
View File

@ -0,0 +1,41 @@
minor_release_name: "Tzuyu"
2.12.0:
commit: "723efcb8ba12f7bda9acc21d81d9930265881c15"
2.12.1:
commit: "a42ed56d2de47f88f873737f0f1374e99be895bf"
2.12.2:
commit: "5006ad2bf1a6d5132ed07d28a7f6f0a9a454d5a7"
2.12.3:
commit: "2a5d9498d1dd7bb6ac62a27d518a87542ec3f344"
2.12.4:
commit: "06c32e205e95df8d3d1e27876887b7da7aa2bdf4"
2.12.5:
commit: "c2f8ecc2dfa1ac4febde228fce150e08fb47be38"
2.12.6:
commit: "a652a22a96ab693a4e7c3271520e6ad79fa025af"
2.12.7:
commit: "5455abd0d1c9ecc8e000d04257f721babacb18e9"
2.12.8:
commit: "8ebd27ab76ad2dcaf1dfef0cc171900fa20d5ee5"
2.12.9:
commit: "49598e914f4e2d7895959bea954238db8a6cee78"
2.12.10:
commit: "9037403777faa089092cf103181027febf6f0340"
2.12.12:
commit: "eaaa0f3b53aa102ba1eb709c1803e94752017d86"
2.12.13:
commit: "c31770a34c96cecc87af78f861819cc49fe98dda"
2.12.14:
commit: "5157ce825eea9bf7b74123cb02dd28e25c6a0767"
2.12.15:
commit: "33af60ed2c8c980f17338827a9cb96e7f2fd2572"
2.12.16:
commit: "26dfdfb569d0beaf4ba8c6c67a9e2295d1362eed"
2.12.17:
commit: "21012234409c01fec3cb5c506f0b4ba74b735b0b"
2.12.18:
commit: "59eaa2264aefa6c9ed5f38e8490f77150bcae27b"
2.12.19:
commit: "8958eb1b547f07d5d063c46cbe59ec57e000ecae"
2.12.20:
commit: "7774d9a9361db986092e143e1bc397ce7a7524dd"

11
dev/releases/2.13.yml Normal file
View File

@ -0,0 +1,11 @@
minor_release_name: "Aqua"
2.13.0:
commit: "8555b28fbc9a220577260014b7f71f433263cb9f"
2.13.1:
commit: "cefed03bc95dd5641b918f79b6ed14b2bfc9898d"
2.13.2:
commit: "0f5ccd4645ead8d1ad48a532752d401424edb236"
2.13.3:
commit: "40648b66f36894a297633c650e570ac77555d143"
2.13.4:
commit: "0ccd39ffd99b19e4cd1b1a14f97bfb4385662eeb"

23
dev/releases/2.14.yml Normal file
View File

@ -0,0 +1,23 @@
minor_release_name: "Mina"
2.14.0:
commit: "1b0e3ffdb2389ae6ca484c78840756d0b7e5c0be"
2.14.1:
commit: "fb2dff8addc7eaf740c5e30cbcd6791aab882c56"
2.14.2:
commit: "cd8e0ff90abf7d01761b0576c4168254b9b1f7c1"
2.14.3:
commit: "f806fb8ed24dd1474b80e7b1a9a7637cdbd35905"
2.14.4:
commit: "868b8396a0a4ff0f687e651772d746af6d9dfab1"
2.14.5:
commit: "21d1643988e40a02531bcc708f43925789d854d1"
2.14.6:
commit: "ccbb3d3a807fd77a1481f9d44f311c7f8df659c7"
2.14.7:
commit: "634df2ffafdfa00b6caf981108d333e30bf160f8"
2.14.8:
commit: "ec5723d2b3122faaa5b76c5a1b156c9a915af9d6"
2.14.9:
commit: "2c73c81434e1a591685a4b1d267a9eb6dbd57174"
2.14.10:
commit: "e152a2edde836f8fb30427d13eb1e9e0d591a00b"

11
dev/releases/2.2.yml Normal file
View File

@ -0,0 +1,11 @@
minor_release_name: "Rosé"
2.2.0:
commit: "33cea26a791e224625aa9bc523e2cf90e39c8a50"
2.2.1:
commit: "fbce600c4edd2b530e6673b89513b1a26b068b64"
2.2.2:
commit: "c518627670f5614a2b9931471337a1a6b2ee344f"
2.2.3:
commit: "a2cc27ddd46c7cf9959f33478eac396e18f90055"
2.2.4:
commit: "c6deb1543779ce8b09af6bcbdc35e7668af86010"

19
dev/releases/2.3.yml Normal file
View File

@ -0,0 +1,19 @@
minor_release_name: "Nancy"
2.3.0:
commit: "8793b149f501fe5f3e237d7ae0fcd23c8f4e5e9d"
2.3.1:
commit: "7c6e2ad60f15d8c4ac85a0808a0abd07549a4a2b"
2.3.2:
commit: "5a08fd78c69c4047b82ff9c394ea23d25356758e"
2.3.3:
commit: "9cf1fb3ed83817168dfe2ac30a42dcadb080c043"
2.3.4:
commit: "eb82282e58259b243958e7590506bd26f8e92db0"
2.3.5:
commit: "a4f13f6923b7783509462944f1abb235b4a068d0"
2.3.6:
commit: "b611387011e4cbd274e210d0c21c83d15302281c"
2.3.7:
commit: "b17060184b6897b18cf8af28a3817c9989aac96f"
2.3.8:
commit: "afe01c8341acd4cf9f4b84fbba85aab6777fd230"

29
dev/releases/2.4.yml Normal file
View File

@ -0,0 +1,29 @@
minor_release_name: "Songhee"
2.4.0:
commit: "6aa65bf1ce273d9fd36d44f6e24439981b2228a3"
2.4.1:
commit: "b117e6f7ec80afc6210314ce97bac087d5ab7b54"
2.4.2:
commit: "d989134e65c20ab33b0ea8e4a132655074057757"
2.4.3:
commit: "9b787fa3b13d77a9cfbe21061f519defac7fafd0"
2.4.4:
commit: "948772b1c26070d7814871824b970fb60fc6976d"
2.4.5:
commit: "2da5ab83b3a410b02af48e70b298069218a7e2a3"
2.4.6:
commit: "65f9c88da4d56df37e4a3f974d7f660502c7a310"
2.4.7:
commit: "c166620d5f9706e54f9cd67044d42bf8583575d8"
2.4.8:
commit: "98c1527f778958b1a3322a4f026cfe2c421388aa"
2.4.9:
commit: "b21b27bb6e230901281bb524f84e177c937b48fd"
2.4.10:
commit: "08fe4695f6d5ef09789688481db478d0decbd5df"
2.4.11:
commit: "5c6a901f5118be54ae44affbd6881b14bc30e04a"
2.4.12:
commit: "6658165baedeee3939084ba4500de3de06bbc045"
2.4.13:
commit: "57403a89ab1d679523341d6a607d0b03e495ff35"

9
dev/releases/2.5.yml Normal file
View File

@ -0,0 +1,9 @@
minor_release_name: "Seungeun"
2.5.0:
commit: "990131f546876d1461bac745e5cab3e60c78d038"
2.5.1:
commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c"
2.5.2:
commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c"
2.5.3:
commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c"

21
dev/releases/2.6.yml Normal file
View File

@ -0,0 +1,21 @@
minor_release_name: "HyunA"
2.6.0:
commit: "b161da1c1a1632725a44e998ff0d1872b3d5d184"
2.6.1:
commit: "1eae55e3bba335d41da0d21dfc383b838d9f0d03"
2.6.2:
commit: "dd3c83920b668466f2c053434bfd6be93bf32942"
2.6.3:
commit: "27f3ff6d085f42bdb67385f967db904022339d1d"
2.6.4:
commit: "5f8e73e6c714e9ca94a66f48d1b72fe516bbb0da"
2.6.5:
commit: "0fdd7669cced6c2b47f657e510bda03a053ee7ae"
2.6.6:
commit: "87cdb9987efe08b466f99f9ccb8b808131f9fbcd"
2.6.7:
commit: "0bdc4654bfb0f42d838e15c3d36dab0b4472db00"
2.6.8:
commit: "bdfb2a4a0b48362aabda7bb735296d83a02b932d"
2.6.9:
commit: "cb7a6d224152048176e6187ede6d60625961ab39"

25
dev/releases/2.7.yml Normal file
View File

@ -0,0 +1,25 @@
minor_release_name: "Shanshan"
2.7.0:
commit: "8d7fb9a2c8be3f813ee5994be1818f9f81088faa"
2.7.1:
commit: "6885fbdeccb8b690fa0af59d8fd341e44803798f"
2.7.2:
commit: "4113d1761e28a7ee3b3cdabe4404cf3876f1fc84"
2.7.3:
commit: "1563a15abde175022b50fa085c6b9b19a6021c31"
2.7.4:
commit: "3e6bcc45d55446c6607664e407768391b47c5421"
2.7.5:
commit: "fa05c406606e269fb4153465611caeb71c12b486"
2.7.6:
commit: "47087b4288cbfa6000ca019a000f27ee5846d161"
2.7.7:
commit: "379ee49f1c61df9720346d3d021dea040587d54d"
2.7.8:
commit: "75bd823ad0cc24efecd1de193436a28dfaecd4f3"
2.7.9:
commit: "fb04dd507cee42092b889fe72cdf9975ea48e3b1"
2.7.10:
commit: "7fc879f77818371721e21c13e9df98796cf632de"
2.7.11:
commit: "44a2739a3b6e58cb90b7f7dfca2197834cf30464"

13
dev/releases/2.8.yml Normal file
View File

@ -0,0 +1,13 @@
minor_release_name: "Haeun"
2.8.0:
commit: "25661f82af9338a024aae429cdafec7c86692aa5"
2.8.1:
commit: "1321fcb45ebe0291c9fd47ff2eb8cc329035acf3"
2.8.2:
commit: "e27a83bdc99a06a207c67c6f0034bc0a554c89af"
2.8.3:
commit: "6acab324dbd3594dcfbf944bfdfb5c8fe173354b"
2.8.4:
commit: "f7f1b1225e64b54d8962467182bddcc1de237f51"
2.8.5:
commit: "1dbc0b7fca05830d654076c74a91b6b74f470d5b"

23
dev/releases/2.9.yml Normal file
View File

@ -0,0 +1,23 @@
minor_release_name: "Yaorenmao"
2.9.0:
commit: "8b4e9609e994d74506fd91471bd5a622b75b2f08"
2.9.1:
commit: "52a9faae90175841b2c259dd4677697e513e12f9"
2.9.2:
commit: "5cf7ca2e9bbf66082c4afb76b4033ff17c9cf8c8"
2.9.3:
commit: "e8c316f1992c3e5f171891272f32d959bb1fa4f0"
2.9.4:
commit: "e8a87cb8a5e2f63850ff3c02ed5aa8ee388460ed"
2.9.5:
commit: "09d3f103832bb7e26949a8f2df60c25851886bdc"
2.9.6:
commit: "9fb352cc6fe2bc41c56304e5ba941035fc1ac82d"
2.9.7:
commit: "f4a563f080f7dba336034feb1c0c42057f8d8d8c"
2.9.8:
commit: "2da9f154be240b8648d68a7eb2a3291738cfc93c"
2.9.9:
commit: "f7861c44b4a44b0cdd34e9f3f62530b8bf2837e3"
2.9.10:
commit: "22172d8b57df2ad1282f8d835183be45843fdd6a"

30
dev/releases/3.0.yml Normal file
View File

@ -0,0 +1,30 @@
minor_release_name: "Yeonhee"
3.0.0:
commit: "f31c95228eb2dc01e661be928ffd881c063377da"
notes:
- "[Architecture] Switched to SQLite for main database"
- "[Architecture] Switched to SQLite for artwork cache"
- "[Feature] Added scrobble deletion from web interface"
3.0.1:
commit: "700b81217cb585df631d6f069243c56074cd1b71"
notes:
- "[Bugfix] Fixed upgrading imported scrobbles"
3.0.2:
commit: "4a8221f7a08f679b21c1fb619f03e5f922a1dc2b"
notes:
- "[Logging] Cleaned up output for waitress warnings"
- "[Bugfix] Fixed exception in native API"
3.0.3:
commit: "1d9247fc724d7410b6e50d2cbfaa8f375d5e70af"
notes:
- "[Documentation] Added descriptions for native API endpoints"
- "[Code Health] Made arguments for native API scrobbling explicit"
- "[Bugfix] Fixed faulty entity type recognition for artists including the string 'artists'"
- "[Bugfix] Fixed OS return codes"
3.0.4:
commit: "206ebd58ea204e0008f2c9bf72d76dd9918fec53"
notes:
- "[Feature] Enabled dual stack for web server"
- "[Feature] Added better feedback to native API endpoints"
- "[Bugfix] Fixed native API receiving superfluous keywords"
- "[Bugfix] Fixed crash when importing scrobbles with artists with similar names"

2
dev/releases/branch.yml Normal file
View File

@ -0,0 +1,2 @@
- "[Feature] Implemented track title and artist name editing from web interface"
- "[Feature] Implemented track and artist merging from web interface"

52
dev/write_tags.py Normal file
View File

@ -0,0 +1,52 @@
import os
import subprocess as sp
import yaml
FOLDER = "dev/releases"
releases = {}
for f in os.listdir(FOLDER):
#maj,min = (int(i) for i in f.split('.')[:2])
with open(os.path.join(FOLDER,f)) as fd:
data = yaml.safe_load(fd)
name = data.pop('minor_release_name')
for tag in data:
tagtup = tuple(int(i) for i in tag.split('.'))
releases[tagtup] = data[tag]
# this is a bit dirty, works on our data
if len(tagtup)<3 or tagtup[2] == 0: releases[tagtup]['name'] = name
for version in releases:
info = releases[version]
version = '.'.join(str(v) for v in version)
msg = [
f"Version {version}" + (f" '{info.get('name')}'" if info.get('name') else ''),
*([""] if info.get('notes') else []),
*[f"* {n}" for n in info.get('notes',[])]
]
cmd = [
'git','tag','--force',
'-a',f'v{version}',
'-m',
'\n'.join(msg),
info['commit']
]
try:
prev_tag = sp.check_output(["git","show",f'v{maj}.{min}.{hot}']).decode()
prev_tag_commit = prev_tag.split('\n')[6].split(" ")[1]
except:
pass
else:
assert prev_tag_commit == info['commit']
print(cmd)
sp.run(cmd)

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.3"
VERSION = "3.0.4"
HOMEPAGE = "https://github.com/krateng/maloja"

View File

@ -37,6 +37,28 @@ api.__apipath__ = "mlj_1"
errors = {
database.MissingScrobbleParameters: lambda e: (400,{
"status":"failure",
"error":{
'type':'missing_scrobble_data',
'value':e.params,
'desc':"A scrobble requires these parameters."
}
}),
Exception: lambda e: (500,{
"status":"failure",
"error":{
'type':'unknown_error',
'value':e.__repr__(),
'desc':"The server has encountered an exception."
}
})
}
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'"
@ -84,24 +106,28 @@ def test_server(key=None):
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"}
return {
"status":"error",
"error":"Wrong API key"
}
else:
response.status = 200
return {"status":"ok"}
return {
"status":"ok"
}
@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.
: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","*")
response.set_header("Content-Type","application/json")
return {
"name":malojaconfig["NAME"],
@ -131,7 +157,9 @@ def get_scrobbles_external(**keys):
result = result[offset:]
if k_amount.get('perpage') is not math.inf: result = result[:k_amount.get('perpage')]
return {"list":result}
return {
"list":result
}
@api.get("numscrobbles")
@ -146,7 +174,10 @@ def get_scrobbles_num_external(**keys):
ckeys = {**k_filter, **k_time, **k_amount}
result = database.get_scrobbles_num(**ckeys)
return {"amount":result}
return {
"amount":result
}
@ -162,7 +193,10 @@ def get_tracks_external(**keys):
ckeys = {**k_filter}
result = database.get_tracks(**ckeys)
return {"list":result}
return {
"list":result
}
@ -174,7 +208,10 @@ def get_artists_external():
:return: list (List)
:rtype: Dictionary"""
result = database.get_artists()
return {"list":result}
return {
"list":result
}
@ -191,7 +228,10 @@ def get_charts_artists_external(**keys):
ckeys = {**k_time}
result = database.get_charts_artists(**ckeys)
return {"list":result}
return {
"list":result
}
@ -206,7 +246,10 @@ def get_charts_tracks_external(**keys):
ckeys = {**k_filter, **k_time}
result = database.get_charts_tracks(**ckeys)
return {"list":result}
return {
"list":result
}
@ -222,7 +265,10 @@ def get_pulse_external(**keys):
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
results = database.get_pulse(**ckeys)
return {"list":results}
return {
"list":results
}
@ -238,7 +284,10 @@ def get_performance_external(**keys):
ckeys = {**k_filter, **k_time, **k_internal, **k_amount}
results = database.get_performance(**ckeys)
return {"list":results}
return {
"list":results
}
@ -254,7 +303,10 @@ def get_top_artists_external(**keys):
ckeys = {**k_time, **k_internal}
results = database.get_top_artists(**ckeys)
return {"list":results}
return {
"list":results
}
@ -272,7 +324,10 @@ def get_top_tracks_external(**keys):
# IMPLEMENT THIS FOR TOP TRACKS OF ARTIST AS WELL?
results = database.get_top_tracks(**ckeys)
return {"list":results}
return {
"list":results
}
@ -325,7 +380,7 @@ def post_scrobble(
"""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. Overwritten by artist parameter.
: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.
@ -339,7 +394,7 @@ def post_scrobble(
"""
rawscrobble = {
'track_artists':artist if artist is not None else artists,
'track_artists':(artist or []) + artists,
'track_title':title,
'album_name':album,
'album_artists':albumartists,
@ -351,15 +406,15 @@ 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]}
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)
)
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)
)
if result:
response = {
responsedict = {
'status': 'success',
'track': {
'artists':result['track']['artists'],
@ -367,14 +422,24 @@ def post_scrobble(
}
}
if extra_kwargs:
response['warnings'] = [
{'type':'invalid_keyword_ignored','value':k}
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
]
else:
response = {"status":"failure"}
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]
return response
@ -523,17 +588,45 @@ def get_export(**keys):
@authenticated_function(api=True)
def delete_scrobble(timestamp):
"""Internal Use Only"""
return database.remove_scrobble(timestamp)
result = database.remove_scrobble(timestamp)
return {
"status":"success"
}
@api.post("edit_artist")
@authenticated_function(api=True)
def edit_artist(id,name):
"""Internal Use Only"""
return database.edit_artist(id,name)
result = database.edit_artist(id,name)
return {
"status":"success"
}
@api.post("edit_track")
@authenticated_function(api=True)
def edit_track(id,title):
"""Internal Use Only"""
return database.edit_track(id,{'title':title})
result = database.edit_track(id,{'title':title})
return {
"status":"success"
}
@api.post("merge_tracks")
@authenticated_function(api=True)
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)
def merge_artists(target_id,source_ids):
"""Internal Use Only"""
result = database.merge_artists(target_id,source_ids)
return {
"status":"success"
}

View File

@ -9,3 +9,4 @@ belongtogether Case & Point
belongtogether Selena Gomez & The Scene
belongtogether Gerry & The Pacemakers
belongtogether AC/DC
belongtogether Au/Ra

Can't render this file because it has a wrong number of fields in line 4.

View File

@ -51,6 +51,11 @@ class DatabaseNotBuilt(HTTPError):
)
class MissingScrobbleParameters(Exception):
def __init__(self,params=[]):
self.params = params
def waitfordb(func):
def newfunc(*args,**kwargs):
if not dbstatus['healthy']: raise DatabaseNotBuilt()
@ -86,10 +91,14 @@ cla = CleanerAgent()
def incoming_scrobble(rawscrobble,fix=True,client=None,api=None,dbconn=None):
if (not "track_artists" in rawscrobble) or (len(rawscrobble['track_artists']) == 0) or (not "track_title" in rawscrobble):
missing = []
for necessary_arg in ["track_artists","track_title"]:
if not necessary_arg in rawscrobble or len(rawscrobble[necessary_arg]) == 0:
missing.append(necessary_arg)
if len(missing) > 0:
log(f"Invalid Scrobble [Client: {client} | API: {api}]: {rawscrobble} ",color='red')
#return {"status":"failure"}
return False
raise MissingScrobbleParameters(missing)
log(f"Incoming scrobble [Client: {client} | API: {api}]: {rawscrobble}")
@ -137,23 +146,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}")
sqldb.edit_artist(id,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']}")
sqldb.edit_track(id,trackinfo)
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

View File

@ -275,7 +275,9 @@ def delete_scrobble(scrobble_id,dbconn=None):
DB['scrobbles'].c.timestamp == scrobble_id
)
dbconn.execute(op)
result = dbconn.execute(op)
return True
### these will 'get' the ID of an entity, creating it if necessary
@ -284,6 +286,7 @@ def delete_scrobble(scrobble_id,dbconn=None):
def get_track_id(trackdict,dbconn=None):
ntitle = normalize_name(trackdict['title'])
artist_ids = [get_artist_id(a) for a in trackdict['artists']]
artist_ids = list(set(artist_ids))
@ -366,6 +369,8 @@ def edit_artist(id,artistdict,dbconn=None):
)
result = dbconn.execute(op)
return True
@connection_provider
def edit_track(id,trackdict,dbconn=None):
dbentry = track_dict_to_db(trackdict)
@ -377,6 +382,37 @@ def edit_track(id,trackdict,dbconn=None):
)
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
### Functions that get rows according to parameters

View File

@ -18,6 +18,7 @@
<script src="/search.js"></script>
<script src="/neopolitan.js"></script>
<script src="/upload.js"></script>
<script src="/notifications.js"></script>
<link rel="preload" href="/static/ttf/Ubuntu-Regular.ttf" as="font" type="font/woff2" crossorigin />
@ -49,6 +50,9 @@
{% endblock %}
{% endblock %}
<div id="notification_area">
</div>
<div class="footer">
@ -80,9 +84,11 @@
</div>
</div>
<a href="/admin_overview">
<div id="icon_bar">
{% block icon_bar %}{% endblock %}
{% include 'icons/settings.jinja' %}
</a>
</div>
</body>
</html>

View File

@ -6,6 +6,7 @@
{% block scripts %}
<script src="/rangeselect.js"></script>
<script src="/edit.js"></script>
{% endblock %}
{% set artist = filterkeys.artist %}
@ -26,12 +27,20 @@
{% 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' %}
{% endif %}
{% endblock %}
{% block content %}
<script>
const entity_id = {{ info.id }};
const entity_type = 'artist';
const entity_name = {{ artist | tojson }};
</script>
@ -51,7 +60,6 @@
</td>
<td class="text">
<h1 id="main_entity_name" class="headerwithextra">{{ info.artist }}</h1>
{% if adminmode %}{% include 'icons/edit.jinja' %}{% endif %}
{% if competes %}<span class="rank"><a href="/charts_artists?max=100">#{{ info.position }}</a></span>{% endif %}
<br/>
{% if competes and included %}
@ -60,7 +68,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>

View File

@ -1,6 +1,5 @@
<script src="/edit.js"></script>
<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" x="20px" y="20px"
<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

View File

@ -0,0 +1,8 @@
<div title="Merge" id="mergeicon" class="clickable_icon" 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>

View File

@ -0,0 +1,5 @@
<div title="Mark for merging" id="mergemarkicon" class="clickable_icon" 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>

View File

@ -1,5 +1,7 @@
<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">
<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>

View File

@ -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,11 +22,20 @@
{% 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' %}
{% endif %}
{% endblock %}
{% block content %}
<script>
const entity_id = {{ info.id }};
const entity_type = 'track';
const entity_name = {{ track.title | tojson }};
</script>
@ -48,7 +58,6 @@
<td class="text">
<span>{{ links.links(track.artists) }}</span><br/>
<h1 id="main_entity_name" class="headerwithextra">{{ info.track.title }}</h1>
{% if adminmode %}{% include 'icons/edit.jinja' %}{% endif %}
{{ awards.certs(track) }}
<span class="rank"><a href="/charts_tracks?max=100">#{{ info.position }}</a></span>
<br/>

View File

@ -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,15 +77,6 @@ div.clickable_icon.danger:hover {
fill: red;
}
div#settingsicon {
position:fixed;
right:30px;
top:30px;
}
div#editicon {
height:20px;
width:20px;
}
/**
@ -189,6 +190,29 @@ div.searchresults table.searchresults_tracks td span:nth-child(1) {
}
/**
Notifications
**/
div#notification_area {
position: fixed;
width:420px;
bottom:40px;
right:20px;
}
div#notification_area div.notification {
background-color:white;
width:400px;
height:100px;
margin-bottom:7px;
padding:9px;
opacity:0.4;
}
div#notification_area div.notification:hover {
opacity:0.95;
}
@media (max-width: 1000px) {
div.footer {

View File

@ -1,17 +1,6 @@
// JS for all web interface editing / deletion of scrobble data
function toggleDeleteConfirm(element) {
element.parentElement.parentElement.classList.toggle('active');
}
function deleteScrobble(id,element) {
element.parentElement.parentElement.parentElement.classList.add('removed');
neo.xhttpreq("/apis/mlj_1/delete_scrobble",data={'timestamp':id},method="POST",callback=(()=>null),json=true);
}
// HELPERS
function selectAll(e) {
// https://stackoverflow.com/a/6150060/6651341
var range = document.createRange();
@ -21,30 +10,56 @@ function selectAll(e) {
sel.addRange(range);
}
// DELETION
function toggleDeleteConfirm(element) {
element.parentElement.parentElement.classList.toggle('active');
}
function deleteScrobble(id,element) {
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=callback_func,json=true);
}
// EDIT NAME
function editEntity() {
var namefield = document.getElementById('main_entity_name');
namefield.contentEditable = "plaintext-only";
// dont allow new lines, done on enter
namefield.addEventListener('keypress',function(e){
if (e.which === 13) {
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.innerHTML.includes("\n")) {
namefield.innerHTML = namefield.innerHTML.replace("\n","");
if (namefield.textContent.includes("\n")) {
namefield.textContent = namefield.textContent.replace("\n","");
}
})
// manually clicking away OR enter
namefield.addEventListener('blur',function(e){
doneEditing();
})
namefield.focus();
@ -52,27 +67,82 @@ function editEntity() {
}
function doneEditing() {
window.getSelection().removeAllRanges();
var namefield = document.getElementById('main_entity_name');
namefield.contentEditable = "false";
newname = document.getElementById('main_entity_name').innerHTML;
var searchParams = new URLSearchParams(window.location.search);
newname = namefield.textContent;
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}
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);
}
};
neo.xhttpreq(
endpoint,
data=payload,
method="POST",
callback=callback_func,
json=true
);
}
}
// MERGING
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!")
}
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(
endpoint,
data=payload,
"/apis/mlj_1/merge_" + entity_type + "s",
data={
'source_ids':current_stored,
'target_id':entity_id
},
method="POST",
callback=(()=>window.location = "?" + searchParams.toString()),
callback=callback_func,
json=true
);
lcst.removeItem(key);
}

View File

@ -0,0 +1,51 @@
// JS for feedback to the user whenever any XHTTP action is taken
const colors = {
'warning':'red',
'info':'green'
}
const notification_template = info => `
<div class="notification" style="background-color:${colors[info.notification_type]};">
<b>${info.title}</b><br/>
<span>${info.body}</span>
</div>
`
function htmlToElement(html) {
template = document.createElement('template');
html = html.trim();
template.innerHTML = html;
return template.content.firstChild;
}
function notify(title,msg,notification_type='info',reload=false) {
info = {
'title':title,
'body':msg,
'notification_type':notification_type
}
var element = htmlToElement(notification_template(info));
document.getElementById('notification_area').append(element);
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';
}
else {
var notification_type = 'warning';
}
notify(body.status,body.desc || "",notification_type);
}

View File

@ -1,6 +1,6 @@
[project]
name = "malojaserver"
version = "3.0.3"
version = "3.0.4"
description = "Self-hosted music scrobble database"
readme = "./README.md"
requires-python = ">=3.6"
@ -40,7 +40,7 @@ full = [
]
[project.scripts]
maloja = "maloja:main"
maloja = "maloja.__main__:main"
[build-system]
requires = ["flit_core >=3.2,<4"]