diff --git a/TODO.md b/TODO.md index 44b864d0..e54eebbf 100644 --- a/TODO.md +++ b/TODO.md @@ -56,8 +56,8 @@ then artist B is similar to A with the same weight, unless overwritten. - Figure out how to update album/artist tables instead of deleting all rows when the app starts - Move get all filtering and sorting operations to the database since all sort keys are table columns +- Replace the DbManager class with cls.execute() - Paginate the following endpoints: 1. Folder tracks 2. Playlist tracks - \ No newline at end of file diff --git a/app/api/__init__.py b/app/api/__init__.py index 73a0c663..b883ca7a 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -76,6 +76,7 @@ def create_api(): CORS(app, origins="*", supports_credentials=True) # RESPONSE COMPRESSION + # Only compress JSON responses Compress(app) app.config["COMPRESS_MIMETYPES"] = [ "application/json", @@ -84,10 +85,6 @@ def create_api(): # JWT jwt = JWTManager(app) - # @jwt.user_identity_loader - # def user_identity_lookup(user): - # return user - @jwt.user_lookup_loader def user_lookup_callback(_jwt_header, jwt_data): identity = jwt_data["sub"] diff --git a/app/db/__init__.py b/app/db/__init__.py index e6d1cc54..42fcb287 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -9,14 +9,12 @@ from sqlalchemy import ( from sqlalchemy.engine import Engine from sqlalchemy import event -from sqlalchemy.orm import ( - DeclarativeBase, - MappedAsDataclass, - Session -) +from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Session from app.db.engine import DbEngine + +# Enable foreign key constraints for SQLite @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() @@ -25,9 +23,12 @@ def set_sqlite_pragma(dbapi_connection, connection_record): class DbManager: + """ """ + def __init__(self, commit: bool = False): self.commit = commit self.conn = DbEngine.engine.connect() + with Session(DbEngine.engine) as session: session.connection @@ -42,9 +43,15 @@ class DbManager: class Base(MappedAsDataclass, DeclarativeBase): + """ + Base class for all database models. + + It has methods common to all tables. eg. `insert_one`, `insert_many`, `remove_all`, `remove_one`, `all`, `count`. + """ + @classmethod def execute(cls, stmt: Any, commit: bool = False): - with DbManager(commit=commit) as conn: + with DbEngine.manager(commit=commit) as conn: return conn.execute(stmt) @classmethod @@ -52,8 +59,7 @@ class Base(MappedAsDataclass, DeclarativeBase): """ Inserts multiple items into the database. """ - with DbManager(commit=True) as conn: - return conn.execute(insert(cls).values(items)) + return cls.execute(insert(cls).values(items), commit=True) @classmethod def insert_one(cls, item: dict[str, Any]): @@ -64,12 +70,11 @@ class Base(MappedAsDataclass, DeclarativeBase): @classmethod def remove_all(cls): - with DbManager(commit=True) as conn: - conn.execute(delete(cls)) + return cls.execute(delete(cls), commit=True) @classmethod def remove_one(cls, id: int): - cls.execute(delete(cls).where(cls.id == id), commit=True) + return cls.execute(delete(cls).where(cls.id == id), commit=True) @classmethod def all(cls): @@ -80,5 +85,8 @@ class Base(MappedAsDataclass, DeclarativeBase): return cls.execute(select(func.count()).select_from(cls)).scalar() -def create_all(): +def create_all_tables(): + """ + Creates all the tables that build on the Base class. + """ Base().metadata.create_all(DbEngine.engine) diff --git a/app/db/engine.py b/app/db/engine.py index 233ba004..841702de 100644 --- a/app/db/engine.py +++ b/app/db/engine.py @@ -1,5 +1,30 @@ +from contextlib import contextmanager from sqlalchemy import Engine class DbEngine: - engine: Engine = None + """ + The database engine instance. + """ + + engine: Engine + + @classmethod + @contextmanager + def manager(cls, commit: bool): + """ + This context manager manages access to the database. + + When the context manager is entered, it returns a connection object that can be used to execute SQL statements. + + If the `commit` parameter is set to `True`, the context manager will commit the transaction when it exits. + """ + + try: + conn = cls.engine.connect() + yield conn.execution_options(preserve_rowcount=True) + + if commit: + conn.commit() + finally: + conn.close() diff --git a/app/db/libdata.py b/app/db/libdata.py index 23e93af9..caa0b3ab 100644 --- a/app/db/libdata.py +++ b/app/db/libdata.py @@ -23,6 +23,9 @@ from typing import Any, Iterable, Optional def create_all(): """ Create all the tables defined in this file. + + NOTE: We need this function because the MasterBase does not collect + the tables defined here (as they are grand-children of the MasterBase) """ Base.metadata.create_all(DbEngine.engine) @@ -317,7 +320,7 @@ class AlbumTable(Base): # NOTE: The artist dict keys need to in the same order they appear in the db for this to work! select(AlbumTable).where(AlbumTable.artisthashes.contains(artist)) ) - albums[artist] = (albums_to_dataclasses(result.fetchall())) + albums[artist] = albums_to_dataclasses(result.fetchall()) return albums diff --git a/app/db/sqlite/utils.py b/app/db/sqlite/utils.py index 27535d25..06a45640 100644 --- a/app/db/sqlite/utils.py +++ b/app/db/sqlite/utils.py @@ -90,10 +90,10 @@ class SQLiteManager: if self.test_db_path: db_path = self.test_db_path else: - db_path = settings.Db.get_app_db_path() + db_path = settings.DbPaths.get_app_db_path() if self.userdata_db: - db_path = settings.Db.get_userdata_db_path() + db_path = settings.DbPaths.get_userdata_db_path() self.conn = sqlite3.connect( db_path, diff --git a/app/lib/albumslib.py b/app/lib/albumslib.py index 36034f3a..afa28101 100644 --- a/app/lib/albumslib.py +++ b/app/lib/albumslib.py @@ -2,17 +2,11 @@ Contains methods relating to albums. """ -from dataclasses import asdict -from typing import Any -from itertools import groupby - from app.models.track import Track -from app.store.albums import AlbumStore -from app.store.tracks import TrackStore -def remove_duplicate_on_merge_versions(tracks: list[Track]) -> list[Track]: +def remove_duplicate_on_merge_versions(tracks: list[Track]): """ Removes duplicate tracks when merging versions of the same album. """ @@ -21,8 +15,6 @@ def remove_duplicate_on_merge_versions(tracks: list[Track]) -> list[Track]: def sort_by_track_no(tracks: list[Track]): - # tracks = [asdict(t) for t in tracks] - for t in tracks: track = str(t.track).zfill(3) t._pos = int(f"{t.disc}{track}") diff --git a/app/lib/artistlib.py b/app/lib/artistlib.py index c101b515..f4664e84 100644 --- a/app/lib/artistlib.py +++ b/app/lib/artistlib.py @@ -145,34 +145,3 @@ class CheckArtistImages: if url is not None: return DownloadImage(url, name=f"{artist['artisthash']}.webp") - - -# def fetch_album_bio(title: str, albumartist: str) -> str | None: """ Returns the album bio for a given album. """ -# last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={ -# }&format=json".format( settings.Paths.LAST_FM_API_KEY, albumartist, title ) - -# try: -# response = requests.get(last_fm_url) -# data = response.json() -# except: -# return None - -# try: -# bio = data["album"]["wiki"]["summary"].split('= 0 and user_agent.find("Chrome") < 0 if is_safari: