port search to stores

+ fix favorites
This commit is contained in:
cwilvx
2024-07-27 21:44:33 +03:00
parent 5d32536758
commit b0e904c84f
25 changed files with 428 additions and 666 deletions
+4
View File
@@ -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": {
+35 -13
View File
@@ -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:
+22 -7
View File
@@ -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
+8 -8
View File
@@ -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,
+3 -2
View File
@@ -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")
+3 -34
View File
@@ -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]]):
+20 -5
View File
@@ -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()
+22 -26
View File
@@ -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))
+5 -3
View File
@@ -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()
+44 -63
View File
@@ -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())
+20 -22
View File
@@ -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()
+25
View File
@@ -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()
+68
View File
@@ -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)
+3 -3
View File
@@ -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,
+11 -165
View File
@@ -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
+16 -25
View File
@@ -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:
+6 -34
View File
@@ -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()
+18 -5
View File
@@ -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"
+18 -2
View File
@@ -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"
self.image = self.artisthash + ".webp"
+11 -157
View File
@@ -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)
+19 -20
View File
@@ -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)
+12
View File
@@ -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()
+12
View File
@@ -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()
+19 -71
View File
@@ -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
+4 -1
View File
@@ -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