malojaserver 3.2.2__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.
Files changed (38) hide show
  1. maloja/__main__.py +1 -1
  2. maloja/__pkginfo__.py +1 -1
  3. maloja/apis/_base.py +26 -19
  4. maloja/apis/_exceptions.py +1 -1
  5. maloja/apis/audioscrobbler.py +35 -7
  6. maloja/apis/audioscrobbler_legacy.py +5 -5
  7. maloja/apis/listenbrainz.py +7 -5
  8. maloja/apis/native_v1.py +43 -26
  9. maloja/cleanup.py +9 -7
  10. maloja/data_files/config/rules/predefined/krateng_kpopgirlgroups.tsv +2 -2
  11. maloja/database/__init__.py +55 -23
  12. maloja/database/associated.py +10 -6
  13. maloja/database/exceptions.py +28 -3
  14. maloja/database/sqldb.py +216 -168
  15. maloja/dev/profiler.py +3 -4
  16. maloja/images.py +6 -0
  17. maloja/malojauri.py +2 -0
  18. maloja/pkg_global/conf.py +29 -28
  19. maloja/proccontrol/tasks/export.py +2 -1
  20. maloja/proccontrol/tasks/import_scrobbles.py +57 -15
  21. maloja/server.py +4 -5
  22. maloja/setup.py +13 -7
  23. maloja/web/jinja/abstracts/base.jinja +1 -1
  24. maloja/web/jinja/admin_albumless.jinja +2 -0
  25. maloja/web/jinja/admin_overview.jinja +3 -3
  26. maloja/web/jinja/admin_setup.jinja +1 -1
  27. maloja/web/jinja/partials/album_showcase.jinja +1 -1
  28. maloja/web/jinja/snippets/entityrow.jinja +2 -2
  29. maloja/web/jinja/snippets/links.jinja +3 -1
  30. maloja/web/static/css/maloja.css +8 -2
  31. maloja/web/static/css/startpage.css +2 -2
  32. maloja/web/static/js/manualscrobble.js +1 -1
  33. maloja/web/static/js/notifications.js +16 -8
  34. {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/METADATA +10 -46
  35. {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/RECORD +38 -38
  36. {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/WHEEL +1 -1
  37. {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/LICENSE +0 -0
  38. {malojaserver-3.2.2.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']("Data Directory: "),conf.dir_settings['state'])
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
@@ -4,7 +4,7 @@
4
4
  # you know what f*ck it
5
5
  # this is hardcoded for now because of that damn project / package name discrepancy
6
6
  # i'll fix it one day
7
- VERSION = "3.2.2"
7
+ VERSION = "3.2.3"
8
8
  HOMEPAGE = "https://github.com/krateng/maloja"
9
9
 
10
10
 
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
- exceptiontype = sys.exc_info()[0]
67
- if exceptiontype in self.errors:
68
- response.status,result = self.errors[exceptiontype]
69
- log(f"Error with {self.__apiname__} API: {exceptiontype} (Request: {path})")
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
- response.status,result = 500,{"status":"Unknown error","code":500}
72
- log(f"Unhandled Exception with {self.__apiname__} API: {exceptiontype} (Request: {path})")
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 Exception:
86
- log("Could not find a handler for method " + str(methodname) + " in API " + self.__apiname__,module="debug")
87
- log("Keys: " + str(keys),module="debug")
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
- try:
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)
@@ -3,4 +3,4 @@ class InvalidAuthException(Exception): pass
3
3
  class InvalidMethodException(Exception): pass
4
4
  class InvalidSessionKey(Exception): pass
5
5
  class MalformedJSONException(Exception): pass
6
- class ScrobblingException(Exception): pass
6
+
@@ -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
- ScrobblingException:(500,{"error":8,"message":"Operation failed"})
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("<", "&lt;")
35
+ str_xml = str_xml.replace("<", "&lt;")
36
+ str_xml = str_xml.replace("\"", "&quot;")
37
+ str_xml = str_xml.replace("'", "&apos;")
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
- return 200,{"session":{"key":sessionkey}}
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
- return 200,{"session":{"key":sessionkey}}
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
- ScrobblingException:(500,"FAILED\n")
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):
@@ -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
- ScrobblingException:(500,{"code":500,"error":"Unspecified server error."})
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
- for f in os.listdir(data_dir["rules"]()):
19
- if f.split('.')[-1].lower() != 'tsv': continue
20
- filepath = data_dir["rules"](f)
21
- with open(filepath,'r') as filed:
22
- reader = csv.reader(filed,delimiter="\t")
23
- rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')]
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 Girls' Generation-TTS TaeTiSeo
164
- countas TaeTiSeo Girls' Generation
163
+ replaceartist Girls' Generation-TTS TaeTiSeo
164
+ countas TaeTiSeo Girls' Generation
165
165
 
166
166
  # Apink
167
167
  replaceartist A Pink Apink
@@ -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
- result = sqldb.count_scrobbles_by_album_combined(since=since,to=to,artist=keys['artist'],associated=keys.get('associated',False),resolve_ids=resolve_ids,dbconn=dbconn)
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 keys['artist'] in (e['album']['artists'] or [])]
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
- try:
582
- res = get_charts_artists(timerange=rng,separate=separate,dbconn=dbconn)[0]
583
- results.append({"range":rng,"artist":res["artist"],"scrobbles":res["scrobbles"],"real_scrobbles":res["real_scrobbles"],"associated_artists":sqldb.get_associated_artists(res["artist"])})
584
- except Exception:
585
- results.append({"range":rng,"artist":None,"scrobbles":0,"real_scrobbles":0})
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
- try:
598
- res = get_charts_tracks(timerange=rng,dbconn=dbconn)[0]
599
- results.append({"range":rng,"track":res["track"],"scrobbles":res["scrobbles"]})
600
- except Exception:
601
- results.append({"range":rng,"track":None,"scrobbles":0})
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
- try:
613
- res = get_charts_albums(timerange=rng,dbconn=dbconn)[0]
614
- results.append({"range":rng,"album":res["album"],"scrobbles":res["scrobbles"]})
615
- except Exception:
616
- results.append({"range":rng,"album":None,"scrobbles":0})
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()))
@@ -19,12 +19,16 @@ def load_associated_rules():
19
19
 
20
20
  # load from file
21
21
  rawrules = []
22
- for f in os.listdir(data_dir["rules"]()):
23
- if f.split('.')[-1].lower() != 'tsv': continue
24
- filepath = data_dir["rules"](f)
25
- with open(filepath,'r') as filed:
26
- reader = csv.reader(filed,delimiter="\t")
27
- rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')]
22
+ try:
23
+ for f in os.listdir(data_dir["rules"]()):
24
+ if f.split('.')[-1].lower() != 'tsv': continue
25
+ filepath = data_dir["rules"](f)
26
+ with open(filepath,'r') as filed:
27
+ reader = csv.reader(filed,delimiter="\t")
28
+ rawrules += [[col for col in entry if col] for entry in reader if len(entry)>0 and not entry[0].startswith('#')]
29
+ except FileNotFoundError:
30
+ return
31
+
28
32
  rules = [{'source_artist':r[1],'target_artist':r[2]} for r in rawrules if r[0]=="countas"]
29
33
 
30
34
  #for rule in rules: