mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
@@ -0,0 +1,214 @@
|
||||
class AlbumMethods:
|
||||
"""
|
||||
Lists all the methods that can be found in the Albums class.
|
||||
"""
|
||||
|
||||
def insert_album():
|
||||
"""
|
||||
Inserts a new album object into the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_all_albums():
|
||||
"""
|
||||
Returns all the albums in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_album_by_id():
|
||||
"""
|
||||
Returns a single album matching the passed id.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_album_by_name():
|
||||
"""
|
||||
Returns a single album matching the passed name.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_album_by_artist():
|
||||
"""
|
||||
Returns a single album matching the passed artist name.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ArtistMethods:
|
||||
"""
|
||||
Lists all the methods that can be found in the Artists class.
|
||||
"""
|
||||
|
||||
def insert_artist():
|
||||
"""
|
||||
Inserts a new artist object into the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_all_artists():
|
||||
"""
|
||||
Returns all the artists in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_artist_by_id():
|
||||
"""
|
||||
Returns an artist matching the mongo Id.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_artists_by_name():
|
||||
"""
|
||||
Returns all the artists matching the query.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PlaylistMethods:
|
||||
"""
|
||||
Lists all the methods that can be found in the Playlists class.
|
||||
"""
|
||||
|
||||
def insert_playlist():
|
||||
"""
|
||||
Inserts a new playlist object into the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_all_playlists():
|
||||
"""
|
||||
Returns all the playlists in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_playlist_by_id():
|
||||
"""
|
||||
Returns a single playlist matching the id in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def add_track_to_playlist():
|
||||
"""
|
||||
Adds a track to a playlist.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_playlist_by_name():
|
||||
"""
|
||||
Returns a single playlist matching the name in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def update_playlist():
|
||||
"""
|
||||
Updates a playlist.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TrackMethods:
|
||||
"""
|
||||
Lists all the methods that can be found in the Tracks class.
|
||||
"""
|
||||
|
||||
def insert_one_track():
|
||||
"""
|
||||
Inserts a new track object into the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def drop_db():
|
||||
"""
|
||||
Drops the entire database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_all_tracks():
|
||||
"""
|
||||
Returns all the tracks in the database.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_track_by_id():
|
||||
"""
|
||||
Returns a single track matching the id in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_track_by_album():
|
||||
"""
|
||||
Returns a single track matching the album in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def search_tracks_by_album():
|
||||
"""
|
||||
Returns all the tracks matching the albums in the query params (using regex).
|
||||
"""
|
||||
pass
|
||||
|
||||
def search_tracks_by_artist():
|
||||
"""
|
||||
Returns all the tracks matching the artists in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_track_by_title():
|
||||
"""
|
||||
Finds all the tracks matching the title in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_album():
|
||||
"""
|
||||
Finds all the tracks matching the album in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_folder():
|
||||
"""
|
||||
Finds all the tracks matching the folder in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_artist():
|
||||
"""
|
||||
Finds all the tracks matching the artist in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_albumartist():
|
||||
"""
|
||||
Finds all the tracks matching the album artist in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_track_by_path():
|
||||
"""
|
||||
Returns a single track matching the path in the query params.
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_track_by_path():
|
||||
"""
|
||||
Removes a track from the database. Returns a boolean indicating success or failure of the operation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_track_by_id():
|
||||
"""
|
||||
Removes a track from the database. Returns a boolean indicating success or failure of the operation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_tracks_by_albumhash():
|
||||
"""
|
||||
Returns all the tracks matching the passed hash.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_dir_t_count():
|
||||
"""
|
||||
Returns a list of all the tracks matching the path in the query params.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
This module contains the functions to interact with the SQLite database.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from sqlite3 import Connection as SqlConn
|
||||
|
||||
from app.settings import APP_DB_PATH
|
||||
|
||||
|
||||
def create_connection(db_file: str) -> SqlConn:
|
||||
"""
|
||||
Creates a connection to the specified database.
|
||||
"""
|
||||
conn = sqlite3.connect(db_file)
|
||||
return conn
|
||||
|
||||
|
||||
def get_sqlite_conn():
|
||||
"""
|
||||
It opens a connection to the database
|
||||
:return: A connection to the database.
|
||||
"""
|
||||
return create_connection(APP_DB_PATH)
|
||||
|
||||
|
||||
def create_tables(conn: SqlConn, sql_query: str):
|
||||
"""
|
||||
Executes the specifiend SQL file to create database tables.
|
||||
"""
|
||||
# with open(sql_query, "r", encoding="utf-8") as sql_file:
|
||||
conn.executescript(sql_query)
|
||||
|
||||
|
||||
def setup_search_db():
|
||||
"""
|
||||
Creates the search database.
|
||||
"""
|
||||
db = sqlite3.connect(":memory:")
|
||||
sql_file = "queries/fts5.sql"
|
||||
|
||||
current_path = Path(__file__).parent.resolve()
|
||||
sql_path = current_path.joinpath(sql_file)
|
||||
|
||||
with open(sql_path, "r", encoding="utf-8") as sql_file:
|
||||
db.executescript(sql_file.read())
|
||||
@@ -0,0 +1,125 @@
|
||||
from sqlite3 import Cursor
|
||||
|
||||
from app.db import AlbumMethods
|
||||
|
||||
from .utils import SQLiteManager, tuple_to_album, tuples_to_albums
|
||||
|
||||
|
||||
class SQLiteAlbumMethods(AlbumMethods):
|
||||
@classmethod
|
||||
def insert_one_album(cls, cur: Cursor, albumhash: str, colors: str):
|
||||
"""
|
||||
Inserts one album into the database
|
||||
"""
|
||||
|
||||
sql = """INSERT INTO albums(
|
||||
albumhash,
|
||||
colors
|
||||
) VALUES(?,?)
|
||||
"""
|
||||
|
||||
cur.execute(sql, (albumhash, colors))
|
||||
|
||||
return cur.lastrowid
|
||||
|
||||
# @classmethod
|
||||
# def insert_many_albums(cls, albums: list[dict]):
|
||||
# """
|
||||
# Takes a generator of albums, and inserts them into the database
|
||||
|
||||
# Parameters
|
||||
# ----------
|
||||
# albums : Generator
|
||||
# Generator
|
||||
# """
|
||||
# with SQLiteManager() as cur:
|
||||
# for album in albums:
|
||||
# cls.insert_one_album(cur, album["albumhash"], album["colors"])
|
||||
|
||||
@classmethod
|
||||
def get_all_albums(cls):
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute("SELECT * FROM albums")
|
||||
albums = cur.fetchall()
|
||||
|
||||
if albums is not None:
|
||||
return albums
|
||||
|
||||
return []
|
||||
|
||||
# @staticmethod
|
||||
# def get_album_by_id(album_id: int):
|
||||
# conn = get_sqlite_conn()
|
||||
# cur = conn.cursor()
|
||||
|
||||
# cur.execute("SELECT * FROM albums WHERE id=?", (album_id,))
|
||||
# album = cur.fetchone()
|
||||
|
||||
# conn.close()
|
||||
|
||||
# if album is None:
|
||||
# return None
|
||||
|
||||
# return tuple_to_album(album)
|
||||
|
||||
@staticmethod
|
||||
def get_album_by_hash(album_hash: str):
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute("SELECT * FROM albums WHERE albumhash=?", (album_hash,))
|
||||
album = cur.fetchone()
|
||||
|
||||
if album is not None:
|
||||
return tuple_to_album(album)
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_hashes(cls, album_hashes: list):
|
||||
"""
|
||||
Gets all the albums with the specified hashes. Returns a generator of albums or an empty list.
|
||||
"""
|
||||
with SQLiteManager() as cur:
|
||||
hashes = ",".join("?" * len(album_hashes))
|
||||
cur.execute(
|
||||
f"SELECT * FROM albums WHERE albumhash IN ({hashes})", album_hashes
|
||||
)
|
||||
albums = cur.fetchall()
|
||||
|
||||
if albums is not None:
|
||||
return tuples_to_albums(albums)
|
||||
|
||||
return []
|
||||
|
||||
# @staticmethod
|
||||
# def update_album_colors(album_hash: str, colors: list[str]):
|
||||
# sql = "UPDATE albums SET colors=? WHERE albumhash=?"
|
||||
|
||||
# colors_str = json.dumps(colors)
|
||||
|
||||
# with SQLiteManager() as cur:
|
||||
# cur.execute(sql, (colors_str, album_hash))
|
||||
|
||||
@staticmethod
|
||||
def get_albums_by_albumartist(albumartist: str):
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute("SELECT * FROM albums WHERE albumartist=?", (albumartist,))
|
||||
albums = cur.fetchall()
|
||||
|
||||
if albums is not None:
|
||||
return tuples_to_albums(albums)
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_all_albums_raw():
|
||||
"""
|
||||
Returns all the albums in the database, as a list of tuples.
|
||||
"""
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute("SELECT * FROM albums")
|
||||
albums = cur.fetchall()
|
||||
|
||||
if albums is not None:
|
||||
return albums
|
||||
|
||||
return []
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Contains methods for reading and writing to the sqlite artists database.
|
||||
"""
|
||||
|
||||
import json
|
||||
from .utils import SQLiteManager
|
||||
|
||||
|
||||
class SQLiteArtistMethods:
|
||||
@classmethod
|
||||
def insert_one_artist(cls, artisthash: str, colors: str | list[str]):
|
||||
"""
|
||||
Inserts a single artist into the database.
|
||||
"""
|
||||
sql = """INSERT INTO artists(
|
||||
artisthash,
|
||||
colors
|
||||
) VALUES(?,?)
|
||||
"""
|
||||
colors = json.dumps(colors)
|
||||
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute(sql, (artisthash, colors))
|
||||
|
||||
@classmethod
|
||||
def get_all_artists(cls):
|
||||
"""
|
||||
Get all artists from the database and return a generator of Artist objects
|
||||
"""
|
||||
sql = """SELECT * FROM artists"""
|
||||
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute(sql)
|
||||
|
||||
for artist in cur.fetchall():
|
||||
yield artist
|
||||
@@ -0,0 +1,77 @@
|
||||
from app.models import FavType
|
||||
from .utils import SQLiteManager
|
||||
|
||||
|
||||
class SQLiteFavoriteMethods:
|
||||
"""THis class contains methods for interacting with the favorites table."""
|
||||
|
||||
@classmethod
|
||||
def insert_one_favorite(cls, fav_type: str, fav_hash: str):
|
||||
"""
|
||||
Inserts a single favorite into the database.
|
||||
"""
|
||||
sql = """INSERT INTO favorites(type, hash) VALUES(?,?)"""
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (fav_type, fav_hash))
|
||||
|
||||
@classmethod
|
||||
def get_all(cls) -> list[tuple]:
|
||||
"""
|
||||
Returns a list of all favorites.
|
||||
"""
|
||||
sql = """SELECT * FROM favorites"""
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql)
|
||||
return cur.fetchall()
|
||||
|
||||
@classmethod
|
||||
def get_favorites(cls, fav_type: str) -> list[tuple]:
|
||||
"""
|
||||
Returns a list of favorite tracks.
|
||||
"""
|
||||
sql = """SELECT * FROM favorites WHERE type = ?"""
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (fav_type,))
|
||||
return cur.fetchall()
|
||||
|
||||
@classmethod
|
||||
def get_fav_tracks(cls) -> list[tuple]:
|
||||
"""
|
||||
Returns a list of favorite tracks.
|
||||
"""
|
||||
return cls.get_favorites(FavType.track)
|
||||
|
||||
@classmethod
|
||||
def get_fav_albums(cls) -> list[tuple]:
|
||||
"""
|
||||
Returns a list of favorite albums.
|
||||
"""
|
||||
return cls.get_favorites(FavType.album)
|
||||
|
||||
@classmethod
|
||||
def get_fav_artists(cls) -> list[tuple]:
|
||||
"""
|
||||
Returns a list of favorite artists.
|
||||
"""
|
||||
return cls.get_favorites(FavType.artist)
|
||||
|
||||
@classmethod
|
||||
def delete_favorite(cls, fav_type: str, fav_hash: str):
|
||||
"""
|
||||
Deletes a favorite from the database.
|
||||
"""
|
||||
sql = """DELETE FROM favorites WHERE hash = ? AND type = ?"""
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (fav_hash, fav_type))
|
||||
|
||||
@classmethod
|
||||
def check_is_favorite(cls, itemhash: str, fav_type: str):
|
||||
"""
|
||||
Checks if an item is favorited.
|
||||
"""
|
||||
sql = """SELECT * FROM favorites WHERE hash = ? AND type = ?"""
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (itemhash, fav_type))
|
||||
items = cur.fetchall()
|
||||
return len(items) > 0
|
||||
@@ -0,0 +1,179 @@
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
from app.db.sqlite.tracks import SQLiteTrackMethods
|
||||
from app.db.sqlite.utils import SQLiteManager, tuple_to_playlist, tuples_to_playlists
|
||||
from app.models import Artist
|
||||
from app.utils import background
|
||||
|
||||
|
||||
class SQLitePlaylistMethods:
|
||||
"""
|
||||
This class contains methods for interacting with the playlists table.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def insert_one_playlist(playlist: dict):
|
||||
sql = """INSERT INTO playlists(
|
||||
artisthashes,
|
||||
banner_pos,
|
||||
has_gif,
|
||||
image,
|
||||
last_updated,
|
||||
name,
|
||||
trackhashes
|
||||
) VALUES(?,?,?,?,?,?,?)
|
||||
"""
|
||||
|
||||
playlist = OrderedDict(sorted(playlist.items()))
|
||||
params = (*playlist.values(),)
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, params)
|
||||
pid = cur.lastrowid
|
||||
params = (pid, *params)
|
||||
|
||||
return tuple_to_playlist(params)
|
||||
|
||||
@staticmethod
|
||||
def get_playlist_by_name(name: str):
|
||||
sql = "SELECT * FROM playlists WHERE name = ?"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (name,))
|
||||
|
||||
data = cur.fetchone()
|
||||
|
||||
if data is not None:
|
||||
return tuple_to_playlist(data)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def count_playlist_by_name(name: str):
|
||||
sql = "SELECT COUNT(*) FROM playlists WHERE name = ?"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (name,))
|
||||
|
||||
data = cur.fetchone()
|
||||
|
||||
return int(data[0])
|
||||
|
||||
@staticmethod
|
||||
def get_all_playlists():
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute("SELECT * FROM playlists")
|
||||
playlists = cur.fetchall()
|
||||
|
||||
if playlists is not None:
|
||||
return tuples_to_playlists(playlists)
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_playlist_by_id(playlist_id: int):
|
||||
sql = "SELECT * FROM playlists WHERE id = ?"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (playlist_id,))
|
||||
|
||||
data = cur.fetchone()
|
||||
|
||||
if data is not None:
|
||||
return tuple_to_playlist(data)
|
||||
|
||||
return None
|
||||
|
||||
# FIXME: Extract the "add_track_to_playlist" method to use it for both the artisthash and trackhash lists.
|
||||
|
||||
@staticmethod
|
||||
def add_item_to_json_list(playlist_id: int, field: str, items: list[str]):
|
||||
"""
|
||||
Adds a string item to a json dumped list using a playlist id and field name. Takes the playlist ID, a field name, an item to add to the field, and an error to raise if the item is already in the field.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
playlist_id : int
|
||||
The ID of the playlist to add the item to.
|
||||
field : str
|
||||
The field in the database that you want to add the item to.
|
||||
item : str
|
||||
The item to add to the list.
|
||||
error : Exception
|
||||
The error to raise if the item is already in the list.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A list of strings.
|
||||
|
||||
"""
|
||||
sql = f"SELECT {field} FROM playlists WHERE id = ?"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (playlist_id,))
|
||||
data = cur.fetchone()
|
||||
|
||||
if data is not None:
|
||||
db_items: list[str] = json.loads(data[0])
|
||||
|
||||
for item in items:
|
||||
if item in db_items:
|
||||
items.remove(item)
|
||||
|
||||
db_items.extend(items)
|
||||
|
||||
sql = f"UPDATE playlists SET {field} = ? WHERE id = ?"
|
||||
cur.execute(sql, (json.dumps(db_items), playlist_id))
|
||||
return len(items)
|
||||
|
||||
@classmethod
|
||||
def add_tracks_to_playlist(cls, playlist_id: int, trackhashes: list[str]):
|
||||
return cls.add_item_to_json_list(playlist_id, "trackhashes", trackhashes)
|
||||
|
||||
@classmethod
|
||||
@background
|
||||
def add_artist_to_playlist(cls, playlist_id: int, trackhash: str):
|
||||
track = SQLiteTrackMethods.get_track_by_trackhash(trackhash)
|
||||
if track is None:
|
||||
return
|
||||
|
||||
artists: list[Artist] = track.artist # type: ignore
|
||||
artisthashes = [a.artisthash for a in artists]
|
||||
|
||||
cls.add_item_to_json_list(playlist_id, "artisthashes", artisthashes)
|
||||
|
||||
@staticmethod
|
||||
def update_playlist(playlist_id: int, playlist: dict):
|
||||
sql = """UPDATE playlists SET
|
||||
has_gif = ?,
|
||||
image = ?,
|
||||
last_updated = ?,
|
||||
name = ?
|
||||
WHERE id = ?
|
||||
"""
|
||||
|
||||
del playlist["id"]
|
||||
del playlist["trackhashes"]
|
||||
del playlist["artisthashes"]
|
||||
del playlist['banner_pos']
|
||||
|
||||
playlist = OrderedDict(sorted(playlist.items()))
|
||||
params = (*playlist.values(), playlist_id)
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, params)
|
||||
|
||||
@staticmethod
|
||||
def delete_playlist(pid: str):
|
||||
sql = "DELETE FROM playlists WHERE id = ?"
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (pid,))
|
||||
|
||||
@staticmethod
|
||||
def update_banner_pos(playlistid: int, pos: int):
|
||||
sql = """UPDATE playlists SET banner_pos = ? WHERE id = ?"""
|
||||
|
||||
with SQLiteManager(userdata_db=True) as cur:
|
||||
cur.execute(sql, (pos, playlistid))
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
This file contains the SQL queries to create the database tables.
|
||||
"""
|
||||
|
||||
|
||||
CREATE_USERDATA_TABLES = """
|
||||
CREATE TABLE IF NOT EXISTS playlists (
|
||||
id integer PRIMARY KEY,
|
||||
artisthashes text,
|
||||
banner_pos integer NOT NULL,
|
||||
has_gif integer,
|
||||
image text,
|
||||
last_updated text not null,
|
||||
name text not null,
|
||||
trackhashes text
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
id integer PRIMARY KEY,
|
||||
hash text not null,
|
||||
type text not null
|
||||
);
|
||||
"""
|
||||
|
||||
CREATE_APPDB_TABLES = """
|
||||
CREATE TABLE IF NOT EXISTS tracks (
|
||||
id integer PRIMARY KEY,
|
||||
album text NOT NULL,
|
||||
albumartist text NOT NULL,
|
||||
albumhash text NOT NULL,
|
||||
artist text NOT NULL,
|
||||
bitrate integer NOT NULL,
|
||||
copyright text,
|
||||
date text NOT NULL,
|
||||
disc integer NOT NULL,
|
||||
duration integer NOT NULL,
|
||||
filepath text NOT NULL,
|
||||
folder text NOT NULL,
|
||||
genre text,
|
||||
title text NOT NULL,
|
||||
track integer NOT NULL,
|
||||
trackhash text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS albums (
|
||||
id integer PRIMARY KEY,
|
||||
albumhash text NOT NULL,
|
||||
colors text NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS artists (
|
||||
id integer PRIMARY KEY,
|
||||
artisthash text NOT NULL,
|
||||
colors text,
|
||||
bio text
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
id integer PRIMARY KEY,
|
||||
path text NOT NULL,
|
||||
trackcount integer NOT NULL
|
||||
);
|
||||
"""
|
||||
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Contains the SQLiteTrackMethods class which contains methods for
|
||||
interacting with the tracks table.
|
||||
"""
|
||||
|
||||
|
||||
from sqlite3 import Cursor
|
||||
|
||||
from app.db.sqlite.utils import tuple_to_track, tuples_to_tracks
|
||||
|
||||
from .utils import SQLiteManager
|
||||
|
||||
|
||||
class SQLiteTrackMethods:
|
||||
"""
|
||||
This class contains all methods for interacting with the tracks table.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def insert_one_track(cls, track: dict, cur: Cursor):
|
||||
"""
|
||||
Inserts a single track into the database.
|
||||
"""
|
||||
sql = """INSERT INTO tracks(
|
||||
album,
|
||||
albumartist,
|
||||
albumhash,
|
||||
artist,
|
||||
bitrate,
|
||||
copyright,
|
||||
date,
|
||||
disc,
|
||||
duration,
|
||||
filepath,
|
||||
folder,
|
||||
genre,
|
||||
title,
|
||||
track,
|
||||
trackhash
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
"""
|
||||
|
||||
cur.execute(
|
||||
sql,
|
||||
(
|
||||
track["album"],
|
||||
track["albumartist"],
|
||||
track["albumhash"],
|
||||
track["artist"],
|
||||
track["bitrate"],
|
||||
track["copyright"],
|
||||
track["date"],
|
||||
track["disc"],
|
||||
track["duration"],
|
||||
track["filepath"],
|
||||
track["folder"],
|
||||
track["genre"],
|
||||
track["title"],
|
||||
track["track"],
|
||||
track["trackhash"],
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def insert_many_tracks(cls, tracks: list[dict]):
|
||||
"""
|
||||
Inserts a list of tracks into the database.
|
||||
"""
|
||||
with SQLiteManager() as cur:
|
||||
for track in tracks:
|
||||
cls.insert_one_track(track, cur)
|
||||
|
||||
@staticmethod
|
||||
def get_all_tracks():
|
||||
"""
|
||||
Get all tracks from the database and return a generator of Track objects
|
||||
or an empty list.
|
||||
"""
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute("SELECT * FROM tracks")
|
||||
rows = cur.fetchall()
|
||||
|
||||
if rows is not None:
|
||||
return tuples_to_tracks(rows)
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_track_by_trackhash(trackhash: str):
|
||||
"""
|
||||
Gets a track using its trackhash. Returns a Track object or None.
|
||||
"""
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute("SELECT * FROM tracks WHERE trackhash=?", (trackhash,))
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is not None:
|
||||
return tuple_to_track(row)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_tracks_by_trackhashes(hashes: list[str]):
|
||||
"""
|
||||
Gets all tracks in a list of trackhashes.
|
||||
Returns a generator of Track objects or an empty list.
|
||||
"""
|
||||
|
||||
sql = "SELECT * FROM tracks WHERE trackhash IN ({})".format(
|
||||
",".join("?" * len(hashes))
|
||||
)
|
||||
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute(sql, hashes)
|
||||
rows = cur.fetchall()
|
||||
|
||||
if rows is not None:
|
||||
return tuples_to_tracks(rows)
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def remove_track_by_filepath(filepath: str):
|
||||
"""
|
||||
Removes a track from the database using its filepath.
|
||||
"""
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute("DELETE FROM tracks WHERE filepath=?", (filepath,))
|
||||
|
||||
@staticmethod
|
||||
def track_exists(filepath: str):
|
||||
"""
|
||||
Checks if a track exists in the database using its filepath.
|
||||
"""
|
||||
with SQLiteManager() as cur:
|
||||
cur.execute("SELECT * FROM tracks WHERE filepath=?", (filepath,))
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is not None:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Helper functions for use with the SQLite database.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from sqlite3 import Connection, Cursor
|
||||
|
||||
from app.models import Album, Playlist, Track
|
||||
from app.settings import APP_DB_PATH, USERDATA_DB_PATH
|
||||
|
||||
|
||||
def tuple_to_track(track: tuple):
|
||||
"""
|
||||
Takes a tuple and returns a Track object
|
||||
"""
|
||||
return Track(*track[1:]) # rowid is removed from the tuple
|
||||
|
||||
|
||||
def tuples_to_tracks(tracks: list[tuple]):
|
||||
"""
|
||||
Takes a list of tuples and returns a generator that yields a Track object for each tuple
|
||||
"""
|
||||
for track in tracks:
|
||||
yield tuple_to_track(track)
|
||||
|
||||
|
||||
def tuple_to_album(album: tuple):
|
||||
"""
|
||||
Takes a tuple and returns an Album object
|
||||
"""
|
||||
return Album(*album[1:]) # rowid is removed from the tuple
|
||||
|
||||
|
||||
def tuples_to_albums(albums: list[tuple]):
|
||||
"""
|
||||
Takes a list of tuples and returns a generator that yields an album object for each tuple
|
||||
"""
|
||||
for album in albums:
|
||||
yield tuple_to_album(album)
|
||||
|
||||
|
||||
def tuple_to_playlist(playlist: tuple):
|
||||
"""
|
||||
Takes a tuple and returns a Playlist object
|
||||
"""
|
||||
return Playlist(*playlist)
|
||||
|
||||
|
||||
def tuples_to_playlists(playlists: list[tuple]):
|
||||
"""
|
||||
Takes a list of tuples and returns a list of Playlist objects
|
||||
"""
|
||||
for playlist in playlists:
|
||||
yield tuple_to_playlist(playlist)
|
||||
|
||||
|
||||
class SQLiteManager:
|
||||
"""
|
||||
This is a context manager that handles the connection and cursor
|
||||
for you. It also commits and closes the connection when you're done.
|
||||
"""
|
||||
|
||||
def __init__(self, conn: Connection | None = None, userdata_db=False) -> None:
|
||||
"""
|
||||
When a connection is passed in, don't close the connection, because it's
|
||||
a connection to the search database [in memory db].
|
||||
"""
|
||||
self.conn: Connection | None = conn
|
||||
self.CLOSE_CONN = True
|
||||
self.userdata_db = userdata_db
|
||||
|
||||
if conn:
|
||||
self.conn = conn
|
||||
self.CLOSE_CONN = False
|
||||
|
||||
def __enter__(self) -> Cursor:
|
||||
if self.conn is not None:
|
||||
return self.conn.cursor()
|
||||
|
||||
db_path = APP_DB_PATH
|
||||
|
||||
if self.userdata_db:
|
||||
db_path = USERDATA_DB_PATH
|
||||
|
||||
self.conn = sqlite3.connect(db_path)
|
||||
return self.conn.cursor()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
if self.conn:
|
||||
self.conn.commit()
|
||||
|
||||
if self.CLOSE_CONN:
|
||||
self.conn.close()
|
||||
+459
@@ -0,0 +1,459 @@
|
||||
"""
|
||||
In memory store.
|
||||
"""
|
||||
import json
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from app.db.sqlite.albums import SQLiteAlbumMethods as aldb
|
||||
from app.db.sqlite.artists import SQLiteArtistMethods as ardb
|
||||
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
|
||||
from app.db.sqlite.tracks import SQLiteTrackMethods as tdb
|
||||
from app.models import Album, Artist, Folder, Track
|
||||
from app.utils import (
|
||||
UseBisection,
|
||||
create_folder_hash,
|
||||
get_all_artists,
|
||||
remove_duplicates,
|
||||
)
|
||||
|
||||
|
||||
class Store:
|
||||
"""
|
||||
This class holds all tracks in memory and provides methods for
|
||||
interacting with them.
|
||||
"""
|
||||
|
||||
tracks: list[Track] = []
|
||||
folders: list[Folder] = []
|
||||
albums: list[Album] = []
|
||||
artists: list[Artist] = []
|
||||
|
||||
@classmethod
|
||||
def load_all_tracks(cls):
|
||||
"""
|
||||
Loads all tracks from the database into the store.
|
||||
"""
|
||||
|
||||
cls.tracks = list(tdb.get_all_tracks())
|
||||
|
||||
fav_hashes = favdb.get_fav_tracks()
|
||||
fav_hashes = [t[1] for t in fav_hashes]
|
||||
|
||||
for track in tqdm(cls.tracks, desc="Loading tracks"):
|
||||
if track.trackhash in fav_hashes:
|
||||
track.is_favorite = True
|
||||
|
||||
@classmethod
|
||||
def add_track(cls, track: Track):
|
||||
"""
|
||||
Adds a single track to the store.
|
||||
"""
|
||||
|
||||
cls.tracks.append(track)
|
||||
|
||||
@classmethod
|
||||
def add_tracks(cls, tracks: list[Track]):
|
||||
"""
|
||||
Adds multiple tracks to the store.
|
||||
"""
|
||||
|
||||
cls.tracks.extend(tracks)
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_trackhashes(cls, trackhashes: list[str]) -> list[Track]:
|
||||
"""
|
||||
Returns a list of tracks by their hashes.
|
||||
"""
|
||||
|
||||
tracks = []
|
||||
|
||||
for trackhash in trackhashes:
|
||||
for track in cls.tracks:
|
||||
if track.trackhash == trackhash:
|
||||
tracks.append(track)
|
||||
|
||||
return tracks
|
||||
|
||||
@classmethod
|
||||
def remove_track_by_filepath(cls, filepath: str):
|
||||
"""
|
||||
Removes a track from the store by its filepath.
|
||||
"""
|
||||
|
||||
for track in cls.tracks:
|
||||
if track.filepath == filepath:
|
||||
cls.tracks.remove(track)
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def count_tracks_by_hash(cls, trackhash: str) -> int:
|
||||
"""
|
||||
Counts the number of tracks with a specific hash.
|
||||
"""
|
||||
|
||||
count = 0
|
||||
|
||||
for track in cls.tracks:
|
||||
if track.trackhash == trackhash:
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
# ====================================================
|
||||
# =================== FAVORITES ======================
|
||||
# ====================================================
|
||||
|
||||
@classmethod
|
||||
def add_fav_track(cls, trackhash: str):
|
||||
"""
|
||||
Adds a track to the favorites.
|
||||
"""
|
||||
|
||||
for track in cls.tracks:
|
||||
if track.trackhash == trackhash:
|
||||
track.is_favorite = True
|
||||
|
||||
@classmethod
|
||||
def remove_fav_track(cls, trackhash: str):
|
||||
"""
|
||||
Removes a track from the favorites.
|
||||
"""
|
||||
|
||||
for track in cls.tracks:
|
||||
if track.trackhash == trackhash:
|
||||
track.is_favorite = False
|
||||
|
||||
# ====================================================
|
||||
# ==================== FOLDERS =======================
|
||||
# ====================================================
|
||||
|
||||
@classmethod
|
||||
def check_has_tracks(cls, path: str): # type: ignore
|
||||
"""
|
||||
Checks if a folder has tracks.
|
||||
"""
|
||||
path_hashes = "".join(f.path_hash for f in cls.folders)
|
||||
path_hash = create_folder_hash(*Path(path).parts[1:])
|
||||
|
||||
return path_hash in path_hashes
|
||||
|
||||
@classmethod
|
||||
def is_empty_folder(cls, path: str):
|
||||
"""
|
||||
Checks if a folder has tracks using tracks in the store.
|
||||
"""
|
||||
|
||||
all_folders = set(track.folder for track in cls.tracks)
|
||||
folder_hashes = "".join(
|
||||
create_folder_hash(*Path(f).parts[1:]) for f in all_folders
|
||||
)
|
||||
|
||||
path_hash = create_folder_hash(*Path(path).parts[1:])
|
||||
return path_hash in folder_hashes
|
||||
|
||||
@staticmethod
|
||||
def create_folder(path: str) -> Folder:
|
||||
"""
|
||||
Creates a folder object from a path.
|
||||
"""
|
||||
folder = Path(path)
|
||||
|
||||
return Folder(
|
||||
name=folder.name,
|
||||
path=str(folder),
|
||||
is_sym=folder.is_symlink(),
|
||||
has_tracks=True,
|
||||
path_hash=create_folder_hash(*folder.parts[1:]),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def add_folder(cls, path: str):
|
||||
"""
|
||||
Adds a folder to the store.
|
||||
"""
|
||||
|
||||
if cls.check_has_tracks(path):
|
||||
return
|
||||
|
||||
folder = cls.create_folder(path)
|
||||
cls.folders.append(folder)
|
||||
|
||||
@classmethod
|
||||
def remove_folder(cls, path: str):
|
||||
"""
|
||||
Removes a folder from the store.
|
||||
"""
|
||||
|
||||
for folder in cls.folders:
|
||||
if folder.path == path:
|
||||
cls.folders.remove(folder)
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def process_folders(cls):
|
||||
"""
|
||||
Creates a list of folders from the tracks in the store.
|
||||
"""
|
||||
all_folders = [track.folder for track in cls.tracks]
|
||||
all_folders = set(all_folders)
|
||||
|
||||
all_folders = [
|
||||
folder for folder in all_folders if not cls.check_has_tracks(folder)
|
||||
]
|
||||
|
||||
all_folders = [Path(f) for f in all_folders]
|
||||
all_folders = [f for f in all_folders if f.exists()]
|
||||
|
||||
for path in tqdm(all_folders, desc="Processing folders"):
|
||||
folder = cls.create_folder(str(path))
|
||||
|
||||
cls.folders.append(folder)
|
||||
|
||||
@classmethod
|
||||
def get_folder(cls, path: str): # type: ignore
|
||||
"""
|
||||
Returns a folder object by its path.
|
||||
"""
|
||||
folders = sorted(cls.folders, key=lambda x: x.path)
|
||||
folder = UseBisection(folders, "path", [path])()[0]
|
||||
|
||||
if folder is not None:
|
||||
return folder
|
||||
|
||||
has_tracks = cls.check_has_tracks(path)
|
||||
|
||||
if not has_tracks:
|
||||
return None
|
||||
|
||||
folder = cls.create_folder(path)
|
||||
cls.folders.append(folder)
|
||||
return folder
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_filepaths(cls, paths: list[str]) -> list[Track]:
|
||||
"""
|
||||
Returns all tracks matching the given paths.
|
||||
"""
|
||||
tracks = sorted(cls.tracks, key=lambda x: x.filepath)
|
||||
tracks = UseBisection(tracks, "filepath", paths)()
|
||||
return [track for track in tracks if track is not None]
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_albumhash(cls, album_hash: str) -> list[Track]:
|
||||
"""
|
||||
Returns all tracks matching the given album hash.
|
||||
"""
|
||||
return [t for t in cls.tracks if t.albumhash == album_hash]
|
||||
|
||||
@classmethod
|
||||
def get_tracks_by_artist(cls, artisthash: str) -> list[Track]:
|
||||
"""
|
||||
Returns all tracks matching the given artist. Duplicate tracks are removed.
|
||||
"""
|
||||
tracks = [t for t in cls.tracks if artisthash in t.artist_hashes]
|
||||
return remove_duplicates(tracks)
|
||||
|
||||
# ====================================================
|
||||
# ==================== ALBUMS ========================
|
||||
# ====================================================
|
||||
|
||||
@staticmethod
|
||||
def create_album(track: Track):
|
||||
"""
|
||||
Creates album object from a track
|
||||
"""
|
||||
return Album(
|
||||
albumhash=track.albumhash,
|
||||
albumartists=track.albumartist, # type: ignore
|
||||
title=track.album,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_albums(cls):
|
||||
"""
|
||||
Loads all albums from the database into the store.
|
||||
"""
|
||||
|
||||
albumhashes = set(t.albumhash for t in cls.tracks)
|
||||
|
||||
for albumhash in tqdm(albumhashes, desc="Loading albums"):
|
||||
for track in cls.tracks:
|
||||
if track.albumhash == albumhash:
|
||||
cls.albums.append(cls.create_album(track))
|
||||
break
|
||||
|
||||
db_albums: list[tuple] = aldb.get_all_albums()
|
||||
|
||||
for album in tqdm(db_albums, desc="Mapping album colors"):
|
||||
albumhash = album[1]
|
||||
colors = json.loads(album[2])
|
||||
|
||||
for al in cls.albums:
|
||||
if al.albumhash == albumhash:
|
||||
al.set_colors(colors)
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def add_album(cls, album: Album):
|
||||
"""
|
||||
Adds an album to the store.
|
||||
"""
|
||||
cls.albums.append(album)
|
||||
|
||||
@classmethod
|
||||
def add_albums(cls, albums: list[Album]):
|
||||
"""
|
||||
Adds multiple albums to the store.
|
||||
"""
|
||||
cls.albums.extend(albums)
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_albumartist(
|
||||
cls, artisthash: str, limit: int, exclude: str
|
||||
) -> list[Album]:
|
||||
"""
|
||||
Returns N albums by the given albumartist, excluding the specified album.
|
||||
"""
|
||||
|
||||
albums = [album for album in cls.albums if artisthash in album.albumartisthash]
|
||||
|
||||
albums = [album for album in albums if album.albumhash != exclude]
|
||||
|
||||
if len(albums) > limit:
|
||||
random.shuffle(albums)
|
||||
|
||||
# TODO: Merge this with `cls.get_albums_by_artisthash()`
|
||||
return albums[:limit]
|
||||
|
||||
@classmethod
|
||||
def get_album_by_hash(cls, albumhash: str) -> Album | None:
|
||||
"""
|
||||
Returns an album by its hash.
|
||||
"""
|
||||
try:
|
||||
return [a for a in cls.albums if a.albumhash == albumhash][0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_albums_by_artisthash(cls, artisthash: str) -> list[Album]:
|
||||
"""
|
||||
Returns all albums by the given artist.
|
||||
"""
|
||||
return [album for album in cls.albums if artisthash in album.albumartisthash]
|
||||
|
||||
@classmethod
|
||||
def count_albums_by_artisthash(cls, artisthash: str):
|
||||
"""
|
||||
Count albums for the given artisthash.
|
||||
"""
|
||||
albumartists = [a.albumartists for a in cls.albums]
|
||||
artisthashes = []
|
||||
|
||||
for artist in albumartists:
|
||||
artisthashes.extend([a.artisthash for a in artist]) # type: ignore
|
||||
|
||||
master_string = "-".join(artisthashes)
|
||||
|
||||
return master_string.count(artisthash)
|
||||
|
||||
@classmethod
|
||||
def album_exists(cls, albumhash: str) -> bool:
|
||||
"""
|
||||
Checks if an album exists.
|
||||
"""
|
||||
return albumhash in "-".join([a.albumhash for a in cls.albums])
|
||||
|
||||
@classmethod
|
||||
def remove_album_by_hash(cls, albumhash: str):
|
||||
"""
|
||||
Removes an album from the store.
|
||||
"""
|
||||
cls.albums = [a for a in cls.albums if a.albumhash != albumhash]
|
||||
|
||||
# ====================================================
|
||||
# ==================== ARTISTS =======================
|
||||
# ====================================================
|
||||
|
||||
@classmethod
|
||||
def load_artists(cls):
|
||||
"""
|
||||
Loads all artists from the database into the store.
|
||||
"""
|
||||
cls.artists = get_all_artists(cls.tracks, cls.albums)
|
||||
|
||||
db_artists: list[tuple] = list(ardb.get_all_artists())
|
||||
|
||||
for art in tqdm(db_artists, desc="Loading artists"):
|
||||
cls.map_artist_color(art)
|
||||
|
||||
@classmethod
|
||||
def map_artist_color(cls, artist_tuple: tuple):
|
||||
"""
|
||||
Maps a color to the corresponding artist.
|
||||
"""
|
||||
|
||||
artisthash = artist_tuple[1]
|
||||
color = json.loads(artist_tuple[2])
|
||||
|
||||
for artist in cls.artists:
|
||||
if artist.artisthash == artisthash:
|
||||
artist.colors = color
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def add_artist(cls, artist: Artist):
|
||||
"""
|
||||
Adds an artist to the store.
|
||||
"""
|
||||
cls.artists.append(artist)
|
||||
|
||||
@classmethod
|
||||
def add_artists(cls, artists: list[Artist]):
|
||||
"""
|
||||
Adds multiple artists to the store.
|
||||
"""
|
||||
for artist in artists:
|
||||
if artist not in cls.artists:
|
||||
cls.artists.append(artist)
|
||||
|
||||
@classmethod
|
||||
def get_artist_by_hash(cls, artisthash: str) -> Artist:
|
||||
"""
|
||||
Returns an artist by its hash.
|
||||
"""
|
||||
artists = sorted(cls.artists, key=lambda x: x.artisthash)
|
||||
artist = UseBisection(artists, "artisthash", [artisthash])()[0]
|
||||
return artist
|
||||
|
||||
@classmethod
|
||||
def artist_exists(cls, artisthash: str) -> bool:
|
||||
"""
|
||||
Checks if an artist exists.
|
||||
"""
|
||||
return artisthash in "-".join([a.artisthash for a in cls.artists])
|
||||
|
||||
@classmethod
|
||||
def artist_has_tracks(cls, artisthash: str) -> bool:
|
||||
"""
|
||||
Checks if an artist has tracks.
|
||||
"""
|
||||
artists: set[str] = set()
|
||||
|
||||
for track in cls.tracks:
|
||||
artists.update(track.artist_hashes)
|
||||
album_artists: list[str] = [a.artisthash for a in track.albumartist]
|
||||
artists.update(album_artists)
|
||||
|
||||
master_hash = "-".join(artists)
|
||||
return artisthash in master_hash
|
||||
|
||||
@classmethod
|
||||
def remove_artist_by_hash(cls, artisthash: str):
|
||||
"""
|
||||
Removes an artist from the store.
|
||||
"""
|
||||
cls.artists = [a for a in cls.artists if a.artisthash != artisthash]
|
||||
Reference in New Issue
Block a user