Move server code to this repo (#95)

move server code to this repo
This commit is contained in:
Mungai Njoroge
2023-01-13 20:01:52 +03:00
committed by GitHub
parent dd257e919d
commit 198957bcae
318 changed files with 6259 additions and 16797 deletions
+47
View File
@@ -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())
+125
View File
@@ -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 []
+36
View File
@@ -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
+77
View File
@@ -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
+179
View File
@@ -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))
+65
View File
@@ -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
);
"""
+142
View File
@@ -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
+93
View File
@@ -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()