malojaserver 3.2.1__py3-none-any.whl → 3.2.3__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- maloja/__main__.py +1 -1
- maloja/__pkginfo__.py +1 -1
- maloja/apis/_base.py +26 -19
- maloja/apis/_exceptions.py +1 -1
- maloja/apis/audioscrobbler.py +35 -7
- maloja/apis/audioscrobbler_legacy.py +5 -5
- maloja/apis/listenbrainz.py +7 -5
- maloja/apis/native_v1.py +43 -26
- maloja/cleanup.py +9 -7
- maloja/data_files/config/rules/predefined/krateng_kpopgirlgroups.tsv +4 -2
- maloja/database/__init__.py +55 -23
- maloja/database/associated.py +10 -6
- maloja/database/exceptions.py +28 -3
- maloja/database/sqldb.py +216 -168
- maloja/dev/profiler.py +3 -4
- maloja/images.py +6 -0
- maloja/malojauri.py +2 -0
- maloja/pkg_global/conf.py +97 -72
- maloja/proccontrol/tasks/export.py +2 -1
- maloja/proccontrol/tasks/import_scrobbles.py +57 -15
- maloja/server.py +4 -5
- maloja/setup.py +56 -44
- maloja/thirdparty/lastfm.py +18 -17
- maloja/web/jinja/abstracts/base.jinja +1 -1
- maloja/web/jinja/admin_albumless.jinja +2 -0
- maloja/web/jinja/admin_overview.jinja +3 -3
- maloja/web/jinja/admin_setup.jinja +1 -1
- maloja/web/jinja/error.jinja +2 -2
- maloja/web/jinja/partials/album_showcase.jinja +1 -1
- maloja/web/jinja/partials/awards_album.jinja +1 -1
- maloja/web/jinja/partials/awards_artist.jinja +2 -2
- maloja/web/jinja/partials/charts_albums_tiles.jinja +4 -0
- maloja/web/jinja/partials/charts_artists_tiles.jinja +5 -1
- maloja/web/jinja/partials/charts_tracks_tiles.jinja +4 -0
- maloja/web/jinja/snippets/entityrow.jinja +2 -2
- maloja/web/jinja/snippets/links.jinja +3 -1
- maloja/web/static/css/maloja.css +14 -2
- maloja/web/static/css/startpage.css +2 -2
- maloja/web/static/js/manualscrobble.js +1 -1
- maloja/web/static/js/notifications.js +16 -8
- {malojaserver-3.2.1.dist-info → malojaserver-3.2.3.dist-info}/METADATA +10 -46
- {malojaserver-3.2.1.dist-info → malojaserver-3.2.3.dist-info}/RECORD +45 -45
- {malojaserver-3.2.1.dist-info → malojaserver-3.2.3.dist-info}/WHEEL +1 -1
- {malojaserver-3.2.1.dist-info → malojaserver-3.2.3.dist-info}/LICENSE +0 -0
- {malojaserver-3.2.1.dist-info → malojaserver-3.2.3.dist-info}/entry_points.txt +0 -0
maloja/__main__.py
CHANGED
@@ -135,7 +135,7 @@ def debug():
|
|
135
135
|
def print_info():
|
136
136
|
print_header_info()
|
137
137
|
print(col['lightblue']("Configuration Directory:"),conf.dir_settings['config'])
|
138
|
-
print(col['lightblue']("
|
138
|
+
print(col['lightblue']("State Directory: "),conf.dir_settings['state'])
|
139
139
|
print(col['lightblue']("Log Directory: "),conf.dir_settings['logs'])
|
140
140
|
print(col['lightblue']("Network: "),f"Dual Stack, Port {conf.malojaconfig['port']}" if conf.malojaconfig['host'] == "*" else f"IPv{ip_address(conf.malojaconfig['host']).version}, Port {conf.malojaconfig['port']}")
|
141
141
|
print(col['lightblue']("Timezone: "),f"UTC{conf.malojaconfig['timezone']:+d}")
|
maloja/__pkginfo__.py
CHANGED
maloja/apis/_base.py
CHANGED
@@ -25,9 +25,20 @@ __logmodulename__ = "apis"
|
|
25
25
|
|
26
26
|
cla = CleanerAgent()
|
27
27
|
|
28
|
+
|
29
|
+
|
30
|
+
# wrapper method: calls handle. final net to catch exceptions and map them to the handlers proper json / xml response
|
31
|
+
# handle method: finds the method for this path / query. can only raise InvalidMethodException
|
32
|
+
# scrobble: NOT the exposed scrobble method - helper for all APIs to scrobble their results with self-identification
|
33
|
+
|
34
|
+
|
28
35
|
class APIHandler:
|
36
|
+
|
37
|
+
__apiname__: str
|
38
|
+
errors: dict
|
29
39
|
# make these classes singletons
|
30
40
|
_instance = None
|
41
|
+
|
31
42
|
def __new__(cls, *args, **kwargs):
|
32
43
|
if not isinstance(cls._instance, cls):
|
33
44
|
cls._instance = object.__new__(cls, *args, **kwargs)
|
@@ -62,37 +73,33 @@ class APIHandler:
|
|
62
73
|
|
63
74
|
try:
|
64
75
|
response.status,result = self.handle(path,keys)
|
65
|
-
except Exception:
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
76
|
+
except Exception as e:
|
77
|
+
for exc_type, exc_response in self.errors.items():
|
78
|
+
if isinstance(e, exc_type):
|
79
|
+
response.status, result = exc_response
|
80
|
+
log(f"Error with {self.__apiname__} API: {e} (Request: {path})")
|
81
|
+
break
|
70
82
|
else:
|
71
|
-
|
72
|
-
|
83
|
+
# THIS SHOULD NOT HAPPEN
|
84
|
+
response.status, result = 500, {"status": "Unknown error", "code": 500}
|
85
|
+
log(f"Unhandled Exception with {self.__apiname__} API: {e} (Request: {path})")
|
73
86
|
|
74
87
|
return result
|
75
|
-
#else:
|
76
|
-
# result = {"error":"Invalid scrobble protocol"}
|
77
|
-
# response.status = 500
|
78
88
|
|
79
89
|
|
80
90
|
def handle(self,path,keys):
|
81
91
|
|
82
92
|
try:
|
83
|
-
methodname = self.get_method(path,keys)
|
93
|
+
methodname = self.get_method(path, keys)
|
84
94
|
method = self.methods[methodname]
|
85
|
-
except
|
86
|
-
log("Could not find a handler for method
|
87
|
-
log("Keys: "
|
95
|
+
except KeyError:
|
96
|
+
log(f"Could not find a handler for method {methodname} in API {self.__apiname__}", module="debug")
|
97
|
+
log(f"Keys: {keys}", module="debug")
|
88
98
|
raise InvalidMethodException()
|
89
|
-
return method(path,keys)
|
99
|
+
return method(path, keys)
|
90
100
|
|
91
101
|
|
92
102
|
def scrobble(self,rawscrobble,client=None):
|
93
103
|
|
94
104
|
# fixing etc is handled by the main scrobble function
|
95
|
-
|
96
|
-
return database.incoming_scrobble(rawscrobble,api=self.__apiname__,client=client)
|
97
|
-
except Exception:
|
98
|
-
raise ScrobblingException()
|
105
|
+
return database.incoming_scrobble(rawscrobble,api=self.__apiname__,client=client)
|
maloja/apis/_exceptions.py
CHANGED
maloja/apis/audioscrobbler.py
CHANGED
@@ -21,13 +21,22 @@ class Audioscrobbler(APIHandler):
|
|
21
21
|
"track.scrobble":self.submit_scrobble
|
22
22
|
}
|
23
23
|
self.errors = {
|
24
|
-
BadAuthException:(400,{"error":6,"message":"Requires authentication"}),
|
25
|
-
InvalidAuthException:(401,{"error":4,"message":"Invalid credentials"}),
|
26
|
-
InvalidMethodException:(200,{"error":3,"message":"Invalid method"}),
|
27
|
-
InvalidSessionKey:(403,{"error":9,"message":"Invalid session key"}),
|
28
|
-
|
24
|
+
BadAuthException: (400, {"error": 6, "message": "Requires authentication"}),
|
25
|
+
InvalidAuthException: (401, {"error": 4, "message": "Invalid credentials"}),
|
26
|
+
InvalidMethodException: (200, {"error": 3, "message": "Invalid method"}),
|
27
|
+
InvalidSessionKey: (403, {"error": 9, "message": "Invalid session key"}),
|
28
|
+
Exception: (500, {"error": 8, "message": "Operation failed"})
|
29
29
|
}
|
30
30
|
|
31
|
+
# xml string escaping: https://stackoverflow.com/a/28703510
|
32
|
+
def xml_escape(self, str_xml: str):
|
33
|
+
str_xml = str_xml.replace("&", "&")
|
34
|
+
str_xml = str_xml.replace("<", "<")
|
35
|
+
str_xml = str_xml.replace("<", "<")
|
36
|
+
str_xml = str_xml.replace("\"", """)
|
37
|
+
str_xml = str_xml.replace("'", "'")
|
38
|
+
return str_xml
|
39
|
+
|
31
40
|
def get_method(self,pathnodes,keys):
|
32
41
|
return keys.get("method")
|
33
42
|
|
@@ -45,12 +54,22 @@ class Audioscrobbler(APIHandler):
|
|
45
54
|
token = keys.get("authToken")
|
46
55
|
user = keys.get("username")
|
47
56
|
password = keys.get("password")
|
57
|
+
format = keys.get("format") or "xml" # Audioscrobbler 2.0 uses XML by default
|
48
58
|
# either username and password
|
49
59
|
if user is not None and password is not None:
|
50
60
|
client = apikeystore.check_and_identify_key(password)
|
51
61
|
if client:
|
52
62
|
sessionkey = self.generate_key(client)
|
53
|
-
|
63
|
+
if format == "json":
|
64
|
+
return 200,{"session":{"key":sessionkey}}
|
65
|
+
else:
|
66
|
+
return 200,"""<lfm status="ok">
|
67
|
+
<session>
|
68
|
+
<name>%s</name>
|
69
|
+
<key>%s</key>
|
70
|
+
<subscriber>0</subscriber>
|
71
|
+
</session>
|
72
|
+
</lfm>""" % (self.xml_escape(user), self.xml_escape(sessionkey))
|
54
73
|
else:
|
55
74
|
raise InvalidAuthException()
|
56
75
|
# or username and token (deprecated by lastfm)
|
@@ -59,7 +78,16 @@ class Audioscrobbler(APIHandler):
|
|
59
78
|
key = apikeystore[client]
|
60
79
|
if md5(user + md5(key)) == token:
|
61
80
|
sessionkey = self.generate_key(client)
|
62
|
-
|
81
|
+
if format == "json":
|
82
|
+
return 200,{"session":{"key":sessionkey}}
|
83
|
+
else:
|
84
|
+
return 200,"""<lfm status="ok">
|
85
|
+
<session>
|
86
|
+
<name>%s</name>
|
87
|
+
<key>%s</key>
|
88
|
+
<subscriber>0</subscriber>
|
89
|
+
</session>
|
90
|
+
</lfm>""" % (self.xml_escape(user), self.xml_escape(sessionkey))
|
63
91
|
raise InvalidAuthException()
|
64
92
|
else:
|
65
93
|
raise BadAuthException()
|
@@ -23,11 +23,11 @@ class AudioscrobblerLegacy(APIHandler):
|
|
23
23
|
"scrobble":self.submit_scrobble
|
24
24
|
}
|
25
25
|
self.errors = {
|
26
|
-
BadAuthException:(403,"BADAUTH\n"),
|
27
|
-
InvalidAuthException:(403,"BADAUTH\n"),
|
28
|
-
InvalidMethodException:(400,"FAILED\n"),
|
29
|
-
InvalidSessionKey:(403,"BADSESSION\n"),
|
30
|
-
|
26
|
+
BadAuthException: (403, "BADAUTH\n"),
|
27
|
+
InvalidAuthException: (403, "BADAUTH\n"),
|
28
|
+
InvalidMethodException: (400, "FAILED\n"),
|
29
|
+
InvalidSessionKey: (403, "BADSESSION\n"),
|
30
|
+
Exception: (500, "FAILED\n")
|
31
31
|
}
|
32
32
|
|
33
33
|
def get_method(self,pathnodes,keys):
|
maloja/apis/listenbrainz.py
CHANGED
@@ -3,6 +3,7 @@ from ._exceptions import *
|
|
3
3
|
from .. import database
|
4
4
|
import datetime
|
5
5
|
from ._apikeys import apikeystore
|
6
|
+
from ..database.exceptions import DuplicateScrobble
|
6
7
|
|
7
8
|
from ..pkg_global.conf import malojaconfig
|
8
9
|
|
@@ -21,11 +22,12 @@ class Listenbrainz(APIHandler):
|
|
21
22
|
"validate-token":self.validate_token
|
22
23
|
}
|
23
24
|
self.errors = {
|
24
|
-
BadAuthException:(401,{"code":401,"error":"You need to provide an Authorization header."}),
|
25
|
-
InvalidAuthException:(401,{"code":401,"error":"Incorrect Authorization"}),
|
26
|
-
InvalidMethodException:(200,{"code":200,"error":"Invalid Method"}),
|
27
|
-
MalformedJSONException:(400,{"code":400,"error":"Invalid JSON document submitted."}),
|
28
|
-
|
25
|
+
BadAuthException: (401, {"code": 401, "error": "You need to provide an Authorization header."}),
|
26
|
+
InvalidAuthException: (401, {"code": 401, "error": "Incorrect Authorization"}),
|
27
|
+
InvalidMethodException: (200, {"code": 200, "error": "Invalid Method"}),
|
28
|
+
MalformedJSONException: (400, {"code": 400, "error": "Invalid JSON document submitted."}),
|
29
|
+
DuplicateScrobble: (200, {"status": "ok"}),
|
30
|
+
Exception: (500, {"code": 500, "error": "Unspecified server error."})
|
29
31
|
}
|
30
32
|
|
31
33
|
def get_method(self,pathnodes,keys):
|
maloja/apis/native_v1.py
CHANGED
@@ -7,7 +7,6 @@ from bottle import response, static_file, FormsDict
|
|
7
7
|
from inspect import signature
|
8
8
|
|
9
9
|
from doreah.logging import log
|
10
|
-
from doreah.auth import authenticated_function
|
11
10
|
|
12
11
|
# nimrodel API
|
13
12
|
from nimrodel import EAPI as API
|
@@ -15,7 +14,7 @@ from nimrodel import Multi
|
|
15
14
|
|
16
15
|
|
17
16
|
from .. import database
|
18
|
-
from ..pkg_global.conf import malojaconfig, data_dir
|
17
|
+
from ..pkg_global.conf import malojaconfig, data_dir, auth
|
19
18
|
|
20
19
|
|
21
20
|
|
@@ -82,6 +81,24 @@ errors = {
|
|
82
81
|
'desc':"This entity does not exist in the database."
|
83
82
|
}
|
84
83
|
}),
|
84
|
+
database.exceptions.DuplicateTimestamp: lambda e: (409,{
|
85
|
+
"status":"error",
|
86
|
+
"error":{
|
87
|
+
'type':'duplicate_timestamp',
|
88
|
+
'value':e.rejected_scrobble,
|
89
|
+
'desc':"A scrobble is already registered with this timestamp."
|
90
|
+
}
|
91
|
+
}),
|
92
|
+
database.exceptions.DuplicateScrobble: lambda e: (200,{
|
93
|
+
"status": "success",
|
94
|
+
"desc": "The scrobble is present in the database.",
|
95
|
+
"track": {},
|
96
|
+
"warnings": [{
|
97
|
+
'type': 'scrobble_exists',
|
98
|
+
'value': None,
|
99
|
+
'desc': 'This scrobble exists in the database (same timestamp and track). The submitted scrobble was not added.'
|
100
|
+
}]
|
101
|
+
}),
|
85
102
|
images.MalformedB64: lambda e: (400,{
|
86
103
|
"status":"failure",
|
87
104
|
"error":{
|
@@ -474,7 +491,7 @@ def get_top_artists_external(k_filter, k_limit, k_delimit, k_amount):
|
|
474
491
|
:rtype: Dictionary"""
|
475
492
|
|
476
493
|
ckeys = {**k_limit, **k_delimit}
|
477
|
-
results = database.get_top_artists(**ckeys)
|
494
|
+
results = database.get_top_artists(**ckeys,compatibility=True)
|
478
495
|
|
479
496
|
return {
|
480
497
|
"status":"ok",
|
@@ -493,7 +510,7 @@ def get_top_tracks_external(k_filter, k_limit, k_delimit, k_amount):
|
|
493
510
|
:rtype: Dictionary"""
|
494
511
|
|
495
512
|
ckeys = {**k_limit, **k_delimit}
|
496
|
-
results = database.get_top_tracks(**ckeys)
|
513
|
+
results = database.get_top_tracks(**ckeys,compatibility=True)
|
497
514
|
# IMPLEMENT THIS FOR TOP TRACKS OF ARTIST/ALBUM AS WELL?
|
498
515
|
|
499
516
|
return {
|
@@ -513,7 +530,7 @@ def get_top_albums_external(k_filter, k_limit, k_delimit, k_amount):
|
|
513
530
|
:rtype: Dictionary"""
|
514
531
|
|
515
532
|
ckeys = {**k_limit, **k_delimit}
|
516
|
-
results = database.get_top_albums(**ckeys)
|
533
|
+
results = database.get_top_albums(**ckeys,compatibility=True)
|
517
534
|
# IMPLEMENT THIS FOR TOP ALBUMS OF ARTIST AS WELL?
|
518
535
|
|
519
536
|
return {
|
@@ -567,7 +584,7 @@ def album_info_external(k_filter, k_limit, k_delimit, k_amount):
|
|
567
584
|
|
568
585
|
|
569
586
|
@api.post("newscrobble")
|
570
|
-
@authenticated_function(alternate=api_key_correct,api=True,pass_auth_result_as='auth_result')
|
587
|
+
@auth.authenticated_function(alternate=api_key_correct,api=True,pass_auth_result_as='auth_result')
|
571
588
|
@catch_exceptions
|
572
589
|
def post_scrobble(
|
573
590
|
artist:Multi=None,
|
@@ -647,7 +664,7 @@ def post_scrobble(
|
|
647
664
|
|
648
665
|
|
649
666
|
@api.post("addpicture")
|
650
|
-
@authenticated_function(alternate=api_key_correct,api=True)
|
667
|
+
@auth.authenticated_function(alternate=api_key_correct,api=True)
|
651
668
|
@catch_exceptions
|
652
669
|
@convert_kwargs
|
653
670
|
def add_picture(k_filter, k_limit, k_delimit, k_amount, k_special):
|
@@ -670,7 +687,7 @@ def add_picture(k_filter, k_limit, k_delimit, k_amount, k_special):
|
|
670
687
|
|
671
688
|
|
672
689
|
@api.post("importrules")
|
673
|
-
@authenticated_function(api=True)
|
690
|
+
@auth.authenticated_function(api=True)
|
674
691
|
@catch_exceptions
|
675
692
|
def import_rulemodule(**keys):
|
676
693
|
"""Internal Use Only"""
|
@@ -689,7 +706,7 @@ def import_rulemodule(**keys):
|
|
689
706
|
|
690
707
|
|
691
708
|
@api.post("rebuild")
|
692
|
-
@authenticated_function(api=True)
|
709
|
+
@auth.authenticated_function(api=True)
|
693
710
|
@catch_exceptions
|
694
711
|
def rebuild(**keys):
|
695
712
|
"""Internal Use Only"""
|
@@ -765,7 +782,7 @@ def search(**keys):
|
|
765
782
|
|
766
783
|
|
767
784
|
@api.post("newrule")
|
768
|
-
@authenticated_function(api=True)
|
785
|
+
@auth.authenticated_function(api=True)
|
769
786
|
@catch_exceptions
|
770
787
|
def newrule(**keys):
|
771
788
|
"""Internal Use Only"""
|
@@ -776,21 +793,21 @@ def newrule(**keys):
|
|
776
793
|
|
777
794
|
|
778
795
|
@api.post("settings")
|
779
|
-
@authenticated_function(api=True)
|
796
|
+
@auth.authenticated_function(api=True)
|
780
797
|
@catch_exceptions
|
781
798
|
def set_settings(**keys):
|
782
799
|
"""Internal Use Only"""
|
783
800
|
malojaconfig.update(keys)
|
784
801
|
|
785
802
|
@api.post("apikeys")
|
786
|
-
@authenticated_function(api=True)
|
803
|
+
@auth.authenticated_function(api=True)
|
787
804
|
@catch_exceptions
|
788
805
|
def set_apikeys(**keys):
|
789
806
|
"""Internal Use Only"""
|
790
807
|
apikeystore.update(keys)
|
791
808
|
|
792
809
|
@api.post("import")
|
793
|
-
@authenticated_function(api=True)
|
810
|
+
@auth.authenticated_function(api=True)
|
794
811
|
@catch_exceptions
|
795
812
|
def import_scrobbles(identifier):
|
796
813
|
"""Internal Use Only"""
|
@@ -798,7 +815,7 @@ def import_scrobbles(identifier):
|
|
798
815
|
import_scrobbles(identifier)
|
799
816
|
|
800
817
|
@api.get("backup")
|
801
|
-
@authenticated_function(api=True)
|
818
|
+
@auth.authenticated_function(api=True)
|
802
819
|
@catch_exceptions
|
803
820
|
def get_backup(**keys):
|
804
821
|
"""Internal Use Only"""
|
@@ -811,7 +828,7 @@ def get_backup(**keys):
|
|
811
828
|
return static_file(os.path.basename(archivefile),root=tmpfolder)
|
812
829
|
|
813
830
|
@api.get("export")
|
814
|
-
@authenticated_function(api=True)
|
831
|
+
@auth.authenticated_function(api=True)
|
815
832
|
@catch_exceptions
|
816
833
|
def get_export(**keys):
|
817
834
|
"""Internal Use Only"""
|
@@ -825,7 +842,7 @@ def get_export(**keys):
|
|
825
842
|
|
826
843
|
|
827
844
|
@api.post("delete_scrobble")
|
828
|
-
@authenticated_function(api=True)
|
845
|
+
@auth.authenticated_function(api=True)
|
829
846
|
@catch_exceptions
|
830
847
|
def delete_scrobble(timestamp):
|
831
848
|
"""Internal Use Only"""
|
@@ -837,7 +854,7 @@ def delete_scrobble(timestamp):
|
|
837
854
|
|
838
855
|
|
839
856
|
@api.post("edit_artist")
|
840
|
-
@authenticated_function(api=True)
|
857
|
+
@auth.authenticated_function(api=True)
|
841
858
|
@catch_exceptions
|
842
859
|
def edit_artist(id,name):
|
843
860
|
"""Internal Use Only"""
|
@@ -847,7 +864,7 @@ def edit_artist(id,name):
|
|
847
864
|
}
|
848
865
|
|
849
866
|
@api.post("edit_track")
|
850
|
-
@authenticated_function(api=True)
|
867
|
+
@auth.authenticated_function(api=True)
|
851
868
|
@catch_exceptions
|
852
869
|
def edit_track(id,title):
|
853
870
|
"""Internal Use Only"""
|
@@ -857,7 +874,7 @@ def edit_track(id,title):
|
|
857
874
|
}
|
858
875
|
|
859
876
|
@api.post("edit_album")
|
860
|
-
@authenticated_function(api=True)
|
877
|
+
@auth.authenticated_function(api=True)
|
861
878
|
@catch_exceptions
|
862
879
|
def edit_album(id,albumtitle):
|
863
880
|
"""Internal Use Only"""
|
@@ -868,7 +885,7 @@ def edit_album(id,albumtitle):
|
|
868
885
|
|
869
886
|
|
870
887
|
@api.post("merge_tracks")
|
871
|
-
@authenticated_function(api=True)
|
888
|
+
@auth.authenticated_function(api=True)
|
872
889
|
@catch_exceptions
|
873
890
|
def merge_tracks(target_id,source_ids):
|
874
891
|
"""Internal Use Only"""
|
@@ -879,7 +896,7 @@ def merge_tracks(target_id,source_ids):
|
|
879
896
|
}
|
880
897
|
|
881
898
|
@api.post("merge_artists")
|
882
|
-
@authenticated_function(api=True)
|
899
|
+
@auth.authenticated_function(api=True)
|
883
900
|
@catch_exceptions
|
884
901
|
def merge_artists(target_id,source_ids):
|
885
902
|
"""Internal Use Only"""
|
@@ -890,7 +907,7 @@ def merge_artists(target_id,source_ids):
|
|
890
907
|
}
|
891
908
|
|
892
909
|
@api.post("merge_albums")
|
893
|
-
@authenticated_function(api=True)
|
910
|
+
@auth.authenticated_function(api=True)
|
894
911
|
@catch_exceptions
|
895
912
|
def merge_artists(target_id,source_ids):
|
896
913
|
"""Internal Use Only"""
|
@@ -901,7 +918,7 @@ def merge_artists(target_id,source_ids):
|
|
901
918
|
}
|
902
919
|
|
903
920
|
@api.post("associate_albums_to_artist")
|
904
|
-
@authenticated_function(api=True)
|
921
|
+
@auth.authenticated_function(api=True)
|
905
922
|
@catch_exceptions
|
906
923
|
def associate_albums_to_artist(target_id,source_ids,remove=False):
|
907
924
|
result = database.associate_albums_to_artist(target_id,source_ids,remove=remove)
|
@@ -913,7 +930,7 @@ def associate_albums_to_artist(target_id,source_ids,remove=False):
|
|
913
930
|
}
|
914
931
|
|
915
932
|
@api.post("associate_tracks_to_artist")
|
916
|
-
@authenticated_function(api=True)
|
933
|
+
@auth.authenticated_function(api=True)
|
917
934
|
@catch_exceptions
|
918
935
|
def associate_tracks_to_artist(target_id,source_ids,remove=False):
|
919
936
|
result = database.associate_tracks_to_artist(target_id,source_ids,remove=remove)
|
@@ -925,7 +942,7 @@ def associate_tracks_to_artist(target_id,source_ids,remove=False):
|
|
925
942
|
}
|
926
943
|
|
927
944
|
@api.post("associate_tracks_to_album")
|
928
|
-
@authenticated_function(api=True)
|
945
|
+
@auth.authenticated_function(api=True)
|
929
946
|
@catch_exceptions
|
930
947
|
def associate_tracks_to_album(target_id,source_ids):
|
931
948
|
result = database.associate_tracks_to_album(target_id,source_ids)
|
@@ -937,7 +954,7 @@ def associate_tracks_to_album(target_id,source_ids):
|
|
937
954
|
|
938
955
|
|
939
956
|
@api.post("reparse_scrobble")
|
940
|
-
@authenticated_function(api=True)
|
957
|
+
@auth.authenticated_function(api=True)
|
941
958
|
@catch_exceptions
|
942
959
|
def reparse_scrobble(timestamp):
|
943
960
|
"""Internal Use Only"""
|
maloja/cleanup.py
CHANGED
@@ -15,13 +15,15 @@ class CleanerAgent:
|
|
15
15
|
def updateRules(self):
|
16
16
|
|
17
17
|
rawrules = []
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
try:
|
19
|
+
for f in os.listdir(data_dir["rules"]()):
|
20
|
+
if f.split('.')[-1].lower() != 'tsv': continue
|
21
|
+
filepath = data_dir["rules"](f)
|
22
|
+
with open(filepath,'r') as filed:
|
23
|
+
reader = csv.reader(filed,delimiter="\t")
|
24
|
+
rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')]
|
25
|
+
except FileNotFoundError:
|
26
|
+
pass
|
25
27
|
|
26
28
|
self.rules_belongtogether = [r[1] for r in rawrules if r[0]=="belongtogether"]
|
27
29
|
self.rules_notanartist = [r[1] for r in rawrules if r[0]=="notanartist"]
|
@@ -160,8 +160,8 @@ replaceartist 여자친구 GFriend GFriend
|
|
160
160
|
# Girl's Generation
|
161
161
|
replaceartist 소녀시대 Girls' Generation
|
162
162
|
replaceartist SNSD Girls' Generation
|
163
|
-
replaceartist
|
164
|
-
countas
|
163
|
+
replaceartist Girls' Generation-TTS TaeTiSeo
|
164
|
+
countas TaeTiSeo Girls' Generation
|
165
165
|
|
166
166
|
# Apink
|
167
167
|
replaceartist A Pink Apink
|
@@ -217,6 +217,8 @@ countas Pristin V Pristin
|
|
217
217
|
|
218
218
|
# CLC
|
219
219
|
countas Sorn CLC
|
220
|
+
countas Yeeun CLC
|
221
|
+
countas Seungyeon CLC
|
220
222
|
|
221
223
|
# Popular Remixes
|
222
224
|
artistintitle Areia Remix Areia
|
maloja/database/__init__.py
CHANGED
@@ -27,7 +27,6 @@ from . import exceptions
|
|
27
27
|
|
28
28
|
# doreah toolkit
|
29
29
|
from doreah.logging import log
|
30
|
-
from doreah.auth import authenticated_api, authenticated_api_with_alternate
|
31
30
|
import doreah
|
32
31
|
|
33
32
|
|
@@ -42,6 +41,7 @@ from collections import namedtuple
|
|
42
41
|
from threading import Lock
|
43
42
|
import yaml, json
|
44
43
|
import math
|
44
|
+
from itertools import takewhile
|
45
45
|
|
46
46
|
# url handling
|
47
47
|
import urllib
|
@@ -318,7 +318,7 @@ def associate_tracks_to_album(target_id,source_ids):
|
|
318
318
|
if target_id:
|
319
319
|
target = sqldb.get_album(target_id)
|
320
320
|
log(f"Adding {sources} into {target}")
|
321
|
-
sqldb.add_tracks_to_albums({src:target_id for src in source_ids})
|
321
|
+
sqldb.add_tracks_to_albums({src:target_id for src in source_ids},replace=True)
|
322
322
|
else:
|
323
323
|
sqldb.remove_album(source_ids)
|
324
324
|
result = {'sources':sources,'target':target}
|
@@ -444,10 +444,11 @@ def get_charts_albums(dbconn=None,resolve_ids=True,only_own_albums=False,**keys)
|
|
444
444
|
(since,to) = keys.get('timerange').timestamps()
|
445
445
|
|
446
446
|
if 'artist' in keys:
|
447
|
-
|
447
|
+
artist = sqldb.get_artist(sqldb.get_artist_id(keys['artist']))
|
448
|
+
result = sqldb.count_scrobbles_by_album_combined(since=since,to=to,artist=artist,associated=keys.get('associated',False),resolve_ids=resolve_ids,dbconn=dbconn)
|
448
449
|
if only_own_albums:
|
449
450
|
# TODO: this doesnt take associated into account and doesnt change ranks
|
450
|
-
result = [e for e in result if
|
451
|
+
result = [e for e in result if artist in (e['album']['artists'] or [])]
|
451
452
|
else:
|
452
453
|
result = sqldb.count_scrobbles_by_album(since=since,to=to,resolve_ids=resolve_ids,dbconn=dbconn)
|
453
454
|
return result
|
@@ -570,7 +571,7 @@ def get_performance(dbconn=None,**keys):
|
|
570
571
|
return results
|
571
572
|
|
572
573
|
@waitfordb
|
573
|
-
def get_top_artists(dbconn=None,**keys):
|
574
|
+
def get_top_artists(dbconn=None,compatibility=True,**keys):
|
574
575
|
|
575
576
|
separate = keys.get('separate')
|
576
577
|
|
@@ -578,42 +579,73 @@ def get_top_artists(dbconn=None,**keys):
|
|
578
579
|
results = []
|
579
580
|
|
580
581
|
for rng in rngs:
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
582
|
+
result = {'range':rng}
|
583
|
+
res = get_charts_artists(timerange=rng,separate=separate,dbconn=dbconn)
|
584
|
+
|
585
|
+
result['top'] = [
|
586
|
+
{'artist': r['artist'], 'scrobbles': r['scrobbles'], 'real_scrobbles':r['real_scrobbles'], 'associated_artists': sqldb.get_associated_artists(r['artist'])}
|
587
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
588
|
+
]
|
589
|
+
# for third party applications
|
590
|
+
if compatibility:
|
591
|
+
if result['top']:
|
592
|
+
result.update(result['top'][0])
|
593
|
+
else:
|
594
|
+
result.update({'artist':None,'scrobbles':0,'real_scrobbles':0})
|
595
|
+
|
596
|
+
results.append(result)
|
586
597
|
|
587
598
|
return results
|
588
599
|
|
589
600
|
|
590
601
|
@waitfordb
|
591
|
-
def get_top_tracks(dbconn=None,**keys):
|
602
|
+
def get_top_tracks(dbconn=None,compatibility=True,**keys):
|
592
603
|
|
593
604
|
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
594
605
|
results = []
|
595
606
|
|
596
607
|
for rng in rngs:
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
608
|
+
result = {'range':rng}
|
609
|
+
res = get_charts_tracks(timerange=rng,dbconn=dbconn)
|
610
|
+
|
611
|
+
result['top'] = [
|
612
|
+
{'track': r['track'], 'scrobbles': r['scrobbles']}
|
613
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
614
|
+
]
|
615
|
+
# for third party applications
|
616
|
+
if compatibility:
|
617
|
+
if result['top']:
|
618
|
+
result.update(result['top'][0])
|
619
|
+
else:
|
620
|
+
result.update({'track':None,'scrobbles':0})
|
621
|
+
|
622
|
+
results.append(result)
|
602
623
|
|
603
624
|
return results
|
604
625
|
|
605
626
|
@waitfordb
|
606
|
-
def get_top_albums(dbconn=None,**keys):
|
627
|
+
def get_top_albums(dbconn=None,compatibility=True,**keys):
|
607
628
|
|
608
629
|
rngs = ranges(**{k:keys[k] for k in keys if k in ["since","to","within","timerange","step","stepn","trail"]})
|
609
630
|
results = []
|
610
631
|
|
611
632
|
for rng in rngs:
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
633
|
+
|
634
|
+
result = {'range':rng}
|
635
|
+
res = get_charts_albums(timerange=rng,dbconn=dbconn)
|
636
|
+
|
637
|
+
result['top'] = [
|
638
|
+
{'album': r['album'], 'scrobbles': r['scrobbles']}
|
639
|
+
for r in takewhile(lambda x:x['rank']==1,res)
|
640
|
+
]
|
641
|
+
# for third party applications
|
642
|
+
if compatibility:
|
643
|
+
if result['top']:
|
644
|
+
result.update(result['top'][0])
|
645
|
+
else:
|
646
|
+
result.update({'album':None,'scrobbles':0})
|
647
|
+
|
648
|
+
results.append(result)
|
617
649
|
|
618
650
|
return results
|
619
651
|
|
@@ -913,7 +945,7 @@ def start_db():
|
|
913
945
|
|
914
946
|
# inform time module about begin of scrobbling
|
915
947
|
try:
|
916
|
-
firstscrobble = sqldb.get_scrobbles()[0]
|
948
|
+
firstscrobble = sqldb.get_scrobbles(limit=1)[0]
|
917
949
|
register_scrobbletime(firstscrobble['time'])
|
918
950
|
except IndexError:
|
919
951
|
register_scrobbletime(int(datetime.datetime.now().timestamp()))
|