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
+214
View File
@@ -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
+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()
+459
View File
@@ -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]