malojaserver 3.2.2__py3-none-any.whl → 3.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2 -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 +29 -28
- maloja/proccontrol/tasks/export.py +2 -1
- maloja/proccontrol/tasks/import_scrobbles.py +57 -15
- maloja/server.py +4 -5
- maloja/setup.py +13 -7
- 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/partials/album_showcase.jinja +1 -1
- maloja/web/jinja/snippets/entityrow.jinja +2 -2
- maloja/web/jinja/snippets/links.jinja +3 -1
- maloja/web/static/css/maloja.css +8 -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.2.dist-info → malojaserver-3.2.3.dist-info}/METADATA +10 -46
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/RECORD +38 -38
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/WHEEL +1 -1
- {malojaserver-3.2.2.dist-info → malojaserver-3.2.3.dist-info}/LICENSE +0 -0
- {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
|
248
|
-
|
249
|
-
|
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
|
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
|
273
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
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
|
-
|
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(
|
383
|
+
dbconn.execute(DB['scrobbles'].insert().values(
|
384
|
+
**scrobble_entry
|
385
|
+
))
|
348
386
|
success += 1
|
349
|
-
except sql.exc.IntegrityError
|
350
|
-
|
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
|
-
|
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:
|
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:
|
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:
|
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(
|
631
|
+
def edit_artist(artist_id: int, artistupdatedict: str, dbconn=None) -> bool:
|
586
632
|
|
587
|
-
artist = get_artist(
|
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,
|
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==
|
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(
|
654
|
+
def edit_track(track_id: int, trackupdatedict: dict, dbconn=None) -> bool:
|
609
655
|
|
610
|
-
track = get_track(
|
611
|
-
changedtrack = {**track
|
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,
|
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==
|
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(
|
677
|
+
def edit_album(album_id: int, albumupdatedict: dict, dbconn=None) -> bool:
|
632
678
|
|
633
|
-
album = get_album(
|
634
|
-
changedalbum = {**album
|
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,
|
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==
|
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
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
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(
|
1643
|
+
def get_track(track_id: int, dbconn=None) -> TrackDict:
|
1605
1644
|
op = DB['tracks'].select().where(
|
1606
|
-
DB['tracks'].c.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(
|
1655
|
+
def get_artist(artist_id: int, dbconn=None) -> str:
|
1616
1656
|
op = DB['artists'].select().where(
|
1617
|
-
DB['artists'].c.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(
|
1667
|
+
def get_album(album_id: int, dbconn=None) -> AlbumDict:
|
1627
1668
|
op = DB['albums'].select().where(
|
1628
|
-
DB['albums'].c.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)",
|