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:
cwilvx
2024-06-30 15:06:33 +03:00
parent 1a66194c6c
commit 4a9f804e70
53 changed files with 1719 additions and 1353 deletions
+32 -349
View File
@@ -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)
+312
View File
@@ -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())
+33
View File
@@ -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
+1 -1
View File
@@ -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
-10
View File
@@ -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):
-7
View File
@@ -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
View File
@@ -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])
+240
View File
@@ -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
+75
View File
@@ -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]