mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-04 20:43:04 +00:00
combine userdata and swing db into one
+ port populate to new db interface
+ add genrehashes and hash info to tracks
+ properly structure new db table files
+ move helpers to dedicated utils file
+ move settings from db to config file
+ move artists, albums, auth and favorites endpoint to new db interface
+ use folder store to index filepaths
+ paginate favorite pages
+ 56 moretiny changes 😅
This commit is contained in:
+32
-349
@@ -1,36 +1,21 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from memory_profiler import profile
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
Integer,
|
||||
Row,
|
||||
String,
|
||||
Tuple,
|
||||
and_,
|
||||
create_engine,
|
||||
delete,
|
||||
insert,
|
||||
select,
|
||||
)
|
||||
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
DeclarativeBase,
|
||||
MappedAsDataclass,
|
||||
sessionmaker,
|
||||
)
|
||||
|
||||
from app.models import Track as TrackModel
|
||||
from app.models import Album as AlbumModel
|
||||
from app.models import Artist as ArtistModel
|
||||
from app.utils.remove_duplicates import remove_duplicates
|
||||
|
||||
# ============================================================
|
||||
# TODO: Make sure the database is created before we run this.
|
||||
fullpath = "/home/cwilvx/temp/swingmusic/swing.db"
|
||||
engine = create_engine(
|
||||
f"sqlite+pysqlite:///{fullpath}",
|
||||
@@ -39,85 +24,46 @@ engine = create_engine(
|
||||
pool_size=5,
|
||||
)
|
||||
|
||||
if not os.path.exists(fullpath):
|
||||
os.makedirs(Path(fullpath).parent)
|
||||
|
||||
connection = engine.connect()
|
||||
all_filepaths = list()
|
||||
# connection = engine.connect()
|
||||
|
||||
|
||||
def getIndexOfFirstMatch(strings: list[str], prefix: str):
|
||||
"""
|
||||
Find the index of the first path that starts with the given path.
|
||||
|
||||
Uses a binary search algorithm to find the index.
|
||||
"""
|
||||
|
||||
left = 0
|
||||
right = len(strings) - 1
|
||||
|
||||
while left <= right:
|
||||
mid = (left + right) // 2
|
||||
|
||||
if strings[mid].startswith(prefix):
|
||||
if mid == 0 or not strings[mid - 1].startswith(prefix):
|
||||
return mid
|
||||
right = mid - 1
|
||||
elif strings[mid] < prefix:
|
||||
left = mid + 1
|
||||
else:
|
||||
right = mid - 1
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
def countFilepathsInDir(dirpath: str):
|
||||
"""
|
||||
Return all the filepaths in a directory.
|
||||
"""
|
||||
global all_filepaths
|
||||
index = getIndexOfFirstMatch(all_filepaths, dirpath)
|
||||
|
||||
if index == -1:
|
||||
return 0
|
||||
|
||||
paths: list[str] = []
|
||||
|
||||
for path in all_filepaths[index:]:
|
||||
if path.startswith(dirpath):
|
||||
paths.append(path)
|
||||
else:
|
||||
break
|
||||
|
||||
return len(paths)
|
||||
@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.engine = create_engine(f"sqlite+pysqlite:///{fullpath}", echo=True)
|
||||
# self.conn = self.engine.connect()
|
||||
# pass
|
||||
self.engine = create_engine(f"sqlite+pysqlite:///{fullpath}", echo=True)
|
||||
self.conn = self.engine.connect()
|
||||
|
||||
def __enter__(self):
|
||||
# return self.conn.execution_options(preserve_rowcount=True)
|
||||
return connection
|
||||
return self.conn.execution_options(preserve_rowcount=True)
|
||||
# return connection
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.commit:
|
||||
connection.commit()
|
||||
self.conn.commit()
|
||||
|
||||
# self.conn.close()
|
||||
self.conn.close()
|
||||
|
||||
|
||||
class Base(MappedAsDataclass, DeclarativeBase):
|
||||
@classmethod
|
||||
def execute(cls, stmt: Any, commit: bool = False):
|
||||
with DbManager(commit=commit) as conn:
|
||||
return conn.execute(stmt)
|
||||
|
||||
@classmethod
|
||||
def insert_many(cls, items: list[dict[str, Any]]):
|
||||
"""
|
||||
Inserts multiple items into the database.
|
||||
"""
|
||||
with DbManager(commit=True) as conn:
|
||||
conn.execute(insert(cls).values(items))
|
||||
return conn.execute(insert(cls).values(items))
|
||||
|
||||
@classmethod
|
||||
def insert_one(cls, item: dict[str, Any]):
|
||||
@@ -127,277 +73,14 @@ class Base(MappedAsDataclass, DeclarativeBase):
|
||||
return cls.insert_many([item])
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
"""
|
||||
Returns all the items from the database.
|
||||
"""
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(select(cls))
|
||||
return result.fetchall()
|
||||
|
||||
|
||||
class ArtistTable(Base):
|
||||
__tablename__ = "artist"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
albumcount: Mapped[int] = mapped_column(Integer())
|
||||
artisthash: Mapped[str] = mapped_column(String(), unique=True, index=True)
|
||||
created_date: Mapped[int] = mapped_column(Integer())
|
||||
date: Mapped[int] = mapped_column(Integer())
|
||||
duration: Mapped[int] = mapped_column(Integer())
|
||||
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())
|
||||
def remove_all(cls):
|
||||
with DbManager(commit=True) as conn:
|
||||
conn.execute(delete(cls))
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, start: int, limit: int):
|
||||
with DbManager() as conn:
|
||||
if start == 0:
|
||||
result = conn.execute(select(cls))
|
||||
else:
|
||||
result = conn.execute(select(cls).offset(start).limit(limit))
|
||||
|
||||
all = result.fetchall()
|
||||
return artists_to_dataclasses(all), len(all)
|
||||
|
||||
@classmethod
|
||||
def get_artist_by_hash(cls, artisthash: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(ArtistTable).where(ArtistTable.artisthash == artisthash)
|
||||
)
|
||||
return artist_to_dataclass(result.fetchone())
|
||||
def all(cls):
|
||||
return cls.execute(select(cls))
|
||||
|
||||
|
||||
class AlbumTable(Base):
|
||||
__tablename__ = "album"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
albumartists: Mapped[list[dict[str, str]]] = mapped_column(JSON(), index=True)
|
||||
artisthashes: Mapped[list[str]] = mapped_column(JSON(), index=True)
|
||||
albumhash: Mapped[str] = mapped_column(String(), unique=True, index=True)
|
||||
base_title: Mapped[str] = mapped_column(String())
|
||||
color: Mapped[Optional[str]] = mapped_column(String())
|
||||
created_date: Mapped[int] = mapped_column(Integer())
|
||||
date: Mapped[int] = mapped_column(Integer())
|
||||
duration: Mapped[int] = mapped_column(Integer())
|
||||
genres: Mapped[str] = mapped_column(JSON())
|
||||
og_title: Mapped[str] = mapped_column(String())
|
||||
title: Mapped[str] = mapped_column(String())
|
||||
trackcount: Mapped[int] = mapped_column(Integer())
|
||||
|
||||
@classmethod
|
||||
def get_album_by_albumhash(cls, hash: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(AlbumTable).where(AlbumTable.albumhash == hash)
|
||||
)
|
||||
album = result.fetchone()
|
||||
|
||||
if album:
|
||||
return album_to_dataclass(album)
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_hash(cls, hashes: set[str]):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(AlbumTable).where(AlbumTable.albumhash.in_(hashes))
|
||||
)
|
||||
return albums_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, start: int, limit: int):
|
||||
with DbManager() as conn:
|
||||
if start == 0:
|
||||
result = conn.execute(select(AlbumTable))
|
||||
else:
|
||||
result = conn.execute(select(AlbumTable).offset(start).limit(limit))
|
||||
|
||||
all = result.fetchall()
|
||||
|
||||
return albums_to_dataclasses(all)[:limit], len(all)
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_artisthashes(cls, artisthashes: list[dict[str, str]]):
|
||||
with DbManager() as conn:
|
||||
albums: list[AlbumModel] = []
|
||||
|
||||
for artist in artisthashes:
|
||||
result = conn.execute(
|
||||
# NOTE: The artist dict keys need to in the same order they appear in the db for this to work!
|
||||
select(AlbumTable).where(AlbumTable.albumartists.contains(artist))
|
||||
)
|
||||
albums.extend(albums_to_dataclasses(result.fetchall()))
|
||||
|
||||
return albums
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_base_title(cls, base_title: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(AlbumTable).where(AlbumTable.base_title == base_title)
|
||||
)
|
||||
return albums_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_artisthash(cls, artisthash: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(AlbumTable).where(AlbumTable.artisthashes.contains(artisthash))
|
||||
)
|
||||
return albums_to_dataclasses(result.all())
|
||||
|
||||
|
||||
class TrackTable(Base):
|
||||
__tablename__ = "track"
|
||||
|
||||
id: Mapped[int] = mapped_column(init=False, primary_key=True)
|
||||
album: Mapped[str] = mapped_column(String())
|
||||
albumartists: Mapped[list[dict[str, str]]] = mapped_column(JSON())
|
||||
albumhash: Mapped[str] = mapped_column(String(), index=True)
|
||||
artisthashes: Mapped[list[str]] = mapped_column(JSON(), index=True)
|
||||
artists: Mapped[list[dict[str, str]]] = mapped_column(JSON(), index=True)
|
||||
bitrate: Mapped[int] = mapped_column(Integer())
|
||||
copyright: Mapped[Optional[str]] = mapped_column(String())
|
||||
date: Mapped[int] = mapped_column(Integer())
|
||||
disc: Mapped[int] = mapped_column(Integer())
|
||||
duration: Mapped[int] = mapped_column(Integer())
|
||||
filepath: Mapped[str] = mapped_column(String(), index=True, unique=True)
|
||||
folder: Mapped[str] = mapped_column(String(), index=True)
|
||||
genre: Mapped[Optional[list[dict[str, str]]]] = mapped_column(JSON())
|
||||
last_mod: Mapped[float] = mapped_column(Integer())
|
||||
og_album: Mapped[str] = mapped_column(String())
|
||||
og_title: Mapped[str] = mapped_column(String())
|
||||
title: Mapped[str] = mapped_column(String())
|
||||
track: Mapped[int] = mapped_column(Integer())
|
||||
trackhash: Mapped[str] = mapped_column(String(), index=True)
|
||||
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON())
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_filepaths(cls, filepaths: list[str]):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.filepath.in_(filepaths))
|
||||
)
|
||||
return tracks_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def count_tracks_containing_paths(cls, paths: list[str]):
|
||||
results: list[dict[str, int | str]] = []
|
||||
|
||||
with ThreadPoolExecutor() as executor:
|
||||
res = executor.map(countFilepathsInDir, paths)
|
||||
results = [
|
||||
{"path": path, "trackcount": count} for path, count in zip(paths, res)
|
||||
]
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_albumhash(cls, albumhash: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.albumhash == albumhash)
|
||||
)
|
||||
tracks = tracks_to_dataclasses(result.fetchall())
|
||||
return remove_duplicates(tracks, is_album_tracks=True)
|
||||
|
||||
@classmethod
|
||||
def get_track_by_trackhash(cls, hash: str, filepath: str = ""):
|
||||
with DbManager() as conn:
|
||||
if filepath:
|
||||
result = conn.execute(
|
||||
select(TrackTable)
|
||||
.where(
|
||||
and_(
|
||||
TrackTable.trackhash == hash,
|
||||
TrackTable.filepath == filepath,
|
||||
)
|
||||
)
|
||||
.order_by(TrackTable.bitrate.desc())
|
||||
)
|
||||
else:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.trackhash == hash)
|
||||
)
|
||||
|
||||
track = result.fetchone()
|
||||
|
||||
if track:
|
||||
return track_to_dataclass(track)
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_artisthash(cls, artisthash: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.artists.contains(artisthash))
|
||||
)
|
||||
return tracks_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def get_tracks_in_path(cls, path: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(TrackTable)
|
||||
.where(TrackTable.filepath.contains(path))
|
||||
.order_by(TrackTable.last_mod)
|
||||
)
|
||||
return tracks_to_dataclasses(result.fetchall())
|
||||
|
||||
|
||||
all_tracks = TrackTable.get_all()
|
||||
|
||||
for track in all_tracks:
|
||||
all_filepaths.append(track.filepath)
|
||||
|
||||
all_filepaths.sort()
|
||||
|
||||
# print("files in path: ",getFilepathsInDir("/home/cwilvx/Music/").__len__())
|
||||
|
||||
|
||||
# SECTION: Userdata database
|
||||
class UserTable(Base):
|
||||
__tablename__ = "user"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(), unique=True)
|
||||
firstname: Mapped[Optional[str]] = mapped_column(String())
|
||||
lastname: Mapped[Optional[str]] = mapped_column(String())
|
||||
password: Mapped[str] = mapped_column(String())
|
||||
email: Mapped[Optional[str]] = mapped_column(String())
|
||||
image: Mapped[Optional[str]] = mapped_column(String())
|
||||
roles: Mapped[list[str]] = mapped_column(JSON(), default_factory=lambda: ["user"])
|
||||
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(
|
||||
JSON(), default_factory=dict
|
||||
)
|
||||
|
||||
|
||||
# SECTION: HELPER FUNCTIONS
|
||||
|
||||
|
||||
def artist_to_dataclass(artist: Any):
|
||||
return ArtistModel(**artist._asdict())
|
||||
|
||||
|
||||
def artists_to_dataclasses(artists: Any):
|
||||
return [artist_to_dataclass(artist) for artist in artists]
|
||||
|
||||
|
||||
def album_to_dataclass(album: Any):
|
||||
return AlbumModel(**album._asdict())
|
||||
|
||||
|
||||
def albums_to_dataclasses(albums: Any):
|
||||
return [album_to_dataclass(album) for album in albums]
|
||||
|
||||
|
||||
def track_to_dataclass(track: Any):
|
||||
return TrackModel(**track._asdict())
|
||||
|
||||
|
||||
def tracks_to_dataclasses(tracks: Any):
|
||||
return [track_to_dataclass(track) for track in tracks]
|
||||
|
||||
|
||||
Base().metadata.create_all(engine)
|
||||
def create_all():
|
||||
Base().metadata.create_all(engine)
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
from app.db import (
|
||||
Base as MasterBase,
|
||||
DbManager,
|
||||
)
|
||||
from app.db.utils import (
|
||||
album_to_dataclass,
|
||||
albums_to_dataclasses,
|
||||
artist_to_dataclass,
|
||||
artists_to_dataclasses,
|
||||
track_to_dataclass,
|
||||
tracks_to_dataclasses,
|
||||
)
|
||||
from app.models import Album as AlbumModel
|
||||
from app.utils.remove_duplicates import remove_duplicates
|
||||
from app.db import engine
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Integer, String, and_, delete, select, update
|
||||
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
|
||||
|
||||
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
|
||||
def create_all():
|
||||
"""
|
||||
Create all the tables defined in this file.
|
||||
"""
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
class Base(MasterBase, DeclarativeBase):
|
||||
@classmethod
|
||||
def get_all_hashes(cls):
|
||||
with DbManager() as conn:
|
||||
if cls.__tablename__ == "track":
|
||||
stmt = select(TrackTable.trackhash)
|
||||
elif cls.__tablename__ == "album":
|
||||
stmt = select(AlbumTable.albumhash)
|
||||
elif cls.__tablename__ == "artist":
|
||||
stmt = select(ArtistTable.artisthash)
|
||||
|
||||
result = conn.execute(stmt)
|
||||
return {row[0] for row in result.fetchall()}
|
||||
|
||||
@classmethod
|
||||
def set_is_favorite(cls, hash: str, is_favorite: bool):
|
||||
"""
|
||||
Set the 'is_favorite' flag for a specific hash.
|
||||
|
||||
Args:
|
||||
hash (str): The hash value.
|
||||
is_favorite (bool): The value of the 'is_favorite' flag.
|
||||
"""
|
||||
with DbManager(commit=True) as conn:
|
||||
if cls.__tablename__ == "track":
|
||||
stmt = (
|
||||
update(cls)
|
||||
.where(TrackTable.trackhash == hash)
|
||||
.values(is_favorite=is_favorite)
|
||||
)
|
||||
elif cls.__tablename__ == "album":
|
||||
stmt = (
|
||||
update(cls)
|
||||
.where(AlbumTable.albumhash == hash)
|
||||
.values(is_favorite=is_favorite)
|
||||
)
|
||||
elif cls.__tablename__ == "artist":
|
||||
stmt = (
|
||||
update(cls)
|
||||
.where(ArtistTable.artisthash == hash)
|
||||
.values(is_favorite=is_favorite)
|
||||
)
|
||||
|
||||
conn.execute(stmt)
|
||||
|
||||
|
||||
class TrackTable(Base):
|
||||
__tablename__ = "track"
|
||||
|
||||
id: Mapped[int] = mapped_column(init=False, primary_key=True)
|
||||
album: Mapped[str] = mapped_column(String())
|
||||
albumartists: Mapped[list[dict[str, str]]] = mapped_column(JSON())
|
||||
albumhash: Mapped[str] = mapped_column(String(), index=True)
|
||||
artisthashes: Mapped[list[str]] = mapped_column(JSON(), index=True)
|
||||
artists: Mapped[list[dict[str, str]]] = mapped_column(JSON(), index=True)
|
||||
bitrate: Mapped[int] = mapped_column(Integer())
|
||||
copyright: Mapped[Optional[str]] = mapped_column(String())
|
||||
date: Mapped[int] = mapped_column(Integer(), nullable=True)
|
||||
disc: Mapped[int] = mapped_column(Integer())
|
||||
duration: Mapped[int] = mapped_column(Integer())
|
||||
filepath: Mapped[str] = mapped_column(String(), unique=True)
|
||||
folder: Mapped[str] = mapped_column(String(), index=True)
|
||||
genrehashes: Mapped[list[str]] = mapped_column(JSON(), index=True)
|
||||
genres: Mapped[Optional[list[dict[str, str]]]] = mapped_column(JSON())
|
||||
last_mod: Mapped[float] = mapped_column(Integer())
|
||||
og_album: Mapped[str] = mapped_column(String())
|
||||
og_title: Mapped[str] = mapped_column(String())
|
||||
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())
|
||||
playcount: Mapped[int] = mapped_column(Integer())
|
||||
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON())
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
with DbManager() 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:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.filepath.in_(filepaths))
|
||||
)
|
||||
return tracks_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_albumhash(cls, albumhash: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.albumhash == albumhash)
|
||||
)
|
||||
tracks = tracks_to_dataclasses(result.fetchall())
|
||||
return remove_duplicates(tracks, is_album_tracks=True)
|
||||
|
||||
@classmethod
|
||||
def get_track_by_trackhash(cls, hash: str, filepath: str = ""):
|
||||
with DbManager() as conn:
|
||||
if filepath:
|
||||
result = conn.execute(
|
||||
select(TrackTable)
|
||||
.where(
|
||||
and_(
|
||||
TrackTable.trackhash == hash,
|
||||
TrackTable.filepath == filepath,
|
||||
)
|
||||
)
|
||||
.order_by(TrackTable.bitrate.desc())
|
||||
)
|
||||
else:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.trackhash == hash)
|
||||
)
|
||||
|
||||
track = result.fetchone()
|
||||
|
||||
if track:
|
||||
return track_to_dataclass(track)
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_artisthash(cls, artisthash: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.artists.contains(artisthash))
|
||||
)
|
||||
return tracks_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def get_tracks_in_path(cls, path: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(TrackTable)
|
||||
.where(TrackTable.filepath.contains(path))
|
||||
.order_by(TrackTable.last_mod)
|
||||
)
|
||||
return tracks_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_trackhashes(cls, hashes: Iterable[str], limit: int | None = None):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(TrackTable).where(TrackTable.trackhash.in_(hashes)).limit(limit)
|
||||
)
|
||||
return tracks_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def remove_tracks_by_filepaths(cls, filepaths: set[str]):
|
||||
with DbManager(commit=True) as conn:
|
||||
conn.execute(delete(TrackTable).where(TrackTable.filepath.in_(filepaths)))
|
||||
|
||||
|
||||
class AlbumTable(Base):
|
||||
__tablename__ = "album"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
albumartists: Mapped[list[dict[str, str]]] = mapped_column(JSON(), index=True)
|
||||
artisthashes: Mapped[list[str]] = mapped_column(JSON(), index=True)
|
||||
albumhash: Mapped[str] = mapped_column(String(), unique=True, index=True)
|
||||
base_title: Mapped[str] = mapped_column(String())
|
||||
color: Mapped[Optional[str]] = mapped_column(String())
|
||||
created_date: Mapped[int] = mapped_column(Integer())
|
||||
date: Mapped[int] = mapped_column(Integer())
|
||||
duration: Mapped[int] = mapped_column(Integer())
|
||||
genrehashes: Mapped[list[str]] = mapped_column(JSON(), nullable=True, index=True)
|
||||
genres: Mapped[str] = mapped_column(JSON())
|
||||
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())
|
||||
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON())
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
with DbManager() 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:
|
||||
result = conn.execute(
|
||||
select(AlbumTable).where(AlbumTable.albumhash == hash)
|
||||
)
|
||||
album = result.fetchone()
|
||||
|
||||
if album:
|
||||
return album_to_dataclass(album)
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_albumhashes(cls, hashes: Iterable[str], limit: int | None = None):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(AlbumTable).where(AlbumTable.albumhash.in_(hashes)).limit(limit)
|
||||
)
|
||||
return albums_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_artisthashes(cls, artisthashes: list[str]):
|
||||
with DbManager() as conn:
|
||||
albums: list[AlbumModel] = []
|
||||
|
||||
for artist in artisthashes:
|
||||
result = conn.execute(
|
||||
# NOTE: The artist dict keys need to in the same order they appear in the db for this to work!
|
||||
select(AlbumTable).where(AlbumTable.albumartists.contains(artist))
|
||||
)
|
||||
albums.extend(albums_to_dataclasses(result.fetchall()))
|
||||
|
||||
return albums
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_base_title(cls, base_title: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(AlbumTable).where(AlbumTable.base_title == base_title)
|
||||
)
|
||||
return albums_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_artisthash(cls, artisthash: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(AlbumTable).where(AlbumTable.artisthashes.contains(artisthash))
|
||||
)
|
||||
return albums_to_dataclasses(result.all())
|
||||
|
||||
|
||||
class ArtistTable(Base):
|
||||
__tablename__ = "artist"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
albumcount: Mapped[int] = mapped_column(Integer())
|
||||
artisthash: Mapped[str] = mapped_column(String(), unique=True, index=True)
|
||||
created_date: Mapped[int] = mapped_column(Integer())
|
||||
date: Mapped[int] = mapped_column(Integer())
|
||||
duration: Mapped[int] = mapped_column(Integer())
|
||||
genrehashes: Mapped[list[str]] = mapped_column(JSON(), nullable=True, index=True)
|
||||
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())
|
||||
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON())
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
with DbManager() 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:
|
||||
result = conn.execute(
|
||||
select(ArtistTable).where(ArtistTable.artisthash == artisthash)
|
||||
)
|
||||
return artist_to_dataclass(result.fetchone())
|
||||
|
||||
@classmethod
|
||||
def get_artisthashes_not_in(cls, artisthashes: list[str]):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(ArtistTable.artisthash, ArtistTable.name).where(
|
||||
~ArtistTable.artisthash.in_(artisthashes)
|
||||
)
|
||||
)
|
||||
return [{"artisthash": row[0], "name": row[1]} for row in result.fetchall()]
|
||||
|
||||
@classmethod
|
||||
def get_artists_by_artisthashes(
|
||||
cls, hashes: Iterable[str], limit: int | None = None
|
||||
):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(ArtistTable)
|
||||
.where(ArtistTable.artisthash.in_(hashes))
|
||||
.limit(limit)
|
||||
)
|
||||
return artists_to_dataclasses(result.fetchall())
|
||||
@@ -0,0 +1,33 @@
|
||||
from app.db import Base, DbManager
|
||||
|
||||
|
||||
from sqlalchemy import Integer, insert, select, update
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class MigrationTable(Base):
|
||||
__tablename__ = "dbmigration"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
version: Mapped[int] = mapped_column(Integer())
|
||||
|
||||
@classmethod
|
||||
def set_version(cls, version: int):
|
||||
with DbManager(commit=True) as conn:
|
||||
result = conn.execute(
|
||||
update(cls).where(cls.id == 1).values(version=version)
|
||||
)
|
||||
|
||||
if result.rowcount == 0:
|
||||
conn.execute(insert(cls).values(id=1, version=version))
|
||||
|
||||
@classmethod
|
||||
def get_version(cls):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(select(cls.version).where(cls.id == 1))
|
||||
result = result.fetchone()
|
||||
|
||||
if result:
|
||||
return result[0]
|
||||
|
||||
return -1
|
||||
@@ -16,7 +16,7 @@ class SQLiteLastFMSimilarArtists:
|
||||
sql = """INSERT OR REPLACE INTO lastfm_similar_artists(artisthash, similar_artists) VALUES(?,?)"""
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (artist.artisthash, artist.similar_artist_hashes))
|
||||
cur.execute(sql, (artist.artisthash, artist.similar_artists))
|
||||
cur.close()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -8,7 +8,6 @@ from ..utils import SQLiteManager
|
||||
def plugin_tuple_to_obj(plugin_tuple: tuple) -> Plugin:
|
||||
return Plugin(
|
||||
name=plugin_tuple[1],
|
||||
description=plugin_tuple[2],
|
||||
active=bool(plugin_tuple[3]),
|
||||
settings=json.loads(plugin_tuple[4]),
|
||||
)
|
||||
@@ -43,15 +42,6 @@ class PluginsMethods:
|
||||
|
||||
return lastrowid
|
||||
|
||||
@classmethod
|
||||
def insert_lyrics_plugin(cls):
|
||||
plugin = Plugin(
|
||||
name="lyrics_finder",
|
||||
description="Find lyrics from the internet",
|
||||
active=False,
|
||||
settings={"auto_download": False},
|
||||
)
|
||||
cls.insert_plugin(plugin)
|
||||
|
||||
@classmethod
|
||||
def get_all_plugins(cls):
|
||||
|
||||
@@ -14,13 +14,6 @@ CREATE TABLE IF NOT EXISTS playlists (
|
||||
constraint fk_users foreign key (userid) references users(id) on delete cascade
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
id integer PRIMARY KEY,
|
||||
hash text not null,
|
||||
type text not null,
|
||||
timestamp integer not null default 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id integer PRIMARY KEY,
|
||||
root_dirs text NOT NULL,
|
||||
|
||||
+109
-108
@@ -1,150 +1,151 @@
|
||||
from pprint import pprint
|
||||
from typing import Any
|
||||
|
||||
from app.config import UserConfig
|
||||
from app.db.sqlite.utils import SQLiteManager
|
||||
from app.settings import SessionVars
|
||||
from app.utils.wintools import win_replace_slash
|
||||
|
||||
|
||||
class SettingsSQLMethods:
|
||||
"""
|
||||
Methods for interacting with the settings table.
|
||||
"""
|
||||
# class SettingsSQLMethods:
|
||||
# """
|
||||
# Methods for interacting with the settings table.
|
||||
# """
|
||||
|
||||
@staticmethod
|
||||
def get_all_settings():
|
||||
"""
|
||||
Gets all settings from the database.
|
||||
"""
|
||||
# @staticmethod
|
||||
# def get_all_settings():
|
||||
# """
|
||||
# Gets all settings from the database.
|
||||
# """
|
||||
|
||||
sql = "SELECT * FROM settings WHERE id = 1"
|
||||
# sql = "SELECT * FROM settings WHERE id = 1"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql)
|
||||
settings = cur.fetchone()
|
||||
cur.close()
|
||||
# with SQLiteManager(userdata_db=True) as cur:
|
||||
# cur.execute(sql)
|
||||
# settings = cur.fetchone()
|
||||
# cur.close()
|
||||
|
||||
# if root_dirs not set
|
||||
if settings is None:
|
||||
return []
|
||||
# # if root_dirs not set
|
||||
# if settings is None:
|
||||
# return []
|
||||
|
||||
# omit id, root_dirs, and exclude_dirs
|
||||
return settings[3:]
|
||||
# # omit id, root_dirs, and exclude_dirs
|
||||
# return settings[3:]
|
||||
|
||||
@staticmethod
|
||||
def get_root_dirs() -> list[str]:
|
||||
"""
|
||||
Gets custom root directories from the database.
|
||||
"""
|
||||
# @staticmethod
|
||||
# def get_root_dirs() -> list[str]:
|
||||
# """
|
||||
# Gets custom root directories from the database.
|
||||
# """
|
||||
|
||||
sql = "SELECT root_dirs FROM settings"
|
||||
# sql = "SELECT root_dirs FROM settings"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql)
|
||||
dirs = cur.fetchall()
|
||||
cur.close()
|
||||
# with SQLiteManager(userdata_db=True) as cur:
|
||||
# cur.execute(sql)
|
||||
# dirs = cur.fetchall()
|
||||
# cur.close()
|
||||
|
||||
dirs = [_dir[0] for _dir in dirs]
|
||||
return [win_replace_slash(d) for d in dirs]
|
||||
# dirs = [_dir[0] for _dir in dirs]
|
||||
# return [win_replace_slash(d) for d in dirs]
|
||||
|
||||
@staticmethod
|
||||
def add_root_dirs(dirs: list[str]):
|
||||
"""
|
||||
Add custom root directories to the database.
|
||||
"""
|
||||
# @staticmethod
|
||||
# def add_root_dirs(dirs: list[str]):
|
||||
# """
|
||||
# Add custom root directories to the database.
|
||||
# """
|
||||
|
||||
sql = "INSERT INTO settings (root_dirs) VALUES (?)"
|
||||
existing_dirs = SettingsSQLMethods.get_root_dirs()
|
||||
# sql = "INSERT INTO settings (root_dirs) VALUES (?)"
|
||||
# existing_dirs = SettingsSQLMethods.get_root_dirs()
|
||||
|
||||
dirs = [_dir for _dir in dirs if _dir not in existing_dirs]
|
||||
# dirs = [_dir for _dir in dirs if _dir not in existing_dirs]
|
||||
|
||||
if len(dirs) == 0:
|
||||
return
|
||||
# if len(dirs) == 0:
|
||||
# return
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
for _dir in dirs:
|
||||
cur.execute(sql, (_dir,))
|
||||
# with SQLiteManager(userdata_db=True) as cur:
|
||||
# for _dir in dirs:
|
||||
# cur.execute(sql, (_dir,))
|
||||
|
||||
@staticmethod
|
||||
def remove_root_dirs(dirs: list[str]):
|
||||
"""
|
||||
Remove custom root directories from the database.
|
||||
"""
|
||||
# @staticmethod
|
||||
# def remove_root_dirs(dirs: list[str]):
|
||||
# """
|
||||
# Remove custom root directories from the database.
|
||||
# """
|
||||
|
||||
sql = "DELETE FROM settings WHERE root_dirs = ?"
|
||||
# sql = "DELETE FROM settings WHERE root_dirs = ?"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
for _dir in dirs:
|
||||
cur.execute(sql, (_dir,))
|
||||
# with SQLiteManager(userdata_db=True) as cur:
|
||||
# for _dir in dirs:
|
||||
# cur.execute(sql, (_dir,))
|
||||
|
||||
# Not currently used anywhere, to be used later
|
||||
@staticmethod
|
||||
def add_excluded_dirs(dirs: list[str]):
|
||||
"""
|
||||
Add custom exclude directories to the database.
|
||||
"""
|
||||
# # Not currently used anywhere, to be used later
|
||||
# @staticmethod
|
||||
# def add_excluded_dirs(dirs: list[str]):
|
||||
# """
|
||||
# Add custom exclude directories to the database.
|
||||
# """
|
||||
|
||||
sql = "INSERT INTO settings (exclude_dirs) VALUES (?)"
|
||||
# sql = "INSERT INTO settings (exclude_dirs) VALUES (?)"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.executemany(sql, dirs)
|
||||
# with SQLiteManager(userdata_db=True) as cur:
|
||||
# cur.executemany(sql, dirs)
|
||||
|
||||
@staticmethod
|
||||
def remove_excluded_dirs(dirs: list[str]):
|
||||
"""
|
||||
Remove custom exclude directories from the database.
|
||||
"""
|
||||
# @staticmethod
|
||||
# def remove_excluded_dirs(dirs: list[str]):
|
||||
# """
|
||||
# Remove custom exclude directories from the database.
|
||||
# """
|
||||
|
||||
sql = "DELETE FROM settings WHERE exclude_dirs = ?"
|
||||
# sql = "DELETE FROM settings WHERE exclude_dirs = ?"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.executemany(sql, dirs)
|
||||
# with SQLiteManager(userdata_db=True) as cur:
|
||||
# cur.executemany(sql, dirs)
|
||||
|
||||
@staticmethod
|
||||
def get_excluded_dirs() -> list[str]:
|
||||
"""
|
||||
Gets custom exclude directories from the database.
|
||||
"""
|
||||
# @staticmethod
|
||||
# def get_excluded_dirs() -> list[str]:
|
||||
# """
|
||||
# Gets custom exclude directories from the database.
|
||||
# """
|
||||
|
||||
sql = "SELECT exclude_dirs FROM settings"
|
||||
# sql = "SELECT exclude_dirs FROM settings"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql)
|
||||
dirs = cur.fetchall()
|
||||
return [_dir[0] for _dir in dirs]
|
||||
# with SQLiteManager(userdata_db=True) as cur:
|
||||
# cur.execute(sql)
|
||||
# dirs = cur.fetchall()
|
||||
# return [_dir[0] for _dir in dirs]
|
||||
|
||||
@staticmethod
|
||||
def get_settings() -> dict[str, Any]:
|
||||
pass
|
||||
# @staticmethod
|
||||
# def get_settings() -> dict[str, Any]:
|
||||
# pass
|
||||
|
||||
@staticmethod
|
||||
def set_setting(key: str, value: Any):
|
||||
sql = f"UPDATE settings SET {key} = :value WHERE id = 1"
|
||||
# @staticmethod
|
||||
# def set_setting(key: str, value: Any):
|
||||
# sql = f"UPDATE settings SET {key} = :value WHERE id = 1"
|
||||
|
||||
if type(value) == bool:
|
||||
value = str(int(value))
|
||||
# if type(value) == bool:
|
||||
# value = str(int(value))
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, {"value": value})
|
||||
# with SQLiteManager(userdata_db=True) as cur:
|
||||
# cur.execute(sql, {"value": value})
|
||||
|
||||
|
||||
def load_settings():
|
||||
s = SettingsSQLMethods.get_all_settings()
|
||||
# def load_settings():
|
||||
# # s = SettingsSQLMethods.get_all_settings()
|
||||
# config = UserConfig()
|
||||
|
||||
try:
|
||||
db_separators: str = s[0]
|
||||
db_separators = db_separators.replace(" ", "")
|
||||
separators = db_separators.split(",")
|
||||
separators = set(separators)
|
||||
except IndexError:
|
||||
separators = {";", "/"}
|
||||
# try:
|
||||
# db_separators: str = s[0]
|
||||
# db_separators = db_separators.replace(" ", "")
|
||||
# separators = db_separators.split(",")
|
||||
# separators = set(separators)
|
||||
# except IndexError:
|
||||
# separators = {";", "/"}
|
||||
|
||||
SessionVars.ARTIST_SEPARATORS = separators
|
||||
# SessionVars.ARTIST_SEPARATORS = config.artistSeparators
|
||||
|
||||
# boolean settings
|
||||
SessionVars.EXTRACT_FEAT = bool(s[1])
|
||||
SessionVars.REMOVE_PROD = bool(s[2])
|
||||
SessionVars.CLEAN_ALBUM_TITLE = bool(s[3])
|
||||
SessionVars.REMOVE_REMASTER_FROM_TRACK = bool(s[4])
|
||||
SessionVars.MERGE_ALBUM_VERSIONS = bool(s[5])
|
||||
SessionVars.SHOW_ALBUMS_AS_SINGLES = bool(s[6])
|
||||
# # boolean settings
|
||||
# SessionVars.EXTRACT_FEAT = bool(s[1])
|
||||
# SessionVars.REMOVE_PROD = bool(s[2])
|
||||
# SessionVars.CLEAN_ALBUM_TITLE = bool(s[3])
|
||||
# SessionVars.REMOVE_REMASTER_FROM_TRACK = bool(s[4])
|
||||
# SessionVars.MERGE_ALBUM_VERSIONS = bool(s[5])
|
||||
# SessionVars.SHOW_ALBUMS_AS_SINGLES = bool(s[6])
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import datetime
|
||||
from shlex import join
|
||||
from typing import Any
|
||||
from flask_jwt_extended import current_user
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
and_,
|
||||
delete,
|
||||
insert,
|
||||
select,
|
||||
update,
|
||||
join,
|
||||
)
|
||||
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.utils import (
|
||||
albums_to_dataclasses,
|
||||
artists_to_dataclasses,
|
||||
favorites_to_dataclass,
|
||||
plugin_to_dataclasses,
|
||||
similar_artist_to_dataclass,
|
||||
similar_artists_to_dataclass,
|
||||
tracks_to_dataclasses,
|
||||
user_to_dataclass,
|
||||
user_to_dataclasses,
|
||||
)
|
||||
|
||||
from app.db import Base, DbManager
|
||||
from app.utils.auth import hash_password
|
||||
|
||||
|
||||
class UserTable(Base):
|
||||
__tablename__ = "user"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
image: Mapped[str] = mapped_column(String(), nullable=True)
|
||||
password: Mapped[str] = mapped_column(String())
|
||||
username: Mapped[str] = mapped_column(String(), index=True)
|
||||
roles: Mapped[list[str]] = mapped_column(JSON(), default_factory=lambda: ["user"])
|
||||
extra: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSON(), nullable=True, default_factory=dict
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
result = cls.execute(select(cls))
|
||||
return user_to_dataclasses(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def insert_default_user(cls):
|
||||
user = {
|
||||
"username": "admin",
|
||||
"password": hash_password("admin"),
|
||||
"roles": ["admin"],
|
||||
}
|
||||
|
||||
return cls.insert_one(user)
|
||||
|
||||
@classmethod
|
||||
def insert_guest_user(cls):
|
||||
user = {
|
||||
"username": "guest",
|
||||
"password": hash_password("guest"),
|
||||
"roles": ["guest"],
|
||||
}
|
||||
|
||||
return cls.insert_one(user)
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, id: int):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(select(cls).where(cls.id == id))
|
||||
res = result.fetchone()
|
||||
|
||||
if res:
|
||||
return user_to_dataclass(res)
|
||||
|
||||
@classmethod
|
||||
def get_by_username(cls, username: str):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(select(cls).where(cls.username == username))
|
||||
res = result.fetchone()
|
||||
|
||||
if res:
|
||||
return user_to_dataclass(res)
|
||||
|
||||
@classmethod
|
||||
def update_one(cls, user: dict[str, Any]):
|
||||
with DbManager(commit=True) as conn:
|
||||
conn.execute(update(cls).where(cls.id == user["id"]).values(user))
|
||||
|
||||
@classmethod
|
||||
def remove_by_username(cls, username: str):
|
||||
return cls.execute(delete(cls).where(cls.username == username), commit=True)
|
||||
|
||||
|
||||
class PluginTable(Base):
|
||||
__tablename__ = "plugin"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(), unique=True)
|
||||
active: Mapped[bool] = mapped_column(Boolean())
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON())
|
||||
extra: Mapped[dict[str, Any]] = mapped_column(JSON(), nullable=True)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
return plugin_to_dataclasses(cls.all())
|
||||
|
||||
|
||||
class SimilarArtistTable(Base):
|
||||
__tablename__ = "notlastfm_similar_artists"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer(), primary_key=True)
|
||||
artisthash: Mapped[str] = mapped_column(String(), index=True)
|
||||
similar_artists: Mapped[dict[str, str]] = mapped_column(JSON())
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(select(cls))
|
||||
return similar_artists_to_dataclass(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def exists(cls, artisthash: str):
|
||||
"""
|
||||
Check whether an artisthash exists in the database.
|
||||
"""
|
||||
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(
|
||||
select(cls.artisthash).where(cls.artisthash == artisthash)
|
||||
)
|
||||
return result.fetchone() is not None
|
||||
|
||||
@classmethod
|
||||
def get_by_hash(cls, artisthash: str):
|
||||
"""
|
||||
Get a single artist by hash.
|
||||
"""
|
||||
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(select(cls).where(cls.artisthash == artisthash))
|
||||
result = result.fetchone()
|
||||
|
||||
if result:
|
||||
return similar_artist_to_dataclass(result)
|
||||
|
||||
|
||||
class FavoritesTable(Base):
|
||||
__tablename__ = "favorite"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
hash: Mapped[str] = mapped_column(String())
|
||||
type: Mapped[str] = mapped_column(String(), index=True)
|
||||
timestamp: Mapped[int] = mapped_column(Integer(), index=True)
|
||||
userid: Mapped[int] = mapped_column(
|
||||
Integer(), ForeignKey("user.id"), default=1, index=True
|
||||
)
|
||||
extra: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSON(), nullable=True, default_factory=dict
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
with DbManager() as conn:
|
||||
result = conn.execute(select(cls))
|
||||
return favorites_to_dataclass(result.fetchall())
|
||||
|
||||
@classmethod
|
||||
def insert_item(cls, item: dict[str, Any]):
|
||||
item["timestamp"] = int(datetime.datetime.now().timestamp())
|
||||
item["userid"] = current_user["id"]
|
||||
|
||||
with DbManager(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:
|
||||
conn.execute(
|
||||
delete(cls).where(
|
||||
(cls.hash == item["hash"]) & (cls.type == item["type"])
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check_exists(cls, hash: str, type: str):
|
||||
result = cls.execute(select(cls).where((cls.hash == hash) & (cls.type == type)))
|
||||
return result.fetchone() is not None
|
||||
|
||||
@classmethod
|
||||
def get_all_of_type(cls, table: Any, field: Any, 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 == current_user["id"]))
|
||||
.offset(start)
|
||||
# INFO: If start is 0, fetch all so we can get the total count
|
||||
.limit(limit if start != 0 else None)
|
||||
)
|
||||
|
||||
res = result.fetchall()
|
||||
|
||||
if start == 0:
|
||||
return res[:limit], len(res)
|
||||
|
||||
return res, -1
|
||||
|
||||
@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
|
||||
|
||||
@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
|
||||
|
||||
@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
|
||||
@@ -0,0 +1,75 @@
|
||||
from typing import Any
|
||||
|
||||
from app.models import Album as AlbumModel, Artist as ArtistModel, Track as TrackModel
|
||||
from app.models.favorite import Favorite
|
||||
from app.models.lastfm import SimilarArtist
|
||||
from app.models.plugins import Plugin
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def track_to_dataclass(track: Any):
|
||||
return TrackModel(**track._asdict())
|
||||
|
||||
|
||||
def tracks_to_dataclasses(tracks: Any):
|
||||
return [track_to_dataclass(track) for track in tracks]
|
||||
|
||||
|
||||
def album_to_dataclass(album: Any):
|
||||
return AlbumModel(**album._asdict())
|
||||
|
||||
|
||||
def albums_to_dataclasses(albums: Any):
|
||||
return [album_to_dataclass(album) for album in albums]
|
||||
|
||||
|
||||
def artist_to_dataclass(artist: Any):
|
||||
return ArtistModel(**artist._asdict())
|
||||
|
||||
|
||||
def artists_to_dataclasses(artists: Any):
|
||||
return [artist_to_dataclass(artist) for artist in artists]
|
||||
|
||||
|
||||
# SECTION: User data helpers
|
||||
|
||||
|
||||
def similar_artist_to_dataclass(entry: Any):
|
||||
entry_dict = entry._asdict()
|
||||
del entry_dict["id"]
|
||||
|
||||
return SimilarArtist(**entry_dict)
|
||||
|
||||
|
||||
def similar_artists_to_dataclass(entries: Any):
|
||||
return [similar_artist_to_dataclass(entry) for entry in entries]
|
||||
|
||||
|
||||
def favorite_to_dataclass(entry: Any):
|
||||
entry_dict = entry._asdict()
|
||||
del entry_dict["id"]
|
||||
|
||||
return Favorite(**entry_dict)
|
||||
|
||||
|
||||
def favorites_to_dataclass(entries: Any):
|
||||
return [favorite_to_dataclass(entry) for entry in entries]
|
||||
|
||||
|
||||
def user_to_dataclass(entry: Any):
|
||||
entry_dict = entry._asdict()
|
||||
return User(**entry_dict)
|
||||
|
||||
|
||||
def user_to_dataclasses(entries: Any):
|
||||
return [user_to_dataclass(entry) for entry in entries]
|
||||
|
||||
|
||||
def plugin_to_dataclass(entry: Any):
|
||||
entry_dict = entry._asdict()
|
||||
del entry_dict["id"]
|
||||
return Plugin(**entry_dict)
|
||||
|
||||
|
||||
def plugin_to_dataclasses(entries: Any):
|
||||
return [plugin_to_dataclass(entry) for entry in entries]
|
||||
Reference in New Issue
Block a user