diff --git a/app/api/album.py b/app/api/album.py index 8c762f1f..13dee219 100644 --- a/app/api/album.py +++ b/app/api/album.py @@ -2,6 +2,7 @@ Contains all the album routes. """ +from pprint import pprint import random from pydantic import BaseModel, Field @@ -54,6 +55,9 @@ def get_album_tracks_and_info(body: AlbumHashSchema): track_total = sum({int(t.extra.get("track_total", 1) or 1) for t in tracks}) avg_bitrate = sum(t.bitrate for t in tracks) // (len(tracks) or 1) + album.fav_userids = [1] + pprint(album) + return { "info": album, "extra": { diff --git a/app/api/favorites.py b/app/api/favorites.py index c6127a5c..481bbf66 100644 --- a/app/api/favorites.py +++ b/app/api/favorites.py @@ -11,7 +11,11 @@ from app.db.libdata import AlbumTable, TrackTable from app.db.userdata import FavoritesTable from app.models import FavType from app.settings import Defaults -from app.utils.bisection import use_bisection + +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore +from app.store.tracks import TrackStore + from app.serializers.track import serialize_track, serialize_tracks from app.serializers.artist import ( serialize_for_card as serialize_artist, @@ -46,14 +50,27 @@ def toggle_favorite(body: FavoritesAddBody): """ Adds a favorite to the database. """ - FavoritesTable.insert_item({"hash": body.hash, "type": body.type}) + + try: + FavoritesTable.insert_item({"hash": body.hash, "type": body.type}) + except: + return {"msg": "Failed! An error occured"}, 500 if body.type == FavType.track: - TrackTable.set_is_favorite(body.hash, True) + entry = TrackStore.trackhashmap.get(body.hash) + if entry is not None: + entry.toggle_favorite_user() + elif body.type == FavType.album: - AlbumTable.set_is_favorite(body.hash, True) + entry = AlbumStore.albummap.get(body.hash) + + if entry is not None: + entry.toggle_favorite_user() elif body.type == FavType.artist: - ArtistTable.set_is_favorite(body.hash, True) + entry = ArtistStore.artistmap.get(body.hash) + + if entry is not None: + entry.toggle_favorite_user() return {"msg": "Added to favorites"} @@ -95,7 +112,8 @@ def get_favorite_albums(query: GetAllOfTypeQuery): fav_albums, total = FavoritesTable.get_fav_albums(query.start, query.limit) fav_albums.reverse() - return {"albums": serialize_for_card_many(fav_albums), "total": total} + albums = AlbumStore.get_albums_by_hashes(a.hash for a in fav_albums) + return {"albums": serialize_for_card_many(albums), "total": total} @api.get("/tracks") @@ -104,6 +122,10 @@ def get_favorite_tracks(query: GetAllOfTypeQuery): Get favorite tracks """ tracks, total = FavoritesTable.get_fav_tracks(query.start, query.limit) + + tracks.reverse() + tracks = TrackTable.get_tracks_by_trackhashes([t.hash for t in tracks]) + return {"tracks": serialize_tracks(tracks), "total": total} @@ -118,6 +140,7 @@ def get_favorite_artists(query: GetAllOfTypeQuery): ) artists.reverse() + artists = ArtistStore.get_artists_by_hashes(a.hash for a in artists) return {"artists": [serialize_artist(a) for a in artists], "total": total} @@ -164,9 +187,9 @@ def get_all_favorites(query: GetAllFavoritesQuery): albums = [] artists = [] - track_master_hash = TrackTable.get_all_hashes() - album_master_hash = AlbumTable.get_all_hashes() - artist_master_hash = ArtistTable.get_all_hashes() + track_master_hash = TrackStore.trackhashmap.keys() + album_master_hash = AlbumStore.albummap.keys() + artist_master_hash = ArtistStore.artistmap.keys() # INFO: Filter out invalid hashes (file not found or tags edited) for fav in favs: @@ -188,12 +211,11 @@ def get_all_favorites(query: GetAllFavoritesQuery): "artists": len(artists), } - tracks = TrackTable.get_tracks_by_trackhashes(tracks, limit=track_limit) - albums = AlbumTable.get_albums_by_albumhashes(albums, limit=album_limit) - artists = ArtistTable.get_artists_by_artisthashes(artists, limit=artist_limit) + tracks = TrackStore.get_tracks_by_trackhashes(tracks[:track_limit]) + albums = AlbumStore.get_albums_by_hashes(albums[:album_limit]) + artists = ArtistStore.get_artists_by_hashes(artists[:artist_limit]) recents = [] - # first_n = favs for fav in favs: if len(recents) >= largest: diff --git a/app/api/scrobble/__init__.py b/app/api/scrobble/__init__.py index 19137a0b..36400c2b 100644 --- a/app/api/scrobble/__init__.py +++ b/app/api/scrobble/__init__.py @@ -3,9 +3,11 @@ from flask_openapi3 import APIBlueprint from pydantic import Field from app.api.apischemas import TrackHashSchema -from app.db.libdata import AlbumTable, ArtistTable, TrackTable from app.db.userdata import ScrobbleTable from app.settings import Defaults +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore +from app.store.tracks import TrackStore bp_tag = Tag(name="Logger", description="Log item plays") api = APIBlueprint("logger", __name__, url_prefix="/logger", abp_tags=[bp_tag]) @@ -33,14 +35,27 @@ def log_track(body: LogTrackBody): if not timestamp or duration < 5: return {"msg": "Invalid entry."}, 400 - track = TrackTable.get_track_by_trackhash(body.trackhash) - - if track is None: + trackentry = TrackStore.trackhashmap.get(body.trackhash) + if trackentry is None: return {"msg": "Track not found."}, 404 ScrobbleTable.add(dict(body)) - TrackTable.increment_playcount(body.trackhash, duration, timestamp) - AlbumTable.increment_playcount(track.albumhash, duration, timestamp) - ArtistTable.increment_playcount(track.artisthashes, duration, timestamp) + + # Update play data on the in-memory stores + track = trackentry.tracks[0] + album = AlbumStore.albummap.get(track.albumhash) + + if album: + album.increment_playcount(duration, timestamp) + + for hash in track.artisthashes: + artist = ArtistStore.artistmap.get(hash) + + if artist: + artist.increment_playcount(duration, timestamp) + + track = TrackStore.trackhashmap.get(body.trackhash) + if track: + track.increment_playcount(duration, timestamp) return {"msg": "recorded"}, 201 diff --git a/app/api/search.py b/app/api/search.py index f08840f3..19265185 100644 --- a/app/api/search.py +++ b/app/api/search.py @@ -9,9 +9,9 @@ from flask_openapi3 import APIBlueprint from app import models from app.api.apischemas import GenericLimitSchema -from app.db.libdata import TrackTable from app.lib import searchlib from app.settings import Defaults +from app.store.tracks import TrackStore tag = Tag(name="Search", description="Search for tracks, albums and artists") @@ -31,7 +31,7 @@ class Search: Calls :class:`SearchTracks` which returns the tracks that fuzzily match the search terms. Then adds them to the `SearchResults` store. """ - self.tracks = TrackTable.get_all() + self.tracks = TrackStore.get_flat_list() return searchlib.TopResults().search(self.query, tracks_only=True) def search_artists(self): @@ -124,7 +124,7 @@ def get_top_results(query: TopResultsQuery): class SearchLoadMoreQuery(SearchQuery): type: str = Field(description="The type of search", example="tracks") - index: int = Field(description="The index to start from", default=0) + start: int = Field(description="The index to start from", default=0) @api.get("/loadmore") @@ -136,26 +136,26 @@ def search_load_more(query: SearchLoadMoreQuery): NOTE: You must first initiate a search using the `/search` endpoint. """ - query = query.q + q = query.q item_type = query.type - index = query.index + index = query.start if item_type == "tracks": - t = Search(query).search_tracks() + t = Search(q).search_tracks() return { "tracks": t[index : index + SEARCH_COUNT], "more": len(t) > index + SEARCH_COUNT, } elif item_type == "albums": - a = Search(query).search_albums() + a = Search(q).search_albums() return { "albums": a[index : index + SEARCH_COUNT], "more": len(a) > index + SEARCH_COUNT, } elif item_type == "artists": - a = Search(query).search_artists() + a = Search(q).search_artists() return { "artists": a[index : index + SEARCH_COUNT], "more": len(a) > index + SEARCH_COUNT, diff --git a/app/api/settings.py b/app/api/settings.py index 3f1a3047..b333a34b 100644 --- a/app/api/settings.py +++ b/app/api/settings.py @@ -10,7 +10,7 @@ from app.db.sqlite.plugins import PluginsMethods as pdb from app.db.sqlite.tracks import SQLiteTrackMethods as trackdb from app.db.userdata import PluginTable from app.lib import populate -from app.lib.tagger import index_everything +from app.lib.index import index_everything from app.lib.watchdogg import Watcher as WatchDog from app.logger import log from app.settings import Info, Paths, SessionVarKeys @@ -238,7 +238,8 @@ def set_setting(body: SetSettingBody): @background def run_populate(): - populate.Populate(instance_key=get_random_str()) + # populate.Populate(instance_key=get_random_str()) + pass @api.get("/trigger-scan") diff --git a/app/db/__init__.py b/app/db/__init__.py index 42fcb287..4f9bcad9 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -7,41 +7,10 @@ from sqlalchemy import ( select, ) -from sqlalchemy.engine import Engine -from sqlalchemy import event -from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Session - +from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass 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() - cursor.execute("PRAGMA foreign_keys=ON") - cursor.close() - - -class DbManager: - """ """ - - def __init__(self, commit: bool = False): - self.commit = commit - self.conn = DbEngine.engine.connect() - - with Session(DbEngine.engine) as session: - session.connection - - def __enter__(self): - return self.conn.execution_options(preserve_rowcount=True) - - def __exit__(self, exc_type, exc_val, exc_tb): - if self.commit: - self.conn.commit() - - self.conn.close() - - class Base(MappedAsDataclass, DeclarativeBase): """ Base class for all database models. @@ -51,8 +20,8 @@ class Base(MappedAsDataclass, DeclarativeBase): @classmethod def execute(cls, stmt: Any, commit: bool = False): - with DbEngine.manager(commit=commit) as conn: - return conn.execute(stmt) + with DbEngine.manager(commit=commit) as session: + return session.execute(stmt) @classmethod def insert_many(cls, items: list[dict[str, Any]]): diff --git a/app/db/engine.py b/app/db/engine.py index 841702de..055f5b42 100644 --- a/app/db/engine.py +++ b/app/db/engine.py @@ -1,5 +1,18 @@ from contextlib import contextmanager -from sqlalchemy import Engine +import gc +from sqlalchemy import Engine, event + + +@event.listens_for(Engine, "connect") +def set_sqlite_pragma(dbapi_connection, connection_record): + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA journal_mode=WAL") + cursor.execute("PRAGMA synchronous=NORMAL") + cursor.execute("PRAGMA cache_size=10000") + cursor.execute("PRAGMA foreign_keys=ON") + cursor.execute("PRAGMA temp_store=MEMORY") + cursor.execute("PRAGMA mmap_size=30000000000") + cursor.close() class DbEngine: @@ -11,20 +24,22 @@ class DbEngine: @classmethod @contextmanager - def manager(cls, commit: bool): + def manager(cls, commit: bool = False): """ 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. + When the context manager is entered, it returns a session 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. """ + conn = cls.engine.connect() try: - conn = cls.engine.connect() yield conn.execution_options(preserve_rowcount=True) - if commit: conn.commit() + except Exception as e: + conn.rollback() + raise e finally: conn.close() diff --git a/app/db/libdata.py b/app/db/libdata.py index acf4c510..c6db3f15 100644 --- a/app/db/libdata.py +++ b/app/db/libdata.py @@ -1,6 +1,5 @@ from app.db import ( Base as MasterBase, - DbManager, ) from app.db.utils import ( album_to_dataclass, @@ -13,7 +12,7 @@ from app.db.utils import ( from app.models import Album as AlbumModel from app.utils.remove_duplicates import remove_duplicates from app.db.engine import DbEngine -from sqlalchemy import JSON, Boolean, Integer, String, delete, select, update +from sqlalchemy import JSON, Integer, String, delete, select, update from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase @@ -33,7 +32,7 @@ def create_all(): class Base(MasterBase, DeclarativeBase): @classmethod def get_all_hashes(cls, create_date: int | None = None): - with DbManager() as conn: + with DbEngine.manager() as conn: if create_date: if cls.__tablename__ == "track": stmt = select(TrackTable.trackhash).where( @@ -67,7 +66,7 @@ class Base(MasterBase, DeclarativeBase): hash (str): The hash value. is_favorite (bool): The value of the 'is_favorite' flag. """ - with DbManager(commit=True) as conn: + with DbEngine.manager(commit=True) as conn: if cls.__tablename__ == "track": stmt = ( update(cls) @@ -129,7 +128,6 @@ class TrackTable(Base): title: Mapped[str] = mapped_column(String()) track: Mapped[int] = mapped_column(Integer()) trackhash: Mapped[str] = mapped_column(String(), index=True) - # is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean()) lastplayed: Mapped[int] = mapped_column(Integer(), default=0) playcount: Mapped[int] = mapped_column(Integer(), default=0) playduration: Mapped[int] = mapped_column(Integer(), default=0) @@ -139,13 +137,13 @@ class TrackTable(Base): @classmethod def get_all(cls): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(cls)) return tracks_to_dataclasses(result.fetchall()) @classmethod def get_tracks_by_filepaths(cls, filepaths: list[str]): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(TrackTable) .where(TrackTable.filepath.in_(filepaths)) @@ -155,7 +153,7 @@ class TrackTable(Base): @classmethod def get_tracks_by_albumhash(cls, albumhash: str): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(TrackTable).where(TrackTable.albumhash == albumhash) ) @@ -164,7 +162,7 @@ class TrackTable(Base): @classmethod def get_track_by_trackhash(cls, hash: str, filepath: str = ""): - with DbManager() as conn: + with DbEngine.manager() as conn: if filepath: result = conn.execute( select(TrackTable) @@ -186,7 +184,7 @@ class TrackTable(Base): @classmethod def get_tracks_by_artisthash(cls, artisthash: str): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(TrackTable).where(TrackTable.artists.contains(artisthash)) ) @@ -194,7 +192,7 @@ class TrackTable(Base): @classmethod def get_tracks_in_path(cls, path: str): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(TrackTable) .where(TrackTable.filepath.contains(path)) @@ -204,7 +202,7 @@ class TrackTable(Base): @classmethod def get_tracks_by_trackhashes(cls, hashes: Iterable[str], limit: int | None = None): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(TrackTable) .where(TrackTable.trackhash.in_(hashes)) @@ -221,7 +219,7 @@ class TrackTable(Base): @classmethod def get_recently_added(cls, start: int, limit: int): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(TrackTable) .order_by(TrackTable.last_mod.desc()) @@ -243,7 +241,7 @@ class TrackTable(Base): @classmethod def remove_tracks_by_filepaths(cls, filepaths: set[str]): - with DbManager(commit=True) as conn: + with DbEngine.manager(commit=True) as conn: conn.execute(delete(TrackTable).where(TrackTable.filepath.in_(filepaths))) @classmethod @@ -270,7 +268,6 @@ class AlbumTable(Base): og_title: Mapped[str] = mapped_column(String()) title: Mapped[str] = mapped_column(String()) trackcount: Mapped[int] = mapped_column(Integer()) - # is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean()) lastplayed: Mapped[int] = mapped_column(Integer(), default=0) playcount: Mapped[int] = mapped_column(Integer(), default=0) playduration: Mapped[int] = mapped_column(Integer(), default=0) @@ -280,14 +277,14 @@ class AlbumTable(Base): @classmethod def get_all(cls): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(AlbumTable)) all = result.fetchall() return albums_to_dataclasses(all) @classmethod def get_album_by_albumhash(cls, hash: str): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(AlbumTable).where(AlbumTable.albumhash == hash) ) @@ -298,7 +295,7 @@ class AlbumTable(Base): @classmethod def get_albums_by_albumhashes(cls, hashes: Iterable[str], limit: int | None = None): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(AlbumTable).where(AlbumTable.albumhash.in_(hashes)).limit(limit) ) @@ -312,7 +309,7 @@ class AlbumTable(Base): @classmethod def get_albums_by_artisthashes(cls, artisthashes: list[str]): - with DbManager() as conn: + with DbEngine.manager() as conn: albums: dict[str, list[AlbumModel]] = {} for artist in artisthashes: @@ -325,7 +322,7 @@ class AlbumTable(Base): @classmethod def get_albums_by_base_title(cls, base_title: str): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(AlbumTable).where(AlbumTable.base_title == base_title) ) @@ -333,7 +330,7 @@ class AlbumTable(Base): @classmethod def get_albums_by_artisthash(cls, artisthash: str): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(AlbumTable).where(AlbumTable.artisthashes.contains(artisthash)) ) @@ -359,7 +356,6 @@ class ArtistTable(Base): genres: Mapped[str] = mapped_column(JSON()) name: Mapped[str] = mapped_column(String(), index=True) trackcount: Mapped[int] = mapped_column(Integer()) - # is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean()) lastplayed: Mapped[int] = mapped_column(Integer(), default=0) playcount: Mapped[int] = mapped_column(Integer(), default=0) playduration: Mapped[int] = mapped_column(Integer(), default=0) @@ -369,14 +365,14 @@ class ArtistTable(Base): @classmethod def get_all(cls): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(cls)) all = result.fetchall() return artists_to_dataclasses(all) @classmethod def get_artist_by_hash(cls, artisthash: str): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(ArtistTable).where(ArtistTable.artisthash == artisthash) ) @@ -384,7 +380,7 @@ class ArtistTable(Base): @classmethod def get_artisthashes_not_in(cls, artisthashes: list[str]): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(ArtistTable.artisthash, ArtistTable.name).where( ~ArtistTable.artisthash.in_(artisthashes) @@ -396,7 +392,7 @@ class ArtistTable(Base): def get_artists_by_artisthashes( cls, hashes: Iterable[str], limit: int | None = None ): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(ArtistTable) .where(ArtistTable.artisthash.in_(hashes)) diff --git a/app/db/metadata.py b/app/db/metadata.py index 16976b45..3e5d37cf 100644 --- a/app/db/metadata.py +++ b/app/db/metadata.py @@ -1,9 +1,11 @@ -from app.db import Base, DbManager +from app.db import Base from sqlalchemy import Integer, insert, select, update from sqlalchemy.orm import Mapped, mapped_column +from app.db.engine import DbEngine + class MigrationTable(Base): __tablename__ = "dbmigration" @@ -13,7 +15,7 @@ class MigrationTable(Base): @classmethod def set_version(cls, version: int): - with DbManager(commit=True) as conn: + with DbEngine.manager(commit=True) as conn: result = conn.execute( update(cls).where(cls.id == 1).values(version=version) ) @@ -23,7 +25,7 @@ class MigrationTable(Base): @classmethod def get_version(cls): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(cls.version).where(cls.id == 1)) result = result.fetchone() diff --git a/app/db/userdata.py b/app/db/userdata.py index 7ab3d0db..57e87433 100644 --- a/app/db/userdata.py +++ b/app/db/userdata.py @@ -18,6 +18,7 @@ from sqlalchemy import ( from sqlalchemy.orm import Mapped, mapped_column +from app.db.engine import DbEngine from app.db.utils import ( albums_to_dataclasses, artists_to_dataclasses, @@ -27,14 +28,13 @@ from app.db.utils import ( plugin_to_dataclasses, similar_artist_to_dataclass, similar_artists_to_dataclass, - tracklog_to_dataclass, tracklog_to_dataclasses, tracks_to_dataclasses, user_to_dataclass, user_to_dataclasses, ) -from app.db import Base, DbManager +from app.db import Base from app.utils.auth import get_current_userid, hash_password @@ -77,7 +77,7 @@ class UserTable(Base): @classmethod def get_by_id(cls, id: int): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(cls).where(cls.id == id)) res = result.fetchone() @@ -86,7 +86,7 @@ class UserTable(Base): @classmethod def get_by_username(cls, username: str): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(cls).where(cls.username == username)) res = result.fetchone() @@ -95,7 +95,7 @@ class UserTable(Base): @classmethod def update_one(cls, user: dict[str, Any]): - with DbManager(commit=True) as conn: + with DbEngine.manager(commit=True) as conn: conn.execute(update(cls).where(cls.id == user["id"]).values(user)) @classmethod @@ -126,7 +126,7 @@ class SimilarArtistTable(Base): @classmethod def get_all(cls): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(cls)) return similar_artists_to_dataclass(result.fetchall()) @@ -136,7 +136,7 @@ class SimilarArtistTable(Base): Check whether an artisthash exists in the database. """ - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute( select(cls.artisthash).where(cls.artisthash == artisthash) ) @@ -148,7 +148,7 @@ class SimilarArtistTable(Base): Get a single artist by hash. """ - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(cls).where(cls.artisthash == artisthash)) result = result.fetchone() @@ -160,7 +160,7 @@ class FavoritesTable(Base): __tablename__ = "favorite" id: Mapped[int] = mapped_column(primary_key=True) - hash: Mapped[str] = mapped_column(String()) + hash: Mapped[str] = mapped_column(String(), unique=True) type: Mapped[str] = mapped_column(String(), index=True) timestamp: Mapped[int] = mapped_column(Integer(), index=True) userid: Mapped[int] = mapped_column( @@ -172,7 +172,7 @@ class FavoritesTable(Base): @classmethod def get_all(cls): - with DbManager() as conn: + with DbEngine.manager() as conn: result = conn.execute(select(cls)) return favorites_to_dataclass(result.fetchall()) @@ -181,12 +181,12 @@ class FavoritesTable(Base): item["timestamp"] = int(datetime.datetime.now().timestamp()) item["userid"] = get_current_userid() - with DbManager(commit=True) as conn: + with DbEngine.manager(commit=True) as conn: conn.execute(insert(cls).values(item)) @classmethod def remove_item(cls, item: dict[str, Any]): - with DbManager(commit=True) as conn: + with DbEngine.manager(commit=True) as conn: conn.execute( delete(cls).where( (cls.hash == item["hash"]) & (cls.type == item["type"]) @@ -199,12 +199,13 @@ class FavoritesTable(Base): return result.fetchone() is not None @classmethod - def get_all_of_type(cls, table: Any, field: Any, type: str, start: int, limit: int): + def get_all_of_type(cls, type: str, start: int, limit: int): result = cls.execute( - select(table) - .select_from(join(table, cls, field == cls.hash)) - .where(and_(cls.type == type, cls.userid == get_current_userid())) - .offset(start) + select(cls) + # .select_from(join(table, cls, field == cls.hash)) + .where(and_(cls.type == type, cls.userid == get_current_userid())).offset( + start + ) # INFO: If start is 0, fetch all so we can get the total count .limit(limit if start != 0 else None) ) @@ -218,30 +219,18 @@ class FavoritesTable(Base): @classmethod def get_fav_tracks(cls, start: int, limit: int): - from .libdata import TrackTable - - result, total = cls.get_all_of_type( - TrackTable, TrackTable.trackhash, "track", start, limit - ) - return tracks_to_dataclasses(result), total + result, total = cls.get_all_of_type("track", start, limit) + return favorites_to_dataclass(result), total @classmethod def get_fav_albums(cls, start: int, limit: int): - from .libdata import AlbumTable - - result, total = cls.get_all_of_type( - AlbumTable, AlbumTable.albumhash, "album", start, limit - ) - return albums_to_dataclasses(result), total + result, total = cls.get_all_of_type("album", start, limit) + return favorites_to_dataclass(result), total @classmethod def get_fav_artists(cls, start: int, limit: int): - from .libdata import ArtistTable - - result, total = cls.get_all_of_type( - ArtistTable, ArtistTable.artisthash, "artist", start, limit - ) - return artists_to_dataclasses(result), total + result, total = cls.get_all_of_type("artist", start, limit) + return favorites_to_dataclass(result), total class ScrobbleTable(Base): @@ -265,7 +254,7 @@ class ScrobbleTable(Base): return cls.insert_one(item) @classmethod - def get_all(cls, start: int, limit: int): + def get_all(cls, start: int, limit: int | None): result = cls.execute( select(cls) .where(cls.userid == get_current_userid()) @@ -325,7 +314,7 @@ class PlaylistTable(Base): ) @classmethod - def get_trackhashes(cls, id: int) -> list[str]: + def get_trackhashes(cls, id: int): result = cls.execute( select(cls.trackhashes).where( (cls.id == id) & (cls.userid == get_current_userid()) @@ -388,32 +377,24 @@ class PlaylistTable(Base): ) -# class PlaylistTrackTable(Base): -# __tablename__ = "playlisttrack" +class ArtistData(Base): + __tablename__ = "artistdata" -# id: Mapped[int] = mapped_column(primary_key=True) -# trackhash: Mapped[str] = mapped_column(String(), index=True) -# playlistid: Mapped[int] = mapped_column( -# Integer(), ForeignKey("playlist.id", ondelete="cascade") -# ) -# index: Mapped[int] = mapped_column(Integer()) -# userid: Mapped[int] = mapped_column( -# Integer(), ForeignKey("user.id", ondelete="cascade") -# ) + id: Mapped[int] = mapped_column(primary_key=True) + artisthash: Mapped[str] = mapped_column(String(), index=True) + color: Mapped[str] = mapped_column(String(), nullable=True) + bio: Mapped[str] = mapped_column(String(), nullable=True) + info: Mapped[dict[str, Any]] = mapped_column(JSON(), nullable=True) + extra: Mapped[dict[str, Any]] = mapped_column( + JSON(), nullable=True, default_factory=dict + ) -# @classmethod -# def count_by_playlist() + @classmethod + def find_one(cls, artisthash: str): + result = cls.execute(select(cls).where(cls.artisthash == artisthash)) + return result.fetchone() -# @classmethod -# def insert_many(cls, playlistid: int, trackhashes: list[str]): -# userid = get_current_userid() -# items = [ -# { -# "index": index, -# "userid": userid, -# "trackhash": trackhash, -# "playlistid": playlistid, -# } -# for index, trackhash in enumerate(trackhashes) -# ] -# return cls.execute(insert(cls).values(items), commit=True) + @classmethod + def get_all_colors(cls) -> dict[str, str]: + result = cls.execute(select(cls.artisthash, cls.color)) + return dict(result.fetchall()) diff --git a/app/lib/colorlib.py b/app/lib/colorlib.py index cb360da5..8b73dfcf 100644 --- a/app/lib/colorlib.py +++ b/app/lib/colorlib.py @@ -12,9 +12,10 @@ from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb from app.db.sqlite.artistcolors import SQLiteArtistMethods as adb from app.db.sqlite.utils import SQLiteManager -# from app.store.artists import ArtistStore +from app.db.userdata import ArtistData from app.logger import log from app.lib.errors import PopulateCancelledError +from app.store.artists import ArtistStore from app.utils.progressbar import tqdm PROCESS_ALBUM_COLORS_KEY = "" @@ -100,32 +101,29 @@ class ProcessArtistColors: """ def __init__(self, instance_key: str) -> None: - # all_artists = [a for a in ArtistStore.artists if len(a.colors) == 0] + all_artists = ArtistStore.get_flat_list() global PROCESS_ARTIST_COLORS_KEY PROCESS_ARTIST_COLORS_KEY = instance_key - with SQLiteManager() as cur: - try: - for artist in tqdm( - all_artists, desc="Processing missing artist colors" - ): - if PROCESS_ARTIST_COLORS_KEY != instance_key: - raise PopulateCancelledError( - "A newer 'ProcessArtistColors' instance is running. Stopping this one." - ) + try: + for artist in tqdm(all_artists, desc="Processing missing artist colors"): + if PROCESS_ARTIST_COLORS_KEY != instance_key: + raise PopulateCancelledError( + "A newer 'ProcessArtistColors' instance is running. Stopping this one." + ) - exists = adb.exists(artist.artisthash, cur=cur) + # exists = adb.exists(artist.artisthash, cur=cur) + artist = ArtistData.find_one(artist.artisthash) + if artist and artist.color is not None: + continue - if exists: - continue + colors = process_color(artist.artisthash, is_album=False) - colors = process_color(artist.artisthash, is_album=False) + if colors is None: + continue - if colors is None: - continue - - artist.set_colors(colors) - adb.insert_one_artist(cur, artist.artisthash, colors) - finally: - cur.close() + artist.set_colors(colors) + adb.insert_one_artist(cur, artist.artisthash, colors) + finally: + cur.close() diff --git a/app/lib/index.py b/app/lib/index.py new file mode 100644 index 00000000..4749ba7e --- /dev/null +++ b/app/lib/index.py @@ -0,0 +1,25 @@ +from app.lib.mapstuff import map_favorites, map_scrobble_data +from app.lib.populate import CordinateMedia +from app.lib.tagger import IndexTracks +from app.store.folder import FolderStore + + +import gc +from time import time + +from app.utils.threading import background + + +class IndexEverything: + def __init__(self) -> None: + IndexTracks(instance_key=time()) + FolderStore.load_filepaths() + map_scrobble_data() + map_favorites() + # CordinateMedia(instance_key=str(time())) + gc.collect() + + +@background +def index_everything(): + return IndexEverything() diff --git a/app/lib/mapstuff.py b/app/lib/mapstuff.py new file mode 100644 index 00000000..658e3b8a --- /dev/null +++ b/app/lib/mapstuff.py @@ -0,0 +1,68 @@ +from app.db.userdata import FavoritesTable, ScrobbleTable +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore +from app.store.tracks import TrackStore + + +from typing import Any + + +def map_scrobble_data(): + """ + Maps scrobble data to the in-memory stores. + + The scrobble data is loaded from the database and grouped by trackhash. + The album and artist scrobble data (for those tracks) are then incremented based on the data. + """ + records = ScrobbleTable.get_all(0, None) + + # group records by trackhash + grouped: dict[str, dict[str, Any]] = {} + + for record in records: + # aggregate playcount, playduration and lastplayed + item = grouped.setdefault(record.trackhash, {}) + item["playcount"] = item.get("playcount", 0) + 1 + item["playduration"] = item.get("playduration", 0) + record.duration + item["lastplayed"] = max(item.get("lastplayed", 0), record.timestamp) + + # increment playcount, playduration and lastplayed for albums and artists + for trackhash, data in grouped.items(): + track = TrackStore.trackhashmap.get(trackhash) + + if track is None: + continue + + track.increment_playcount(data["playduration"], data["lastplayed"]) + + album = AlbumStore.albummap.get(track.tracks[0].albumhash) + if album: + album.increment_playcount(data["playduration"], data["lastplayed"]) + + for artisthash in track.tracks[0].artisthashes: + artist = ArtistStore.artistmap.get(artisthash) + if artist: + artist.increment_playcount(data["playduration"], data["lastplayed"]) + + +def map_favorites(): + """ + Maps favorites data to the in-memory stores. + """ + favorites = FavoritesTable.get_all() + + for entry in favorites: + if entry.type == "album": + album = AlbumStore.albummap.get(entry.hash) + if album: + album.toggle_favorite_user(entry.userid) + + elif entry.type == "artist": + artist = ArtistStore.artistmap.get(entry.hash) + if artist: + artist.toggle_favorite_user(entry.userid) + + elif entry.type == "track": + track = TrackStore.trackhashmap.get(entry.hash) + if track: + track.toggle_favorite_user(entry.userid) diff --git a/app/lib/playlistlib.py b/app/lib/playlistlib.py index 5d8f7292..ea62af18 100644 --- a/app/lib/playlistlib.py +++ b/app/lib/playlistlib.py @@ -10,8 +10,9 @@ from typing import Any from PIL import Image, ImageSequence from app import settings -from app.db.libdata import AlbumTable, TrackTable +from app.db.libdata import TrackTable from app.models.track import Track +from app.store.albums import AlbumStore def create_thumbnail(image: Any, img_path: str) -> str: """ @@ -115,8 +116,7 @@ def get_first_4_images( if len(albums) == 4: break - # albums = AlbumStore.get_albums_by_hashes(albums) - albums = AlbumTable.get_albums_by_albumhashes(albums) + albums = AlbumStore.get_albums_by_hashes(albums) images = [ { "image": album.image, diff --git a/app/lib/populate.py b/app/lib/populate.py index ea1e1afe..5506741e 100644 --- a/app/lib/populate.py +++ b/app/lib/populate.py @@ -1,28 +1,22 @@ from dataclasses import asdict import os -from collections import deque from concurrent.futures import ThreadPoolExecutor -from typing import Generator from requests import ConnectionError as RequestConnectionError from requests import ReadTimeout from app import settings -from app.db.libdata import ArtistTable -from app.db.libdata import AlbumTable, TrackTable -from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb - -# from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as lastfmdb from app.db.sqlite.tracks import SQLiteTrackMethods from app.lib.artistlib import CheckArtistImages from app.lib.colorlib import ProcessArtistColors from app.lib.errors import PopulateCancelledError from app.lib.taglib import extract_thumb from app.logger import log -from app.models import Album, Artist, Track +from app.models import Album, Artist from app.models.lastfm import SimilarArtist from app.requests.artists import fetch_similar_artists -from app.utils.filesystem import run_fast_scandir +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore from app.utils.network import has_connection from app.utils.progressbar import tqdm @@ -35,46 +29,6 @@ remove_tracks_by_filepaths = SQLiteTrackMethods.remove_tracks_by_filepaths POPULATE_KEY = "" -class Populate: - """ - Populates the database with all songs in the music directory - - checks if the song is in the database, if not, it adds it - also checks if the album art exists in the image path, if not tries to extract it. - """ - - # def __init__(self, instance_key: str) -> None: - # return - - # if len(dirs_to_scan) == 0: - # log.warning( - # ( - # "The root directory is not configured. " - # + "Open the app in your webbrowser to configure." - # ) - # ) - # return - - # try: - # if dirs_to_scan[0] == "$home": - # dirs_to_scan = [settings.Paths.USER_HOME_DIR] - # except IndexError: - # pass - - # files = set() - - # for _dir in dirs_to_scan: - # files = files.union(run_fast_scandir(_dir, full=True)[1]) - - # unmodified, modified_tracks = self.remove_modified(tracks) - # untagged = files - unmodified - - # if len(untagged) != 0: - # self.tag_untagged(untagged, instance_key) - - # self.extract_thumb_with_overwrite(modified_tracks) - - class CordinateMedia: """ Cordinates the extracting of thumbnails @@ -117,102 +71,6 @@ class CordinateMedia: log.warn(e) return - # @staticmethod - # def remove_modified(tracks: Generator[TrackTable, None, None]): - # """ - # Removes tracks from the database that have been modified - # since they were added to the database. - # """ - - # unmodified_paths = set() - # modified_tracks: list[TrackTable] = [] - # modified_paths = set() - - # for track in tracks: - # try: - # if track.last_mod == round(os.path.getmtime(track.filepath)): - # unmodified_paths.add(track.filepath) - # continue - # except (FileNotFoundError, OSError) as e: - # log.warning(e) # REVIEW More informations = good - # TrackStore.remove_track_obj(track) - # remove_tracks_by_filepaths(track.filepath) - - # modified_paths.add(track.filepath) - # modified_tracks.append(track) - - # TrackStore.remove_tracks_by_filepaths(modified_paths) - # remove_tracks_by_filepaths(modified_paths) - - # return unmodified_paths, modified_tracks - - # @staticmethod - # def tag_untagged(untagged: set[str], key: str): - # pass - # for file in tqdm(untagged, desc="Reading files"): - # if POPULATE_KEY != key: - # log.warning("'Populate.tag_untagged': Populate key changed") - # return - - # tags = get_tags(file) - - # if tags is not None: - # TrackTable.insert_one(tags) - - # ============================================= - - # log.info("Found %s new tracks", len(untagged)) - # # tagged_tracks: deque[dict] = deque() - # # tagged_count = 0 - - # favs = favdb.get_fav_tracks() - # records = dict() - - # for fav in favs: - # r = records.setdefault(fav[1], set()) - # r.add(fav[4]) - - # tagged_tracks.append(tags) - # track = Track(**tags) - - # track.fav_userids = list(records.get(track.trackhash, set())) - - # TrackStore.add_track(track) - - # if not AlbumStore.album_exists(track.albumhash): - # AlbumStore.add_album(AlbumStore.create_album(track)) - - # for artist in track.artists: - # if not ArtistStore.artist_exists(artist.artisthash): - # ArtistStore.add_artist(Artist(artist.name)) - - # for artist in track.albumartists: - # if not ArtistStore.artist_exists(artist.artisthash): - # ArtistStore.add_artist(Artist(artist.name)) - - # tagged_count += 1 - # else: - # log.warning("Could not read file: %s", file) - - # if len(tagged_tracks) > 0: - # log.info("Adding %s tracks to database", len(tagged_tracks)) - # insert_many_tracks(tagged_tracks) - - # log.info("Added %s/%s tracks", tagged_count, len(untagged)) - - # @staticmethod - # def extract_thumb_with_overwrite(tracks: list[TrackTable]): - # """ - # Extracts the thumbnail from a list of filepaths, - # overwriting the existing thumbnail if it exists, - # for modified files. - # """ - # for track in tracks: - # try: - # extract_thumb(track.filepath, track.image, overwrite=True) - # except FileNotFoundError: - # continue - def get_image(_map: tuple[str, Album]): """ @@ -228,25 +86,11 @@ def get_image(_map: tuple[str, Album]): if POPULATE_KEY != instance_key: raise PopulateCancelledError("'ProcessTrackThumbnails': Populate key changed") - matching_tracks = filter( - lambda t: t.albumhash == album.albumhash, - TrackTable.get_tracks_by_albumhash(album.albumhash), - ) + matching_tracks = AlbumStore.get_album_tracks(album.albumhash) - try: - track = next(matching_tracks) - extracted = extract_thumb(track.filepath, track.image) - - while not extracted: - try: - track = next(matching_tracks) - extracted = extract_thumb(track.filepath, track.image) - except StopIteration: - break - - return - except StopIteration: - pass + for track in matching_tracks: + if extract_thumb(track.filepath, track.image): + break def get_cpu_count(): @@ -274,7 +118,7 @@ class ProcessTrackThumbnails: # filter out albums that already have thumbnails albums = filter( - lambda album: album.albumhash not in processed, AlbumTable.get_all() + lambda album: album.albumhash not in processed, AlbumStore.get_flat_list() ) albums = list(albums) @@ -330,7 +174,9 @@ class FetchSimilarArtistsLastFM: processed = ".".join(a.artisthash for a in processed) # filter out artists that already have similar artists - artists = filter(lambda a: a.artisthash not in processed, ArtistTable.get_all()) + artists = filter( + lambda a: a.artisthash not in processed, ArtistStore.get_flat_list() + ) artists = list(artists) # process the rest diff --git a/app/lib/searchlib.py b/app/lib/searchlib.py index 3d6cb1cb..d2bf65b9 100644 --- a/app/lib/searchlib.py +++ b/app/lib/searchlib.py @@ -9,7 +9,8 @@ from unidecode import unidecode from app import models from app.config import UserConfig -from app.db.libdata import AlbumTable, ArtistTable, TrackTable + +# from app.db.libdata import AlbumTable, ArtistTable, TrackTable # from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.models.enums import FavType @@ -19,9 +20,9 @@ from app.serializers.album import serialize_for_card_many as serialize_albums from app.serializers.artist import serialize_for_cards from app.serializers.track import serialize_track, serialize_tracks -# from app.store.albums import AlbumStore -# from app.store.artists import ArtistStore -# from app.store.tracks import TrackStore +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore +from app.store.tracks import TrackStore from app.utils.remove_duplicates import remove_duplicates @@ -54,8 +55,7 @@ class Limit: class SearchTracks: def __init__(self, query: str) -> None: self.query = query - # self.tracks = TrackStore.tracks - self.tracks = TrackTable.get_all() + self.tracks = TrackStore.get_flat_list() def __call__(self) -> List[models.Track]: """ @@ -78,8 +78,7 @@ class SearchTracks: class SearchArtists: def __init__(self, query: str) -> None: self.query = query - # self.artists = ArtistStore.artists - self.artists = ArtistTable.get_all() + self.artists = ArtistStore.get_flat_list() def __call__(self): """ @@ -101,8 +100,7 @@ class SearchArtists: class SearchAlbums: def __init__(self, query: str) -> None: self.query = query - # self.albums = AlbumStore.albums - self.albums = AlbumTable.get_all() + self.albums = AlbumStore.get_flat_list() def __call__(self) -> List[models.Album]: """ @@ -169,9 +167,9 @@ class TopResults: def collect_all(): all_items: list[_type] = [] - all_items.extend(ArtistTable.get_all()) - all_items.extend(TrackTable.get_all()) - all_items.extend(AlbumTable.get_all()) + all_items.extend(ArtistStore.get_flat_list()) + all_items.extend(TrackStore.get_flat_list()) + all_items.extend(TrackStore.get_flat_list()) return all_items, get_titles(all_items) @@ -194,7 +192,7 @@ class TopResults: return {"type": "track", "item": item} if isinstance(item, models.Album): - tracks = TrackTable.get_tracks_by_albumhash(item.albumhash) + tracks = TrackStore.get_tracks_by_albumhash(item.albumhash) tracks = remove_duplicates(tracks) try: @@ -212,19 +210,13 @@ class TopResults: track_count = 0 duration = 0 - tracks = TrackTable.get_tracks_by_artisthash(item.artisthash) + tracks = TrackStore.get_tracks_by_artisthash(item.artisthash) tracks = remove_duplicates(tracks) for track in tracks: track_count += 1 duration += track.duration - # album_count = AlbumStore.count_albums_by_artisthash(item.artisthash) - - # item.set_trackcount(track_count) - # item.set_albumcount(album_count) - # item.set_duration(duration) - return {"type": "artist", "item": item} @staticmethod @@ -235,8 +227,7 @@ class TopResults: tracks.extend(SearchTracks(query)()) if item["type"] == "album": - t = TrackTable.get_tracks_by_albumhash(item["item"].albumhash) - # t = TrackStore.get_tracks_by_albumhash(item["item"].albumhash) + t = TrackStore.get_tracks_by_albumhash(item["item"].albumhash) t.sort(key=lambda x: x.last_mod) # if there are less than the limit, get more tracks @@ -249,7 +240,7 @@ class TopResults: if item["type"] == "artist": # t = TrackStore.get_tracks_by_artisthash(item["item"].artisthash) - t = TrackTable.get_tracks_by_artisthash(item["item"].artisthash) + t = TrackStore.get_tracks_by_artisthash(item["item"].artisthash) # if there are less than the limit, get more tracks if len(t) < limit: @@ -271,7 +262,7 @@ class TopResults: if item["type"] == "artist": # albums = AlbumStore.get_albums_by_artisthash(item["item"].artisthash) - albums = AlbumTable.get_albums_by_artisthash(item["item"].artisthash) + albums = AlbumStore.get_albums_by_artisthash(item["item"].artisthash) # if there are less than the limit, get more albums if len(albums) < limit: diff --git a/app/lib/tagger.py b/app/lib/tagger.py index 0564f94a..b7613125 100644 --- a/app/lib/tagger.py +++ b/app/lib/tagger.py @@ -1,12 +1,9 @@ -import gc import os -from pprint import pprint -from time import time from app import settings from app.config import UserConfig -from app.db.libdata import ArtistTable from app.db.libdata import TrackTable -from app.lib.populate import CordinateMedia + +# from app.lib.populate import CordinateMedia from app.lib.taglib import extract_thumb, get_tags from app.models.album import Album from app.models.artist import Artist @@ -17,7 +14,6 @@ from app.utils.parsers import get_base_album_title from app.utils.progressbar import tqdm from app.logger import log -from app.utils.threading import background POPULATE_KEY: float = 0 @@ -142,7 +138,6 @@ class IndexTracks: print("Done") -# class IndexAlbums: def create_albums(): albums = dict() all_tracks: list[Track] = TrackTable.get_all() @@ -165,7 +160,7 @@ def create_albums(): "playduration": track.playduration, "title": track.album, "trackcount": 1, - "extra": {} + "extra": {}, } else: album = albums[track.albumhash] @@ -187,13 +182,11 @@ def create_albums(): genres.append(genre) album["genres"] = genres - album["genrehashes"] = " ".join([g['genrehash'] for g in genres]) + album["genrehashes"] = " ".join([g["genrehash"] for g in genres]) album["base_title"], _ = get_base_album_title(album["og_title"]) del genres - # AlbumTable.remove_all() - # AlbumTable.insert_many(list(albums.values())) return [Album(**album) for album in albums.values()] @@ -227,9 +220,7 @@ def create_artists(): "playduration": track.playduration, "trackcount": None, "tracks": ( - {track.trackhash} - if thisartist.get("in_track", True) - else set() + {track.trackhash} if thisartist.get("in_track", True) else set() ), "extra": {}, } @@ -261,7 +252,7 @@ def create_artists(): genres.append(genre) artist["genres"] = genres - artist["genrehashes"] = " ".join([g['genrehash'] for g in genres]) + artist["genrehashes"] = " ".join([g["genrehash"] for g in genres]) artist["name"] = sorted(artist["names"])[0] # INFO: Delete temporary keys @@ -272,25 +263,6 @@ def create_artists(): # INFO: Delete local variables del genres - # ArtistTable.remove_all() - # ArtistTable.insert_many(list(artists.values())) - # del artists return [Artist(**artist) for artist in artists.values()] -class IndexEverything: - def __init__(self) -> None: - IndexTracks(instance_key=time()) - # IndexAlbums() - # IndexArtists() - FolderStore.load_filepaths() - - # pass - - # CordinateMedia(instance_key=str(time())) - gc.collect() - - -@background -def index_everything(): - return IndexEverything() diff --git a/app/models/album.py b/app/models/album.py index 5b247e8a..0dc4322f 100644 --- a/app/models/album.py +++ b/app/models/album.py @@ -1,11 +1,10 @@ import dataclasses -import datetime from dataclasses import dataclass -from ..utils.hashing import create_hash -from ..utils.parsers import get_base_title_and_versions, parse_feat_from_title -from .artist import Artist from .track import Track +from ..utils.hashing import create_hash +from app.utils.auth import get_current_userid +from ..utils.parsers import get_base_title_and_versions @dataclass(slots=True) @@ -27,7 +26,6 @@ class Album: og_title: str title: str trackcount: int - # is_favorite: bool lastplayed: int playcount: int playduration: int @@ -37,6 +35,21 @@ class Album: type: str = "album" image: str = "" versions: list[str] = dataclasses.field(default_factory=list) + fav_userids: list[int] = dataclasses.field(default_factory=list) + + @property + def is_favorite(self): + return get_current_userid() in self.fav_userids + + def toggle_favorite_user(self, userid: int): + """ + Adds or removes the given user from the list of users + who have favorited the album. + """ + if userid in self.fav_userids: + self.fav_userids.remove(userid) + else: + self.fav_userids.append(userid) def __post_init__(self): self.image = self.albumhash + ".webp" diff --git a/app/models/artist.py b/app/models/artist.py index f7813f56..540098a7 100644 --- a/app/models/artist.py +++ b/app/models/artist.py @@ -1,6 +1,7 @@ import dataclasses from dataclasses import dataclass +from app.utils.auth import get_current_userid from app.utils.hashing import create_hash @@ -46,7 +47,6 @@ class Artist: genrehashes: list[str] name: str trackcount: int - # is_favorite: bool lastplayed: int playcount: int playduration: int @@ -55,5 +55,21 @@ class Artist: id: int = -1 image: str = "" + fav_userids: list[int] = dataclasses.field(default_factory=list) + + @property + def is_favorite(self): + return get_current_userid() in self.fav_userids + + def toggle_favorite_user(self, userid: int): + """ + Adds or removes the given user from the list of users + who have favorited this artist. + """ + if userid in self.fav_userids: + self.fav_userids.remove(userid) + else: + self.fav_userids.append(userid) + def __post_init__(self): - self.image = self.artisthash + ".webp" \ No newline at end of file + self.image = self.artisthash + ".webp" diff --git a/app/models/track.py b/app/models/track.py index fa6984e5..37436105 100644 --- a/app/models/track.py +++ b/app/models/track.py @@ -38,12 +38,22 @@ class Track: _pos: int = 0 _ati: str = "" image: str = "" - fav_userids: set = field(default_factory=set) + fav_userids: list[int] = field(default_factory=list) @property def is_favorite(self): return get_current_userid() in self.fav_userids + def toggle_favorite_user(self, userid: int): + """ + Adds or removes the given user from the list of users + who have favorited the track. + """ + if userid in self.fav_userids: + self.fav_userids.remove(userid) + else: + self.fav_userids.append(userid) + def __post_init__(self): self.image = self.albumhash + ".webp" self.extra = { @@ -51,159 +61,3 @@ class Track: "track_total": self.extra.get("track_total", 0), "samplerate": self.extra.get("samplerate", -1), } - - # album: str - # albumartists: str | list[ArtistMinimal] - # albumhash: str - # artists: str | list[ArtistMinimal] - # bitrate: int - # copyright: str - # date: int - # disc: int - # duration: int - # filepath: str - # folder: str - # genre: str | list[str] - # title: str - # track: int - # trackhash: str - # last_mod: str | int - - # image: str = "" - # artist_hashes: str = "" - - # """ - # A string of user ids separated by commas. - # """ - # # is_favorite: bool = False - - # # temporary attributes - # _pos: int = 0 # for sorting tracks by disc and track number - # _ati: str = ( - # "" # (album track identifier) for removing duplicates when merging album versions - # ) - - # og_title: str = "" - # og_album: str = "" - # created_date: float = 0.0 - - # def set_created_date(self): - # try: - # self.created_date = Path(self.filepath).stat().st_ctime - # except FileNotFoundError: - # pass - - # def __post_init__(self): - # self.og_title = self.title - # self.og_album = self.album - # self.last_mod = int(self.last_mod) - # self.date = int(self.date) - - # # add a trailing slash to the folder path - # # to avoid matching a folder starting with the same name as the root path - # # eg. .../Music and .../Music Videos - # self.folder = os.path.join(self.folder, "") - - # if self.artists is not None: - # artists = split_artists(self.artists) - # new_title = self.title - - # if get_flag(SessionVarKeys.EXTRACT_FEAT): - # featured, new_title = parse_feat_from_title(self.title) - # original_lower = "-".join([create_hash(a) for a in artists]) - # artists.extend( - # a for a in featured if create_hash(a) not in original_lower - # ) - - # self.artist_hashes = "-".join(create_hash(a, decode=True) for a in artists) - # self.artists = [ArtistMinimal(a) for a in artists] - - # albumartists = split_artists(self.albumartists) - - # if not albumartists: - # self.albumartists = self.artists[:1] - # else: - # self.albumartists = [ArtistMinimal(a) for a in albumartists] - - # if get_flag(SessionVarKeys.REMOVE_PROD): - # new_title = remove_prod(new_title) - - # if track is a single - # if self.og_title == self.album: - # self.rename_album(new_title) - - # if get_flag(SessionVarKeys.REMOVE_REMASTER_FROM_TRACK): - # new_title = clean_title(new_title) - - # self.title = new_title - - # if get_flag(SessionVarKeys.CLEAN_ALBUM_TITLE): - # self.album, _ = get_base_title_and_versions( - # self.album, get_versions=False - # ) - - # if get_flag(SessionVarKeys.MERGE_ALBUM_VERSIONS): - # self.recreate_albumhash() - - # self.image = self.albumhash + ".webp" - - # if self.genre is not None and self.genre != "": - # self.genre = self.genre.lower() - # separators = {"/", ";", "&"} - - # contains_rnb = "r&b" in self.genre - # contains_rock = "rock & roll" in self.genre - - # if contains_rnb: - # self.genre = self.genre.replace("r&b", "RnB") - - # if contains_rock: - # self.genre = self.genre.replace("rock & roll", "rock") - - # for s in separators: - # self.genre: str = self.genre.replace(s, ",") - - # self.genre = self.genre.split(",") - # self.genre = [g.strip() for g in self.genre] - - # self.recreate_hash() - # self.set_created_date() - - # def recreate_hash(self): - # """ - # Recreates a track hash if the track title was altered - # to prevent duplicate tracks having different hashes. - # """ - # if self.og_title == self.title and self.og_album == self.album: - # return - - # self.trackhash = create_hash( - # ", ".join(a.name for a in self.artists), self.og_album, self.title - # ) - - # def recreate_artists_hash(self): - # """ - # Recreates a track's artist hashes if the artist list was altered - # """ - # self.artist_hashes = "-".join(a.artisthash for a in self.artists) - - # def recreate_albumhash(self): - # """ - # Recreates an albumhash of a track to merge all versions of an album. - # """ - # albumartists = (a.name for a in self.albumartists) - # self.albumhash = create_hash(self.album, *albumartists) - - # def rename_album(self, new_album: str): - # """ - # Renames an album - # """ - # self.album = new_album - - # def add_artists(self, artists: list[str], new_album_title: str): - # for artist in artists: - # if create_hash(artist, decode=True) not in self.artist_hashes: - # self.artists.append(ArtistMinimal(artist)) - - # self.recreate_artists_hash() - # self.rename_album(new_album_title) diff --git a/app/periodic_scan.py b/app/periodic_scan.py index f8b19133..931e8779 100644 --- a/app/periodic_scan.py +++ b/app/periodic_scan.py @@ -5,32 +5,31 @@ This module contains functions for the server import time from app.config import UserConfig -from app.lib.populate import Populate, PopulateCancelledError +from app.lib.populate import PopulateCancelledError from app.utils.generators import get_random_str from app.utils.threading import background from app.logger import log -@background -def run_periodic_scans(): - """ - Runs periodic scans. +# @background +# def run_periodic_scans(): +# """ +# Runs periodic scans. - Periodic scans are checks that run every few minutes - in the background to do stuff like: - - checking for new music - - delete deleted entries - - downloading artist images, and other data. - """ - # ValidateAlbumThumbs() - # ValidatePlaylistThumbs() +# Periodic scans are checks that run every few minutes +# in the background to do stuff like: +# - checking for new music +# - delete deleted entries +# - downloading artist images, and other data. +# """ +# # ValidateAlbumThumbs() +# # ValidatePlaylistThumbs() - while UserConfig().enablePeriodicScans: +# while UserConfig().enablePeriodicScans: - try: - Populate(instance_key=get_random_str()) - except PopulateCancelledError: - log.error("'run_periodic_scans': Periodic scan cancelled.") - pass +# try: +# except PopulateCancelledError: +# log.error("'run_periodic_scans': Periodic scan cancelled.") +# pass - time.sleep(UserConfig().scanInterval) +# time.sleep(UserConfig().scanInterval) diff --git a/app/store/albums.py b/app/store/albums.py index b7e97d16..14cc0086 100644 --- a/app/store/albums.py +++ b/app/store/albums.py @@ -9,6 +9,7 @@ from app.lib.tagger import create_albums from app.models import Album, Track from app.store.artists import ArtistStore from app.utils import flatten +from app.utils.auth import get_current_userid from app.utils.customlist import CustomList from app.utils.remove_duplicates import remove_duplicates @@ -28,6 +29,17 @@ class AlbumMapEntry: def basetitle(self): return self.album.base_title + def increment_playcount(self, duration: int, timestamp: int): + self.album.lastplayed = timestamp + self.album.playduration += duration + self.album.playcount += 1 + + def toggle_favorite_user(self, userid: int | None = None): + if userid is None: + userid = get_current_userid() + + self.album.toggle_favorite_user(userid) + class AlbumStore: albums: list[Album] = CustomList() diff --git a/app/store/artists.py b/app/store/artists.py index 5ae6c725..b4d71ae2 100644 --- a/app/store/artists.py +++ b/app/store/artists.py @@ -5,6 +5,7 @@ from app.db.sqlite.artistcolors import SQLiteArtistMethods as ardb from app.lib.tagger import create_artists from app.models import Artist from app.utils import flatten +from app.utils.auth import get_current_userid from app.utils.bisection import use_bisection from app.utils.customlist import CustomList from app.utils.progressbar import tqdm @@ -22,6 +23,17 @@ class ArtistMapEntry: self.albumhashes: set[str] = set() self.trackhashes: set[str] = set() + def increment_playcount(self, duration: int, timestamp: int): + self.artist.lastplayed = timestamp + self.artist.playduration += duration + self.artist.playcount += 1 + + def toggle_favorite_user(self, userid: int | None = None): + if userid is None: + userid = get_current_userid() + + self.artist.toggle_favorite_user(userid) + class ArtistStore: artists: list[Artist] = CustomList() diff --git a/app/store/tracks.py b/app/store/tracks.py index a5905e15..21e56759 100644 --- a/app/store/tracks.py +++ b/app/store/tracks.py @@ -2,13 +2,10 @@ import itertools from typing import Callable, Iterable -from flask_jwt_extended import current_user from app.db.libdata import TrackTable -from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb -# from app.db.sqlite.tracks import SQLiteTrackMethods as trackdb -from app.db.userdata import FavoritesTable from app.models import Track +from app.utils.auth import get_current_userid from app.utils.remove_duplicates import remove_duplicates TRACKS_LOAD_KEY = "" @@ -34,12 +31,24 @@ class TrackGroup: """ self.tracks.remove(track) - def set_fav_userids(self, userids: set[int]): + def increment_playcount(self, duration: int, timestamp: int): """ - Sets the favorite userids. + Increments the playcount of all tracks in the group. """ for track in self.tracks: - track.fav_userids = userids + track.playcount += 1 + track.lastplayed = timestamp + track.playduration += duration + + def toggle_favorite_user(self, userid: int | None = None): + """ + Adds or removes a user from the list of users who have favorited the track. + """ + if userid is None: + userid = get_current_userid() + + for track in self.tracks: + track.toggle_favorite_user(userid) def get_best(self): """ @@ -47,21 +56,6 @@ class TrackGroup: """ return max(self.tracks, key=lambda x: x.bitrate) - def toggle_favorite(self, remove: bool = False): - """ - Adds a track to the favorites. - """ - - userids = set(self.tracks[0].fav_userids) - - if remove: - userids.remove(current_user["id"]) - else: - userids.add(current_user["id"]) - - for track in self.tracks: - track.fav_userids = userids - def __len__(self): return len(self.tracks) @@ -72,7 +66,8 @@ class classproperty(property): """ def __get__(self, owner_self, owner_cls): - return self.fget(owner_cls) + if self.fget: + return self.fget(owner_cls) class TrackStore: @@ -118,35 +113,6 @@ class TrackStore: else: cls.trackhashmap[track.trackhash].append(track) - # favs = favdb.get_fav_tracks() - favs = FavoritesTable.get_all() - records: dict[str, set[int]] = dict() - - # convert records: {trackhash: {userid, userid, ...}} - for fav in favs: - if fav.hash not in records: - # if trackhash not in dict, add it - # and set the value to a set containing the userid - records[fav.hash] = {fav.userid} - - # if trackhash is in dict, add the userid to the set - records[fav.hash].add(fav.userid) - - for record in records: - if instance_key != TRACKS_LOAD_KEY: - return - - group = cls.trackhashmap.get(record, None) - - if not group: - continue - - group.set_fav_userids(records.get(record, set())) - - # print("Done!") - # print(cls.trackhashmap.get("0d6b22c19c").tracks[0].fav_userids) - # sys.exit(0) - @classmethod def add_track(cls, track: Track): """ @@ -219,24 +185,6 @@ class TrackStore: """ return len(cls.trackhashmap.get(trackhash, [])) - @classmethod - def toggle_favorite(cls, trackhash: str, remove: bool = False): - """ - Adds a track to the favorites. - """ - - group = cls.trackhashmap.get(trackhash) - - if group: - group.toggle_favorite(remove=remove) - - @classmethod - def remove_track_from_fav(cls, trackhash: str): - """ - Removes a track from the favorites. - """ - return cls.toggle_favorite(trackhash, remove=True) - # ================================================ # ================== GETTERS ===================== # ================================================ @@ -332,7 +280,7 @@ class TrackStore: """ predicate = lambda artisthashes, artisthash: artisthash in artisthashes return cls.find_tracks_by( - key="artist_hashes", value=artisthash, predicate=predicate + key="artisthashes", value=artisthash, predicate=predicate ) @classmethod diff --git a/manage.py b/manage.py index aaa7e25d..f2f11639 100644 --- a/manage.py +++ b/manage.py @@ -21,7 +21,8 @@ import setproctitle from app.api import create_api from app.arg_handler import ProcessArgs -from app.lib.tagger import IndexEverything +from app.lib.mapstuff import map_favorites, map_scrobble_data +from app.lib.index import IndexEverything from app.lib.watchdogg import Watcher as WatchDog from app.plugins.register import register_plugins from app.settings import FLASKVARS, TCOLOR, Info @@ -65,6 +66,8 @@ def bg_run_setup(): pass # run_periodic_scans() IndexEverything() + # map_scrobble_data() + # map_favorites() # @background