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

Merge branch 'feature-albums' into next_minor_version

This commit is contained in:
krateng 2023-04-01 04:19:33 +02:00
commit 88c5d1da00
12 changed files with 204 additions and 86 deletions

View File

@ -148,7 +148,7 @@ def print_info():
print("Could not determine dependency versions.") print("Could not determine dependency versions.")
print() print()
@mainfunction({"l":"level","v":"version","V":"version"},flags=['version','include_images'],shield=True) @mainfunction({"l":"level","v":"version","V":"version"},flags=['version','include_images','prefer_existing'],shield=True)
def main(*args,**kwargs): def main(*args,**kwargs):
actions = { actions = {
@ -166,7 +166,7 @@ def main(*args,**kwargs):
"generate":generate.generate_scrobbles, # maloja generate 400 "generate":generate.generate_scrobbles, # maloja generate 400
"export":tasks.export, # maloja export "export":tasks.export, # maloja export
"apidebug":apidebug.run, # maloja apidebug "apidebug":apidebug.run, # maloja apidebug
"parsealbums":tasks.parse_albums, # maloja parsealbums "parsealbums":tasks.parse_albums, # maloja parsealbums --strategy majority
# aux # aux
"info":print_info "info":print_info
} }

View File

@ -2,14 +2,14 @@
from bottle import request, response, FormsDict from bottle import request, response, FormsDict
# we're running an auxiliary task that doesn't require all the random background # decorator that makes sure this function is only run in normal operation,
# nonsense to be fired up # not when we run a task that needs to access the database
# this is temporary def no_aux_mode(func):
# FIX YO DAMN ARCHITECTURE ALREADY def wrapper(*args,**kwargs):
AUX_MODE = False from ..pkg_global import conf
def set_aux_mode(): if conf.AUX_MODE: return
global AUX_MODE return func(*args,**kwargs)
AUX_MODE = True return wrapper
# rest of the project # rest of the project
@ -619,6 +619,7 @@ def start_db():
# Upgrade database # Upgrade database
from .. import upgrade from .. import upgrade
upgrade.upgrade_db(sqldb.add_scrobbles) upgrade.upgrade_db(sqldb.add_scrobbles)
upgrade.parse_old_albums()
# Load temporary tables # Load temporary tables
from . import associated from . import associated

View File

@ -10,7 +10,7 @@ from doreah.regular import runhourly
from doreah.logging import log from doreah.logging import log
from ..pkg_global.conf import malojaconfig from ..pkg_global.conf import malojaconfig
from . import no_aux_mode
if malojaconfig['USE_GLOBAL_CACHE']: if malojaconfig['USE_GLOBAL_CACHE']:
@ -21,15 +21,12 @@ if malojaconfig['USE_GLOBAL_CACHE']:
@runhourly @runhourly
@no_aux_mode
def maintenance(): def maintenance():
from . import AUX_MODE
if AUX_MODE: return
print_stats() print_stats()
trim_cache() trim_cache()
def print_stats(): def print_stats():
from . import AUX_MODE
if AUX_MODE: return
for name,c in (('Cache',cache),('Entity Cache',entitycache)): for name,c in (('Cache',cache),('Entity Cache',entitycache)):
hits, misses = c.get_stats() hits, misses = c.get_stats()
log(f"{name}: Size: {len(c)} | Hits: {hits}/{hits+misses} | Estimated Memory: {human_readable_size(c)}") log(f"{name}: Size: {len(c)} | Hits: {hits}/{hits+misses} | Estimated Memory: {human_readable_size(c)}")
@ -37,8 +34,6 @@ if malojaconfig['USE_GLOBAL_CACHE']:
def cached_wrapper(inner_func): def cached_wrapper(inner_func):
from . import AUX_MODE
if AUX_MODE: return inner_func
def outer_func(*args,**kwargs): def outer_func(*args,**kwargs):
@ -63,8 +58,6 @@ if malojaconfig['USE_GLOBAL_CACHE']:
# we don't want a new cache entry for every single combination, but keep a common # we don't want a new cache entry for every single combination, but keep a common
# cache that's aware of what we're calling # cache that's aware of what we're calling
def cached_wrapper_individual(inner_func): def cached_wrapper_individual(inner_func):
from . import AUX_MODE
if AUX_MODE: return
def outer_func(set_arg,**kwargs): def outer_func(set_arg,**kwargs):
if 'dbconn' in kwargs: if 'dbconn' in kwargs:
@ -88,9 +81,8 @@ if malojaconfig['USE_GLOBAL_CACHE']:
return outer_func return outer_func
@no_aux_mode
def invalidate_caches(scrobbletime=None): def invalidate_caches(scrobbletime=None):
from . import AUX_MODE
if AUX_MODE: return
cleared, kept = 0, 0 cleared, kept = 0, 0
for k in cache.keys(): for k in cache.keys():
@ -102,16 +94,11 @@ if malojaconfig['USE_GLOBAL_CACHE']:
kept += 1 kept += 1
log(f"Invalidated {cleared} of {cleared+kept} DB cache entries") log(f"Invalidated {cleared} of {cleared+kept} DB cache entries")
@no_aux_mode
def invalidate_entity_cache(): def invalidate_entity_cache():
from . import AUX_MODE
if AUX_MODE: return
entitycache.clear() entitycache.clear()
def trim_cache(): def trim_cache():
from . import AUX_MODE
if AUX_MODE: return
ramprct = psutil.virtual_memory().percent ramprct = psutil.virtual_memory().percent
if ramprct > malojaconfig["DB_MAX_MEMORY"]: if ramprct > malojaconfig["DB_MAX_MEMORY"]:
log(f"{ramprct}% RAM usage, clearing cache!") log(f"{ramprct}% RAM usage, clearing cache!")

View File

@ -16,7 +16,7 @@ class DatabaseNotBuilt(HTTPError):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
status=503, status=503,
body="The Maloja Database is being upgraded to Version 3. This could take quite a long time! (~ 2-5 minutes per 10 000 scrobbles)", body="The Maloja Database is being upgraded to support new Maloja features. This could take a while.",
headers={"Retry-After":120} headers={"Retry-After":120}
) )

View File

@ -1,4 +1,5 @@
import sqlalchemy as sql import sqlalchemy as sql
from sqlalchemy.dialects.sqlite import insert as sqliteinsert
import json import json
import unicodedata import unicodedata
import math import math
@ -8,6 +9,7 @@ from threading import Lock
from ..pkg_global.conf import data_dir from ..pkg_global.conf import data_dir
from .dbcache import cached_wrapper, cached_wrapper_individual, invalidate_caches, invalidate_entity_cache from .dbcache import cached_wrapper, cached_wrapper_individual, invalidate_caches, invalidate_entity_cache
from . import exceptions as exc from . import exceptions as exc
from . import no_aux_mode
from doreah.logging import log from doreah.logging import log
from doreah.regular import runhourly, runmonthly from doreah.regular import runhourly, runmonthly
@ -19,6 +21,13 @@ from doreah.regular import runhourly, runmonthly
DBTABLES = { DBTABLES = {
# name - type - foreign key - kwargs # name - type - foreign key - kwargs
'_maloja':{
'columns':[
("key", sql.String, {'primary_key':True}),
("value", sql.String, {})
],
'extraargs':(),'extrakwargs':{}
},
'scrobbles':{ 'scrobbles':{
'columns':[ 'columns':[
("timestamp", sql.Integer, {'primary_key':True}), ("timestamp", sql.Integer, {'primary_key':True}),
@ -150,6 +159,29 @@ def connection_provider(func):
wrapper.__innerfunc__ = func wrapper.__innerfunc__ = func
return wrapper return wrapper
@connection_provider
def get_maloja_info(keys,dbconn=None):
op = DB['_maloja'].select().where(
DB['_maloja'].c.key.in_(keys)
)
result = dbconn.execute(op).all()
info = {}
for row in result:
info[row.key] = row.value
return info
@connection_provider
def set_maloja_info(info,dbconn=None):
for k in info:
op = sqliteinsert(DB['_maloja']).values(
key=k, value=info[k]
).on_conflict_do_update(
index_elements=['key'],
set_={'value':info[k]}
)
dbconn.execute(op)
##### DB <-> Dict translations ##### DB <-> Dict translations
## ATTENTION ALL ADVENTURERS ## ATTENTION ALL ADVENTURERS
@ -451,14 +483,11 @@ def get_artist_id(artistname,create_new=True,dbconn=None):
@cached_wrapper @cached_wrapper
@connection_provider @connection_provider
def get_album_id(albumdict,create_new=True,dbconn=None): def get_album_id(albumdict,create_new=True,ignore_albumartists=False,dbconn=None):
ntitle = normalize_name(albumdict['albumtitle']) ntitle = normalize_name(albumdict['albumtitle'])
artist_ids = [get_artist_id(a,dbconn=dbconn) for a in albumdict.get('artists') or []] artist_ids = [get_artist_id(a,dbconn=dbconn) for a in albumdict.get('artists') or []]
artist_ids = list(set(artist_ids)) artist_ids = list(set(artist_ids))
op = DB['albums'].select( op = DB['albums'].select(
# DB['albums'].c.id # DB['albums'].c.id
).where( ).where(
@ -466,20 +495,23 @@ def get_album_id(albumdict,create_new=True,dbconn=None):
) )
result = dbconn.execute(op).all() result = dbconn.execute(op).all()
for row in result: for row in result:
# check if the artists are the same if ignore_albumartists:
foundtrackartists = []
op = DB['albumartists'].select(
# DB['albumartists'].c.artist_id
).where(
DB['albumartists'].c.album_id==row.id
)
result = dbconn.execute(op).all()
match_artist_ids = [r.artist_id for r in result]
#print("required artists",artist_ids,"this match",match_artist_ids)
if set(artist_ids) == set(match_artist_ids):
#print("ID for",albumdict['title'],"was",row[0])
return row.id return row.id
else:
# check if the artists are the same
foundtrackartists = []
op = DB['albumartists'].select(
# DB['albumartists'].c.artist_id
).where(
DB['albumartists'].c.album_id==row.id
)
result = dbconn.execute(op).all()
match_artist_ids = [r.artist_id for r in result]
#print("required artists",artist_ids,"this match",match_artist_ids)
if set(artist_ids) == set(match_artist_ids):
#print("ID for",albumdict['title'],"was",row[0])
return row.id
if not create_new: return None if not create_new: return None
@ -1361,11 +1393,9 @@ def search_album(searchterm,dbconn=None):
@runhourly @runhourly
@connection_provider @connection_provider
@no_aux_mode
def clean_db(dbconn=None): def clean_db(dbconn=None):
from . import AUX_MODE
if AUX_MODE: return
with SCROBBLE_LOCK: with SCROBBLE_LOCK:
log(f"Database Cleanup...") log(f"Database Cleanup...")
@ -1410,11 +1440,9 @@ def clean_db(dbconn=None):
@runmonthly @runmonthly
@no_aux_mode
def renormalize_names(): def renormalize_names():
from . import AUX_MODE
if AUX_MODE: return
with SCROBBLE_LOCK: with SCROBBLE_LOCK:
with engine.begin() as conn: with engine.begin() as conn:
rows = conn.execute(DB['artists'].select()).all() rows = conn.execute(DB['artists'].select()).all()
@ -1573,7 +1601,7 @@ def guess_albums(track_ids=None,replace=False,dbconn=None):
}} }}
if len(artists) == 0: if len(artists) == 0:
# for albums without artist, assume track artist # for albums without artist, assume track artist
res[track_id]["guess_artists"] = True res[track_id]["guess_artists"] = []
else: else:
res[track_id] = {"assigned":False,"reason":"Not enough data"} res[track_id] = {"assigned":False,"reason":"Not enough data"}
@ -1582,7 +1610,7 @@ def guess_albums(track_ids=None,replace=False,dbconn=None):
missing_artists = [track_id for track_id in res if res[track_id].get("guess_artists")] missing_artists = [track_id for track_id in res if "guess_artists" in res[track_id]]
#we're pointlessly getting the albumartist names here even though the IDs would be enough #we're pointlessly getting the albumartist names here even though the IDs would be enough
#but it's better for function separation I guess #but it's better for function separation I guess
@ -1599,10 +1627,7 @@ def guess_albums(track_ids=None,replace=False,dbconn=None):
result = dbconn.execute(op).all() result = dbconn.execute(op).all()
for row in result: for row in result:
res[row.track_id]["assigned"]["artists"].append(row.name) res[row.track_id]["guess_artists"].append(row.name)
for track_id in res:
if res[track_id].get("guess_artists"):
del res[track_id]["guess_artists"]
return res return res

View File

@ -134,6 +134,14 @@ resolve_semaphore = BoundedSemaphore(8)
def resolve_track_image(track_id): def resolve_track_image(track_id):
if malojaconfig["USE_ALBUM_ARTWORK_FOR_TRACKS"]:
track = database.sqldb.get_track(track_id)
if "album" in track:
album_id = database.sqldb.get_album_id(track["album"])
albumart = resolve_album_image(album_id)
if albumart:
return albumart
with resolve_semaphore: with resolve_semaphore:
# check cache # check cache
result = get_image_from_cache(track_id,'tracks') result = get_image_from_cache(track_id,'tracks')

View File

@ -6,6 +6,8 @@ from doreah.configuration import types as tp
from ..__pkginfo__ import VERSION from ..__pkginfo__ import VERSION
# this mode specifies whether we run some auxiliary task instead of the main server
AUX_MODE = True
# if DATA_DIRECTORY is specified, this is the directory to use for EVERYTHING, no matter what # if DATA_DIRECTORY is specified, this is the directory to use for EVERYTHING, no matter what
@ -194,6 +196,7 @@ malojaconfig = Configuration(
"album_showcase":(tp.Boolean(), "Display Album Showcase", True, "Display a graphical album showcase for artist overview pages instead of a chart list"), "album_showcase":(tp.Boolean(), "Display Album Showcase", True, "Display a graphical album showcase for artist overview pages instead of a chart list"),
"display_art_icons":(tp.Boolean(), "Display Album/Artist Icons", True), "display_art_icons":(tp.Boolean(), "Display Album/Artist Icons", True),
"default_album_artist":(tp.String(), "Default Albumartist", "Various Artists"), "default_album_artist":(tp.String(), "Default Albumartist", "Various Artists"),
"use_album_artwork_for_tracks":(tp.Boolean(), "Use Album Artwork for tracks", True),
"discourage_cpu_heavy_stats":(tp.Boolean(), "Discourage CPU-heavy stats", False, "Prevent visitors from mindlessly clicking on CPU-heavy options. Does not actually disable them for malicious actors!"), "discourage_cpu_heavy_stats":(tp.Boolean(), "Discourage CPU-heavy stats", False, "Prevent visitors from mindlessly clicking on CPU-heavy options. Does not actually disable them for malicious actors!"),
"use_local_images":(tp.Boolean(), "Use Local Images", True), "use_local_images":(tp.Boolean(), "Use Local Images", True),
#"local_image_rotate":(tp.Integer(), "Local Image Rotate", 3600), #"local_image_rotate":(tp.Integer(), "Local Image Rotate", 3600),
@ -300,15 +303,6 @@ data_dir = {
### write down the last ran version
with open(pthj(dir_settings['state'],".lastmalojaversion"),"w") as filed:
filed.write(VERSION)
filed.write("\n")
### DOREAH CONFIGURATION ### DOREAH CONFIGURATION
from doreah import config from doreah import config
@ -334,7 +328,8 @@ config(
custom_css_files = [f for f in os.listdir(data_dir['css']()) if f.lower().endswith('.css')] custom_css_files = [f for f in os.listdir(data_dir['css']()) if f.lower().endswith('.css')]
from ..database.sqldb import set_maloja_info
set_maloja_info({'last_run_version':VERSION})
# what the fuck did i just write # what the fuck did i just write
# this spaghetti file is proudly sponsored by the rice crackers i'm eating at the # this spaghetti file is proudly sponsored by the rice crackers i'm eating at the

View File

@ -21,9 +21,6 @@ outputs = {
def import_scrobbles(inputf): def import_scrobbles(inputf):
from ...database import set_aux_mode
set_aux_mode()
from ...database.sqldb import add_scrobbles from ...database.sqldb import add_scrobbles
result = { result = {

View File

@ -1,23 +1,108 @@
from doreah.io import col
def parse_albums(strategy=None,prefer_existing=False):
if strategy not in ("track","none","all","majority","most"):
print("""
Please specify your album parsing strategy:
--strategy Specify what strategy to use when the scrobble contains
no information about album artists.
track Take the track artists. This can lead to
separate albums being created for compilation
albums or albums that have collaboration tracks.
none Merge all albums with the same name and assign
'Various Artists' as the album artist.
all Merge all albums with the same name and assign
every artist that appears on the album as an album
artist.
majority Merge all albums with the same name and assign
artists that appear in at least half the tracks
of the album as album artists. [RECOMMENDED]
most Merge all albums with the same name and assign
the artist that appears most on the album as album
artist.
--prefer_existing If an album with the same name already exists, use it
without further examination of track artists.
""")
return
def parse_albums(replace=False):
from ...database import set_aux_mode
set_aux_mode()
from ...database.sqldb import guess_albums, get_album_id, add_track_to_album from ...database.sqldb import guess_albums, get_album_id, add_track_to_album
print("Parsing album information...") print("Parsing album information...")
result = guess_albums(replace=replace) result = guess_albums()
result = {track_id:result[track_id] for track_id in result if result[track_id]["assigned"]} result = {track_id:result[track_id] for track_id in result if result[track_id]["assigned"]}
print("Adding",len(result),"tracks to albums...") print("Found",col['yellow'](len(result)),"Tracks to assign albums to")
result_authorative = {track_id:result[track_id] for track_id in result if result[track_id]["assigned"]["artists"]}
result_guesswork = {track_id:result[track_id] for track_id in result if not result[track_id]["assigned"]["artists"]}
i = 0 i = 0
for track_id in result:
album_id = get_album_id(result[track_id]["assigned"]) def countup(i):
add_track_to_album(track_id,album_id) i+=1
i += 1
if (i % 100) == 0: if (i % 100) == 0:
print(i,"of",len(result)) print(f"Added album information for {i} of {len(result)} tracks...")
print("Done!") return i
for track_id in result_authorative:
albuminfo = result[track_id]['assigned']
album_id = get_album_id(albuminfo)
add_track_to_album(track_id,album_id)
i=countup(i)
albums = {}
for track_id in result_guesswork:
albuminfo = result[track_id]['assigned']
# check if already exists
if prefer_existing:
album_id = get_album_id(albuminfo,ignore_albumartists=True,create_new=False)
if album_id:
add_track_to_album(track_id,album_id)
i=countup(i)
continue
if strategy == 'track':
albuminfo['artists'] = result[track_id]['guess_artists']
album_id = get_album_id(albuminfo)
add_track_to_album(track_id,album_id)
i=countup(i)
continue
if strategy == 'none':
albuminfo['artists'] = []
album_id = get_album_id(albuminfo)
add_track_to_album(track_id,album_id)
i=countup(i)
continue
if strategy in ['all','majority','most']:
cleantitle = albuminfo['albumtitle'].lower()
albums.setdefault(cleantitle,{'track_ids':[],'artists':{},'title':albuminfo['albumtitle']})
albums[cleantitle]['track_ids'].append(track_id)
for a in result[track_id]['guess_artists']:
albums[cleantitle]['artists'].setdefault(a,0)
albums[cleantitle]['artists'][a] += 1
for cleantitle in albums:
artistoptions = albums[cleantitle]['artists']
track_ids = albums[cleantitle]['track_ids']
realtitle = albums[cleantitle]['title']
if strategy == 'all':
artists = [a for a in artistoptions]
elif strategy == 'majority':
artists = [a for a in artistoptions if artistoptions[a] >= (len(track_ids) / 2)]
elif strategy == 'most':
artists = [max(artistoptions,key=artistoptions.get)]
for track_id in track_ids:
album_id = get_album_id({'albumtitle':realtitle,'artists':artists})
add_track_to_album(track_id,album_id)
i=countup(i)
print(col['lawngreen']("Done!"))

View File

@ -22,6 +22,7 @@ from .database.jinjaview import JinjaDBConnection
from .images import resolve_track_image, resolve_artist_image, resolve_album_image from .images import resolve_track_image, resolve_artist_image, resolve_album_image
from .malojauri import uri_to_internal, remove_identical from .malojauri import uri_to_internal, remove_identical
from .pkg_global.conf import malojaconfig, data_dir from .pkg_global.conf import malojaconfig, data_dir
from .pkg_global import conf
from .jinjaenv.context import jinja_environment from .jinjaenv.context import jinja_environment
from .apis import init_apis, apikeystore from .apis import init_apis, apikeystore
@ -285,6 +286,8 @@ logging.getLogger().addHandler(WaitressLogHandler())
def run_server(): def run_server():
conf.AUX_MODE = False
log("Starting up Maloja server...") log("Starting up Maloja server...")
## start database ## start database

View File

@ -11,6 +11,9 @@ from .pkg_global.conf import data_dir, dir_settings
from .apis import _apikeys from .apis import _apikeys
from .database.sqldb import get_maloja_info, set_maloja_info
# Dealing with old style tsv files - these should be phased out everywhere # Dealing with old style tsv files - these should be phased out everywhere
def read_tsvs(path,types): def read_tsvs(path,types):
result = [] result = []
@ -40,7 +43,7 @@ def upgrade_apikeys():
except Exception: except Exception:
pass pass
# v2 to v3 iupgrade
def upgrade_db(callback_add_scrobbles): def upgrade_db(callback_add_scrobbles):
oldfolder = os.path.join(dir_settings['state'],"scrobbles") oldfolder = os.path.join(dir_settings['state'],"scrobbles")
@ -88,3 +91,13 @@ def upgrade_db(callback_add_scrobbles):
callback_add_scrobbles(scrobblelist) callback_add_scrobbles(scrobblelist)
os.rename(os.path.join(oldfolder,sf),os.path.join(newfolder,sf)) os.rename(os.path.join(oldfolder,sf),os.path.join(newfolder,sf))
log("Done!",color='yellow') log("Done!",color='yellow')
# 3.2 album support
def parse_old_albums():
setting_name = "db_upgrade_albums"
if get_maloja_info([setting_name]).get(setting_name):
pass
else:
pass
#set_maloja_info({setting_name:True})

View File

@ -90,7 +90,11 @@
</table> </table>
{% if info["isalbumartist"] %} {% set albums_info = dbc.get_albums_artist_appears_on(filterkeys,limitkeys) %}
{% set ownalbums = albums_info.own_albums %}
{% set otheralbums = albums_info.appears_on %}
{% if ownalbums or otheralbums %}
{% if settings['ALBUM_SHOWCASE'] %} {% if settings['ALBUM_SHOWCASE'] %}
<h2><a href='{{ mlj_uri.create_uri("/charts_albums",filterkeys) }}'>Albums</a></h2> <h2><a href='{{ mlj_uri.create_uri("/charts_albums",filterkeys) }}'>Albums</a></h2>