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/database/sqldb.py CHANGED
@@ -1,3 +1,5 @@
1
+ from typing import TypedDict, Optional, cast
2
+
1
3
  import sqlalchemy as sql
2
4
  from sqlalchemy.dialects.sqlite import insert as sqliteinsert
3
5
  import json
@@ -213,6 +215,25 @@ def set_maloja_info(info,dbconn=None):
213
215
  # The last two fields are not returned under normal circumstances
214
216
 
215
217
 
218
+ class AlbumDict(TypedDict):
219
+ albumtitle: str
220
+ artists: list[str]
221
+
222
+
223
+ class TrackDict(TypedDict):
224
+ artists: list[str]
225
+ title: str
226
+ album: AlbumDict
227
+ length: int | None
228
+
229
+
230
+ class ScrobbleDict(TypedDict):
231
+ time: int
232
+ track: TrackDict
233
+ duration: int
234
+ origin: str
235
+ extra: Optional[dict]
236
+ rawscrobble: Optional[dict]
216
237
 
217
238
 
218
239
  ##### Conversions between DB and dicts
@@ -222,140 +243,164 @@ def set_maloja_info(info,dbconn=None):
222
243
 
223
244
 
224
245
  ### DB -> DICT
225
- def scrobbles_db_to_dict(rows,include_internal=False,dbconn=None):
226
- tracks = get_tracks_map(set(row.track_id for row in rows),dbconn=dbconn)
246
+ def scrobbles_db_to_dict(rows, include_internal=False, dbconn=None) -> list[ScrobbleDict]:
247
+ tracks: list[TrackDict] = get_tracks_map(set(row.track_id for row in rows), dbconn=dbconn)
227
248
  return [
228
- {
249
+ cast(ScrobbleDict, {
229
250
  **{
230
- "time":row.timestamp,
231
- "track":tracks[row.track_id],
232
- "duration":row.duration,
233
- "origin":row.origin,
251
+ "time": row.timestamp,
252
+ "track": tracks[row.track_id],
253
+ "duration": row.duration,
254
+ "origin": row.origin
234
255
  },
235
256
  **({
236
- "extra":json.loads(row.extra or '{}'),
237
- "rawscrobble":json.loads(row.rawscrobble or '{}')
257
+ "extra": json.loads(row.extra or '{}'),
258
+ "rawscrobble": json.loads(row.rawscrobble or '{}')
238
259
  } if include_internal else {})
239
- }
260
+ })
240
261
 
241
262
  for row in rows
242
263
  ]
243
264
 
244
- def scrobble_db_to_dict(row,dbconn=None):
245
- return scrobbles_db_to_dict([row],dbconn=dbconn)[0]
246
265
 
247
- def tracks_db_to_dict(rows,dbconn=None):
248
- artists = get_artists_of_tracks(set(row.id for row in rows),dbconn=dbconn)
249
- albums = get_albums_map(set(row.album_id for row in rows),dbconn=dbconn)
266
+ def scrobble_db_to_dict(row, dbconn=None) -> ScrobbleDict:
267
+ return scrobbles_db_to_dict([row], dbconn=dbconn)[0]
268
+
269
+
270
+ def tracks_db_to_dict(rows, dbconn=None) -> list[TrackDict]:
271
+ artists = get_artists_of_tracks(set(row.id for row in rows), dbconn=dbconn)
272
+ albums = get_albums_map(set(row.album_id for row in rows), dbconn=dbconn)
250
273
  return [
251
- {
274
+ cast(TrackDict, {
252
275
  "artists":artists[row.id],
253
276
  "title":row.title,
254
277
  "album":albums.get(row.album_id),
255
278
  "length":row.length
256
- }
279
+ })
257
280
  for row in rows
258
281
  ]
259
282
 
260
- def track_db_to_dict(row,dbconn=None):
261
- return tracks_db_to_dict([row],dbconn=dbconn)[0]
262
283
 
263
- def artists_db_to_dict(rows,dbconn=None):
284
+ def track_db_to_dict(row, dbconn=None) -> TrackDict:
285
+ return tracks_db_to_dict([row], dbconn=dbconn)[0]
286
+
287
+
288
+ def artists_db_to_dict(rows, dbconn=None) -> list[str]:
264
289
  return [
265
290
  row.name
266
291
  for row in rows
267
292
  ]
268
293
 
269
- def artist_db_to_dict(row,dbconn=None):
270
- return artists_db_to_dict([row],dbconn=dbconn)[0]
271
294
 
272
- def albums_db_to_dict(rows,dbconn=None):
273
- artists = get_artists_of_albums(set(row.id for row in rows),dbconn=dbconn)
295
+ def artist_db_to_dict(row, dbconn=None) -> str:
296
+ return artists_db_to_dict([row], dbconn=dbconn)[0]
297
+
298
+
299
+ def albums_db_to_dict(rows, dbconn=None) -> list[AlbumDict]:
300
+ artists = get_artists_of_albums(set(row.id for row in rows), dbconn=dbconn)
274
301
  return [
275
- {
276
- "artists":artists.get(row.id),
277
- "albumtitle":row.albtitle,
278
- }
302
+ cast(AlbumDict, {
303
+ "artists": artists.get(row.id),
304
+ "albumtitle": row.albtitle,
305
+ })
279
306
  for row in rows
280
307
  ]
281
308
 
282
- def album_db_to_dict(row,dbconn=None):
283
- return albums_db_to_dict([row],dbconn=dbconn)[0]
284
-
285
309
 
310
+ def album_db_to_dict(row, dbconn=None) -> AlbumDict:
311
+ return albums_db_to_dict([row], dbconn=dbconn)[0]
286
312
 
287
313
 
288
314
  ### DICT -> DB
289
315
  # These should return None when no data is in the dict so they can be used for update statements
290
316
 
291
- def scrobble_dict_to_db(info,update_album=False,dbconn=None):
317
+ def scrobble_dict_to_db(info: ScrobbleDict, update_album=False, dbconn=None):
292
318
  return {
293
- "timestamp":info.get('time'),
294
- "origin":info.get('origin'),
295
- "duration":info.get('duration'),
296
- "track_id":get_track_id(info.get('track'),update_album=update_album,dbconn=dbconn),
297
- "extra":json.dumps(info.get('extra')) if info.get('extra') else None,
298
- "rawscrobble":json.dumps(info.get('rawscrobble')) if info.get('rawscrobble') else None
319
+ "timestamp": info.get('time'),
320
+ "origin": info.get('origin'),
321
+ "duration": info.get('duration'),
322
+ "track_id": get_track_id(info.get('track'), update_album=update_album, dbconn=dbconn),
323
+ "extra": json.dumps(info.get('extra')) if info.get('extra') else None,
324
+ "rawscrobble": json.dumps(info.get('rawscrobble')) if info.get('rawscrobble') else None
299
325
  }
300
326
 
301
- def track_dict_to_db(info,dbconn=None):
327
+
328
+ def track_dict_to_db(info: TrackDict, dbconn=None):
302
329
  return {
303
- "title":info.get('title'),
304
- "title_normalized":normalize_name(info.get('title','')) or None,
305
- "length":info.get('length')
330
+ "title": info.get('title'),
331
+ "title_normalized": normalize_name(info.get('title', '')) or None,
332
+ "length": info.get('length')
306
333
  }
307
334
 
308
- def artist_dict_to_db(info,dbconn=None):
335
+
336
+ def artist_dict_to_db(info: str, dbconn=None):
309
337
  return {
310
338
  "name": info,
311
- "name_normalized":normalize_name(info)
339
+ "name_normalized": normalize_name(info)
312
340
  }
313
341
 
314
- def album_dict_to_db(info,dbconn=None):
342
+
343
+ def album_dict_to_db(info: AlbumDict, dbconn=None):
315
344
  return {
316
- "albtitle":info.get('albumtitle'),
317
- "albtitle_normalized":normalize_name(info.get('albumtitle'))
345
+ "albtitle": info.get('albumtitle'),
346
+ "albtitle_normalized": normalize_name(info.get('albumtitle'))
318
347
  }
319
348
 
320
349
 
321
350
 
322
351
 
323
-
324
352
  ##### Actual Database interactions
325
353
 
326
354
  # TODO: remove all resolve_id args and do that logic outside the caching to improve hit chances
327
355
  # TODO: maybe also factor out all intitial get entity funcs (some here, some in __init__) and throw exceptions
328
356
 
329
357
  @connection_provider
330
- def add_scrobble(scrobbledict,update_album=False,dbconn=None):
331
- add_scrobbles([scrobbledict],update_album=update_album,dbconn=dbconn)
358
+ def add_scrobble(scrobbledict: ScrobbleDict, update_album=False, dbconn=None):
359
+ _, ex, er = add_scrobbles([scrobbledict], update_album=update_album, dbconn=dbconn)
360
+ if er > 0:
361
+ raise exc.DuplicateTimestamp(existing_scrobble=None, rejected_scrobble=scrobbledict)
362
+ # TODO: actually pass existing scrobble
363
+ elif ex > 0:
364
+ raise exc.DuplicateScrobble(scrobble=scrobbledict)
365
+
332
366
 
333
367
  @connection_provider
334
- def add_scrobbles(scrobbleslist,update_album=False,dbconn=None):
368
+ def add_scrobbles(scrobbleslist: list[ScrobbleDict], update_album=False, dbconn=None) -> tuple[int, int, int]:
335
369
 
336
370
  with SCROBBLE_LOCK:
337
371
 
338
- ops = [
339
- DB['scrobbles'].insert().values(
340
- **scrobble_dict_to_db(s,update_album=update_album,dbconn=dbconn)
341
- ) for s in scrobbleslist
342
- ]
372
+ # ops = [
373
+ # DB['scrobbles'].insert().values(
374
+ # **scrobble_dict_to_db(s,update_album=update_album,dbconn=dbconn)
375
+ # ) for s in scrobbleslist
376
+ # ]
343
377
 
344
- success,errors = 0,0
345
- for op in ops:
378
+ success, exists, errors = 0, 0, 0
379
+
380
+ for s in scrobbleslist:
381
+ scrobble_entry = scrobble_dict_to_db(s, update_album=update_album, dbconn=dbconn)
346
382
  try:
347
- dbconn.execute(op)
383
+ dbconn.execute(DB['scrobbles'].insert().values(
384
+ **scrobble_entry
385
+ ))
348
386
  success += 1
349
- except sql.exc.IntegrityError as e:
350
- errors += 1
387
+ except sql.exc.IntegrityError:
388
+ # get existing scrobble
389
+ result = dbconn.execute(DB['scrobbles'].select().where(
390
+ DB['scrobbles'].c.timestamp == scrobble_entry['timestamp']
391
+ )).first()
392
+ if result.track_id == scrobble_entry['track_id']:
393
+ exists += 1
394
+ else:
395
+ errors += 1
351
396
 
352
- # TODO check if actual duplicate
397
+ if errors > 0: log(f"{errors} Scrobbles have not been written to database (duplicate timestamps)!", color='red')
398
+ if exists > 0: log(f"{exists} Scrobbles have not been written to database (already exist)", color='orange')
399
+ return success, exists, errors
353
400
 
354
- if errors > 0: log(f"{errors} Scrobbles have not been written to database!",color='red')
355
- return success,errors
356
401
 
357
402
  @connection_provider
358
- def delete_scrobble(scrobble_id,dbconn=None):
403
+ def delete_scrobble(scrobble_id: int, dbconn=None) -> bool:
359
404
 
360
405
  with SCROBBLE_LOCK:
361
406
 
@@ -369,7 +414,7 @@ def delete_scrobble(scrobble_id,dbconn=None):
369
414
 
370
415
 
371
416
  @connection_provider
372
- def add_track_to_album(track_id,album_id,replace=False,dbconn=None):
417
+ def add_track_to_album(track_id: int, album_id: int, replace=False, dbconn=None) -> bool:
373
418
 
374
419
  conditions = [
375
420
  DB['tracks'].c.id == track_id
@@ -398,39 +443,39 @@ def add_track_to_album(track_id,album_id,replace=False,dbconn=None):
398
443
  # ALL OF RECORDED HISTORY in order to display top weeks
399
444
  # lmao
400
445
  # TODO: figure out something better
401
-
402
-
403
446
  return True
404
447
 
448
+
405
449
  @connection_provider
406
- def add_tracks_to_albums(track_to_album_id_dict,replace=False,dbconn=None):
450
+ def add_tracks_to_albums(track_to_album_id_dict: dict[int, int], replace=False, dbconn=None) -> bool:
407
451
 
408
452
  for track_id in track_to_album_id_dict:
409
- add_track_to_album(track_id,track_to_album_id_dict[track_id],dbconn=dbconn)
453
+ add_track_to_album(track_id,track_to_album_id_dict[track_id], replace=replace, dbconn=dbconn)
454
+ return True
455
+
410
456
 
411
457
  @connection_provider
412
- def remove_album(*track_ids,dbconn=None):
458
+ def remove_album(*track_ids: list[int], dbconn=None) -> bool:
413
459
 
414
460
  DB['tracks'].update().where(
415
461
  DB['tracks'].c.track_id.in_(track_ids)
416
462
  ).values(
417
463
  album_id=None
418
464
  )
465
+ return True
466
+
419
467
 
420
468
  ### these will 'get' the ID of an entity, creating it if necessary
421
469
 
422
470
  @cached_wrapper
423
471
  @connection_provider
424
- def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None):
472
+ def get_track_id(trackdict: TrackDict, create_new=True, update_album=False, dbconn=None) -> int | None:
425
473
  ntitle = normalize_name(trackdict['title'])
426
- artist_ids = [get_artist_id(a,create_new=create_new,dbconn=dbconn) for a in trackdict['artists']]
474
+ artist_ids = [get_artist_id(a, create_new=create_new, dbconn=dbconn) for a in trackdict['artists']]
427
475
  artist_ids = list(set(artist_ids))
428
476
 
429
-
430
-
431
-
432
477
  op = DB['tracks'].select().where(
433
- DB['tracks'].c.title_normalized==ntitle
478
+ DB['tracks'].c.title_normalized == ntitle
434
479
  )
435
480
  result = dbconn.execute(op).all()
436
481
  for row in result:
@@ -440,7 +485,7 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None):
440
485
  op = DB['trackartists'].select(
441
486
  # DB['trackartists'].c.artist_id
442
487
  ).where(
443
- DB['trackartists'].c.track_id==row.id
488
+ DB['trackartists'].c.track_id == row.id
444
489
  )
445
490
  result = dbconn.execute(op).all()
446
491
  match_artist_ids = [r.artist_id for r in result]
@@ -456,14 +501,14 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None):
456
501
  album_id = get_album_id(trackdict['album'],create_new=(update_album or not row.album_id),dbconn=dbconn)
457
502
  add_track_to_album(row.id,album_id,replace=update_album,dbconn=dbconn)
458
503
 
459
-
460
504
  return row.id
461
505
 
462
- if not create_new: return None
506
+ if not create_new:
507
+ return None
463
508
 
464
509
  #print("Creating new track")
465
510
  op = DB['tracks'].insert().values(
466
- **track_dict_to_db(trackdict,dbconn=dbconn)
511
+ **track_dict_to_db(trackdict, dbconn=dbconn)
467
512
  )
468
513
  result = dbconn.execute(op)
469
514
  track_id = result.inserted_primary_key[0]
@@ -478,24 +523,26 @@ def get_track_id(trackdict,create_new=True,update_album=False,dbconn=None):
478
523
  #print("Created",trackdict['title'],track_id)
479
524
 
480
525
  if trackdict.get('album'):
481
- add_track_to_album(track_id,get_album_id(trackdict['album'],dbconn=dbconn),dbconn=dbconn)
526
+ add_track_to_album(track_id, get_album_id(trackdict['album'], dbconn=dbconn), dbconn=dbconn)
482
527
  return track_id
483
528
 
529
+
484
530
  @cached_wrapper
485
531
  @connection_provider
486
- def get_artist_id(artistname,create_new=True,dbconn=None):
532
+ def get_artist_id(artistname: str, create_new=True, dbconn=None) -> int | None:
487
533
  nname = normalize_name(artistname)
488
534
  #print("looking for",nname)
489
535
 
490
536
  op = DB['artists'].select().where(
491
- DB['artists'].c.name_normalized==nname
537
+ DB['artists'].c.name_normalized == nname
492
538
  )
493
539
  result = dbconn.execute(op).all()
494
540
  for row in result:
495
541
  #print("ID for",artistname,"was",row[0])
496
542
  return row.id
497
543
 
498
- if not create_new: return None
544
+ if not create_new:
545
+ return None
499
546
 
500
547
  op = DB['artists'].insert().values(
501
548
  name=artistname,
@@ -508,15 +555,15 @@ def get_artist_id(artistname,create_new=True,dbconn=None):
508
555
 
509
556
  @cached_wrapper
510
557
  @connection_provider
511
- def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None):
558
+ def get_album_id(albumdict: AlbumDict, create_new=True, ignore_albumartists=False, dbconn=None) -> int | None:
512
559
  ntitle = normalize_name(albumdict['albumtitle'])
513
- artist_ids = [get_artist_id(a,dbconn=dbconn) for a in (albumdict.get('artists') or [])]
560
+ artist_ids = [get_artist_id(a, dbconn=dbconn) for a in (albumdict.get('artists') or [])]
514
561
  artist_ids = list(set(artist_ids))
515
562
 
516
563
  op = DB['albums'].select(
517
564
  # DB['albums'].c.id
518
565
  ).where(
519
- DB['albums'].c.albtitle_normalized==ntitle
566
+ DB['albums'].c.albtitle_normalized == ntitle
520
567
  )
521
568
  result = dbconn.execute(op).all()
522
569
  for row in result:
@@ -529,7 +576,7 @@ def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None
529
576
  op = DB['albumartists'].select(
530
577
  # DB['albumartists'].c.artist_id
531
578
  ).where(
532
- DB['albumartists'].c.album_id==row.id
579
+ DB['albumartists'].c.album_id == row.id
533
580
  )
534
581
  result = dbconn.execute(op).all()
535
582
  match_artist_ids = [r.artist_id for r in result]
@@ -538,11 +585,11 @@ def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None
538
585
  #print("ID for",albumdict['title'],"was",row[0])
539
586
  return row.id
540
587
 
541
- if not create_new: return None
542
-
588
+ if not create_new:
589
+ return None
543
590
 
544
591
  op = DB['albums'].insert().values(
545
- **album_dict_to_db(albumdict,dbconn=dbconn)
592
+ **album_dict_to_db(albumdict, dbconn=dbconn)
546
593
  )
547
594
  result = dbconn.execute(op)
548
595
  album_id = result.inserted_primary_key[0]
@@ -557,18 +604,15 @@ def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None
557
604
  return album_id
558
605
 
559
606
 
560
-
561
-
562
607
  ### Edit existing
563
608
 
564
-
565
609
  @connection_provider
566
- def edit_scrobble(scrobble_id,scrobbleupdatedict,dbconn=None):
610
+ def edit_scrobble(scrobble_id: int, scrobbleupdatedict: dict, dbconn=None) -> bool:
567
611
 
568
612
  dbentry = scrobble_dict_to_db(scrobbleupdatedict,dbconn=dbconn)
569
- dbentry = {k:v for k,v in dbentry.items() if v}
613
+ dbentry = {k: v for k, v in dbentry.items() if v}
570
614
 
571
- print("Updating scrobble",dbentry)
615
+ print("Updating scrobble", dbentry)
572
616
 
573
617
  with SCROBBLE_LOCK:
574
618
 
@@ -579,97 +623,97 @@ def edit_scrobble(scrobble_id,scrobbleupdatedict,dbconn=None):
579
623
  )
580
624
 
581
625
  dbconn.execute(op)
626
+ return True
627
+
582
628
 
583
629
  # edit function only for primary db information (not linked fields)
584
630
  @connection_provider
585
- def edit_artist(id,artistupdatedict,dbconn=None):
631
+ def edit_artist(artist_id: int, artistupdatedict: str, dbconn=None) -> bool:
586
632
 
587
- artist = get_artist(id)
633
+ artist = get_artist(artist_id)
588
634
  changedartist = artistupdatedict # well
589
635
 
590
- dbentry = artist_dict_to_db(artistupdatedict,dbconn=dbconn)
591
- dbentry = {k:v for k,v in dbentry.items() if v}
636
+ dbentry = artist_dict_to_db(artistupdatedict, dbconn=dbconn)
637
+ dbentry = {k: v for k, v in dbentry.items() if v}
592
638
 
593
- existing_artist_id = get_artist_id(changedartist,create_new=False,dbconn=dbconn)
594
- if existing_artist_id not in (None,id):
639
+ existing_artist_id = get_artist_id(changedartist, create_new=False, dbconn=dbconn)
640
+ if existing_artist_id not in (None, artist_id):
595
641
  raise exc.ArtistExists(changedartist)
596
642
 
597
643
  op = DB['artists'].update().where(
598
- DB['artists'].c.id==id
644
+ DB['artists'].c.id == artist_id
599
645
  ).values(
600
646
  **dbentry
601
647
  )
602
648
  result = dbconn.execute(op)
603
-
604
649
  return True
605
650
 
651
+
606
652
  # edit function only for primary db information (not linked fields)
607
653
  @connection_provider
608
- def edit_track(id,trackupdatedict,dbconn=None):
654
+ def edit_track(track_id: int, trackupdatedict: dict, dbconn=None) -> bool:
609
655
 
610
- track = get_track(id,dbconn=dbconn)
611
- changedtrack = {**track,**trackupdatedict}
656
+ track = get_track(track_id, dbconn=dbconn)
657
+ changedtrack: TrackDict = {**track, **trackupdatedict}
612
658
 
613
- dbentry = track_dict_to_db(trackupdatedict,dbconn=dbconn)
614
- dbentry = {k:v for k,v in dbentry.items() if v}
659
+ dbentry = track_dict_to_db(trackupdatedict, dbconn=dbconn)
660
+ dbentry = {k: v for k, v in dbentry.items() if v}
615
661
 
616
- existing_track_id = get_track_id(changedtrack,create_new=False,dbconn=dbconn)
617
- if existing_track_id not in (None,id):
662
+ existing_track_id = get_track_id(changedtrack, create_new=False, dbconn=dbconn)
663
+ if existing_track_id not in (None, track_id):
618
664
  raise exc.TrackExists(changedtrack)
619
665
 
620
666
  op = DB['tracks'].update().where(
621
- DB['tracks'].c.id==id
667
+ DB['tracks'].c.id == track_id
622
668
  ).values(
623
669
  **dbentry
624
670
  )
625
671
  result = dbconn.execute(op)
626
-
627
672
  return True
628
673
 
674
+
629
675
  # edit function only for primary db information (not linked fields)
630
676
  @connection_provider
631
- def edit_album(id,albumupdatedict,dbconn=None):
677
+ def edit_album(album_id: int, albumupdatedict: dict, dbconn=None) -> bool:
632
678
 
633
- album = get_album(id,dbconn=dbconn)
634
- changedalbum = {**album,**albumupdatedict}
679
+ album = get_album(album_id, dbconn=dbconn)
680
+ changedalbum: AlbumDict = {**album, **albumupdatedict}
635
681
 
636
- dbentry = album_dict_to_db(albumupdatedict,dbconn=dbconn)
637
- dbentry = {k:v for k,v in dbentry.items() if v}
682
+ dbentry = album_dict_to_db(albumupdatedict, dbconn=dbconn)
683
+ dbentry = {k: v for k, v in dbentry.items() if v}
638
684
 
639
- existing_album_id = get_album_id(changedalbum,create_new=False,dbconn=dbconn)
640
- if existing_album_id not in (None,id):
685
+ existing_album_id = get_album_id(changedalbum, create_new=False, dbconn=dbconn)
686
+ if existing_album_id not in (None, album_id):
641
687
  raise exc.TrackExists(changedalbum)
642
688
 
643
689
  op = DB['albums'].update().where(
644
- DB['albums'].c.id==id
690
+ DB['albums'].c.id == album_id
645
691
  ).values(
646
692
  **dbentry
647
693
  )
648
694
  result = dbconn.execute(op)
649
-
650
695
  return True
651
696
 
652
697
 
653
698
  ### Edit associations
654
699
 
655
700
  @connection_provider
656
- def add_artists_to_tracks(track_ids,artist_ids,dbconn=None):
701
+ def add_artists_to_tracks(track_ids: list[int], artist_ids: list[int], dbconn=None) -> bool:
657
702
 
658
703
  op = DB['trackartists'].insert().values([
659
- {'track_id':track_id,'artist_id':artist_id}
704
+ {'track_id': track_id, 'artist_id': artist_id}
660
705
  for track_id in track_ids for artist_id in artist_ids
661
706
  ])
662
707
 
663
708
  result = dbconn.execute(op)
664
-
665
709
  # the resulting tracks could now be duplicates of existing ones
666
710
  # this also takes care of clean_db
667
711
  merge_duplicate_tracks(dbconn=dbconn)
668
-
669
712
  return True
670
713
 
714
+
671
715
  @connection_provider
672
- def remove_artists_from_tracks(track_ids,artist_ids,dbconn=None):
716
+ def remove_artists_from_tracks(track_ids: list[int], artist_ids: list[int], dbconn=None) -> bool:
673
717
 
674
718
  # only tracks that have at least one other artist
675
719
  subquery = DB['trackartists'].select().where(
@@ -687,16 +731,14 @@ def remove_artists_from_tracks(track_ids,artist_ids,dbconn=None):
687
731
  )
688
732
 
689
733
  result = dbconn.execute(op)
690
-
691
734
  # the resulting tracks could now be duplicates of existing ones
692
735
  # this also takes care of clean_db
693
736
  merge_duplicate_tracks(dbconn=dbconn)
694
-
695
737
  return True
696
738
 
697
739
 
698
740
  @connection_provider
699
- def add_artists_to_albums(album_ids,artist_ids,dbconn=None):
741
+ def add_artists_to_albums(album_ids: list[int], artist_ids: list[int], dbconn=None) -> bool:
700
742
 
701
743
  op = DB['albumartists'].insert().values([
702
744
  {'album_id':album_id,'artist_id':artist_id}
@@ -704,16 +746,14 @@ def add_artists_to_albums(album_ids,artist_ids,dbconn=None):
704
746
  ])
705
747
 
706
748
  result = dbconn.execute(op)
707
-
708
749
  # the resulting albums could now be duplicates of existing ones
709
750
  # this also takes care of clean_db
710
751
  merge_duplicate_albums(dbconn=dbconn)
711
-
712
752
  return True
713
753
 
714
754
 
715
755
  @connection_provider
716
- def remove_artists_from_albums(album_ids,artist_ids,dbconn=None):
756
+ def remove_artists_from_albums(album_ids: list[int], artist_ids: list[int], dbconn=None) -> bool:
717
757
 
718
758
  # no check here, albums are allowed to have zero artists
719
759
 
@@ -725,17 +765,16 @@ def remove_artists_from_albums(album_ids,artist_ids,dbconn=None):
725
765
  )
726
766
 
727
767
  result = dbconn.execute(op)
728
-
729
768
  # the resulting albums could now be duplicates of existing ones
730
769
  # this also takes care of clean_db
731
770
  merge_duplicate_albums(dbconn=dbconn)
732
-
733
771
  return True
734
772
 
773
+
735
774
  ### Merge
736
775
 
737
776
  @connection_provider
738
- def merge_tracks(target_id,source_ids,dbconn=None):
777
+ def merge_tracks(target_id: int, source_ids: list[int], dbconn=None) -> bool:
739
778
 
740
779
  op = DB['scrobbles'].update().where(
741
780
  DB['scrobbles'].c.track_id.in_(source_ids)
@@ -744,11 +783,11 @@ def merge_tracks(target_id,source_ids,dbconn=None):
744
783
  )
745
784
  result = dbconn.execute(op)
746
785
  clean_db(dbconn=dbconn)
747
-
748
786
  return True
749
787
 
788
+
750
789
  @connection_provider
751
- def merge_artists(target_id,source_ids,dbconn=None):
790
+ def merge_artists(target_id: int, source_ids: list[int], dbconn=None) -> bool:
752
791
 
753
792
  # some tracks could already have multiple of the to be merged artists
754
793
 
@@ -776,7 +815,6 @@ def merge_artists(target_id,source_ids,dbconn=None):
776
815
 
777
816
  result = dbconn.execute(op)
778
817
 
779
-
780
818
  # same for albums
781
819
  op = DB['albumartists'].select().where(
782
820
  DB['albumartists'].c.artist_id.in_(source_ids + [target_id])
@@ -797,7 +835,6 @@ def merge_artists(target_id,source_ids,dbconn=None):
797
835
 
798
836
  result = dbconn.execute(op)
799
837
 
800
-
801
838
  # tracks_artists = {}
802
839
  # for row in result:
803
840
  # tracks_artists.setdefault(row.track_id,[]).append(row.artist_id)
@@ -814,15 +851,14 @@ def merge_artists(target_id,source_ids,dbconn=None):
814
851
  # result = dbconn.execute(op)
815
852
 
816
853
  # this could have created duplicate tracks and albums
817
- merge_duplicate_tracks(artist_id=target_id,dbconn=dbconn)
818
- merge_duplicate_albums(artist_id=target_id,dbconn=dbconn)
854
+ merge_duplicate_tracks(artist_id=target_id, dbconn=dbconn)
855
+ merge_duplicate_albums(artist_id=target_id, dbconn=dbconn)
819
856
  clean_db(dbconn=dbconn)
820
-
821
857
  return True
822
858
 
823
859
 
824
860
  @connection_provider
825
- def merge_albums(target_id,source_ids,dbconn=None):
861
+ def merge_albums(target_id: int, source_ids: list[int], dbconn=None) -> bool:
826
862
 
827
863
  op = DB['tracks'].update().where(
828
864
  DB['tracks'].c.album_id.in_(source_ids)
@@ -831,7 +867,6 @@ def merge_albums(target_id,source_ids,dbconn=None):
831
867
  )
832
868
  result = dbconn.execute(op)
833
869
  clean_db(dbconn=dbconn)
834
-
835
870
  return True
836
871
 
837
872
 
@@ -860,19 +895,24 @@ def get_scrobbles_of_artist(artist,since=None,to=None,resolve_references=True,li
860
895
  op = op.order_by(sql.desc('timestamp'))
861
896
  else:
862
897
  op = op.order_by(sql.asc('timestamp'))
863
- if limit:
898
+ if limit and not associated:
899
+ # if we count associated we cant limit here because we remove stuff later!
864
900
  op = op.limit(limit)
865
901
  result = dbconn.execute(op).all()
866
902
 
867
903
  # remove duplicates (multiple associated artists in the song, e.g. Irene & Seulgi being both counted as Red Velvet)
868
904
  # distinct on doesn't seem to exist in sqlite
869
- seen = set()
870
- filtered_result = []
871
- for row in result:
872
- if row.timestamp not in seen:
873
- filtered_result.append(row)
874
- seen.add(row.timestamp)
875
- result = filtered_result
905
+ if associated:
906
+ seen = set()
907
+ filtered_result = []
908
+ for row in result:
909
+ if row.timestamp not in seen:
910
+ filtered_result.append(row)
911
+ seen.add(row.timestamp)
912
+ result = filtered_result
913
+ if limit:
914
+ result = result[:limit]
915
+
876
916
 
877
917
 
878
918
  if resolve_references:
@@ -962,7 +1002,6 @@ def get_scrobbles(since=None,to=None,resolve_references=True,limit=None,reverse=
962
1002
  result = scrobbles_db_to_dict(result,dbconn=dbconn)
963
1003
  #result = [scrobble_db_to_dict(row,resolve_references=resolve_references) for i,row in enumerate(result) if i<max]
964
1004
 
965
-
966
1005
  return result
967
1006
 
968
1007
 
@@ -1072,7 +1111,7 @@ def count_scrobbles_by_artist(since,to,associated=True,resolve_ids=True,dbconn=N
1072
1111
  DB['scrobbles'].c.timestamp.between(since,to)
1073
1112
  ).group_by(
1074
1113
  artistselect
1075
- ).order_by(sql.desc('count'))
1114
+ ).order_by(sql.desc('count'),sql.desc('really_by_this_artist'))
1076
1115
  result = dbconn.execute(op).all()
1077
1116
 
1078
1117
  if resolve_ids:
@@ -1601,48 +1640,52 @@ def get_credited_artists(*artists,dbconn=None):
1601
1640
 
1602
1641
  @cached_wrapper
1603
1642
  @connection_provider
1604
- def get_track(id,dbconn=None):
1643
+ def get_track(track_id: int, dbconn=None) -> TrackDict:
1605
1644
  op = DB['tracks'].select().where(
1606
- DB['tracks'].c.id==id
1645
+ DB['tracks'].c.id == track_id
1607
1646
  )
1608
1647
  result = dbconn.execute(op).all()
1609
1648
 
1610
1649
  trackinfo = result[0]
1611
- return track_db_to_dict(trackinfo,dbconn=dbconn)
1650
+ return track_db_to_dict(trackinfo, dbconn=dbconn)
1651
+
1612
1652
 
1613
1653
  @cached_wrapper
1614
1654
  @connection_provider
1615
- def get_artist(id,dbconn=None):
1655
+ def get_artist(artist_id: int, dbconn=None) -> str:
1616
1656
  op = DB['artists'].select().where(
1617
- DB['artists'].c.id==id
1657
+ DB['artists'].c.id == artist_id
1618
1658
  )
1619
1659
  result = dbconn.execute(op).all()
1620
1660
 
1621
1661
  artistinfo = result[0]
1622
- return artist_db_to_dict(artistinfo,dbconn=dbconn)
1662
+ return artist_db_to_dict(artistinfo, dbconn=dbconn)
1663
+
1623
1664
 
1624
1665
  @cached_wrapper
1625
1666
  @connection_provider
1626
- def get_album(id,dbconn=None):
1667
+ def get_album(album_id: int, dbconn=None) -> AlbumDict:
1627
1668
  op = DB['albums'].select().where(
1628
- DB['albums'].c.id==id
1669
+ DB['albums'].c.id == album_id
1629
1670
  )
1630
1671
  result = dbconn.execute(op).all()
1631
1672
 
1632
1673
  albuminfo = result[0]
1633
- return album_db_to_dict(albuminfo,dbconn=dbconn)
1674
+ return album_db_to_dict(albuminfo, dbconn=dbconn)
1675
+
1634
1676
 
1635
1677
  @cached_wrapper
1636
1678
  @connection_provider
1637
- def get_scrobble(timestamp, include_internal=False, dbconn=None):
1679
+ def get_scrobble(timestamp: int, include_internal=False, dbconn=None) -> ScrobbleDict:
1638
1680
  op = DB['scrobbles'].select().where(
1639
- DB['scrobbles'].c.timestamp==timestamp
1681
+ DB['scrobbles'].c.timestamp == timestamp
1640
1682
  )
1641
1683
  result = dbconn.execute(op).all()
1642
1684
 
1643
1685
  scrobble = result[0]
1644
1686
  return scrobbles_db_to_dict(rows=[scrobble], include_internal=include_internal)[0]
1645
1687
 
1688
+
1646
1689
  @cached_wrapper
1647
1690
  @connection_provider
1648
1691
  def search_artist(searchterm,dbconn=None):
@@ -1684,6 +1727,11 @@ def clean_db(dbconn=None):
1684
1727
  log(f"Database Cleanup...")
1685
1728
 
1686
1729
  to_delete = [
1730
+ # NULL associations
1731
+ "from albumartists where album_id is NULL",
1732
+ "from albumartists where artist_id is NULL",
1733
+ "from trackartists where track_id is NULL",
1734
+ "from trackartists where artist_id is NULL",
1687
1735
  # tracks with no scrobbles (trackartist entries first)
1688
1736
  "from trackartists where track_id in (select id from tracks where id not in (select track_id from scrobbles))",
1689
1737
  "from tracks where id not in (select track_id from scrobbles)",