db: Add an Artist Table
This table allows us to work with Artist playlists that have a name and (optional) mbid. Note that we can insert multiple artists with the same name into the database as long as they have different mbids. Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
parent
d57509425b
commit
d3bdaaa063
|
@ -3,6 +3,7 @@
|
|||
import pathlib
|
||||
from gi.repository import GObject
|
||||
from typing import Generator
|
||||
from . import artists
|
||||
from . import connection
|
||||
from . import playlist
|
||||
from . import playlists
|
||||
|
@ -29,6 +30,7 @@ class Connection(connection.Connection):
|
|||
|
||||
self.settings = settings.Table(self)
|
||||
self.playlists = playlists.Table(self)
|
||||
self.artists = artists.Table(self)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the database connection."""
|
||||
|
@ -51,7 +53,7 @@ class Connection(connection.Connection):
|
|||
|
||||
def playlist_tables(self) -> Generator[playlist.Table, None, None]:
|
||||
"""Iterate over each playlist table."""
|
||||
for tbl in [self.playlists]:
|
||||
for tbl in [self.playlists, self.artists]:
|
||||
yield tbl
|
||||
|
||||
def set_active_playlist(self, plist: playlist.Playlist) -> None:
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
# Copyright 2022 (c) Anna Schumaker
|
||||
"""A custom Gio.ListModel for working with artists."""
|
||||
import sqlite3
|
||||
from gi.repository import GObject
|
||||
from .. import format
|
||||
from . import playlist
|
||||
|
||||
|
||||
class Artist(playlist.Playlist):
|
||||
"""Our custom Artist object."""
|
||||
|
||||
artistid = GObject.Property(type=int)
|
||||
mbid = GObject.Property(type=str)
|
||||
|
||||
@property
|
||||
def primary_key(self) -> int:
|
||||
"""Get the Artist primary key."""
|
||||
return self.artistid
|
||||
|
||||
|
||||
class Table(playlist.Table):
|
||||
"""Our Artist Table."""
|
||||
|
||||
def do_construct(self, **kwargs) -> Artist:
|
||||
"""Construct a new artist."""
|
||||
return Artist(**kwargs)
|
||||
|
||||
def do_get_sort_key(self, artist: Artist) -> tuple[tuple, bool, str]:
|
||||
"""Get a sort key for the requested Playlist."""
|
||||
return (format.sort_key(artist.name),
|
||||
len(artist.mbid) == 0,
|
||||
artist.mbid.casefold())
|
||||
|
||||
def do_sql_delete(self, artist: Artist) -> bool:
|
||||
"""Delete an artist."""
|
||||
return self.sql("DELETE FROM artists WHERE artistid=?",
|
||||
artist.artistid)
|
||||
|
||||
def do_sql_glob(self, glob: str) -> sqlite3.Cursor:
|
||||
"""Search for artists matching the search text."""
|
||||
return self.sql("""SELECT artistid FROM artists WHERE
|
||||
CASEFOLD(name) GLOB ?""", glob)
|
||||
|
||||
def do_sql_insert(self, name: str,
|
||||
mbid: str = "") -> sqlite3.Cursor | None:
|
||||
"""Create a new artist."""
|
||||
if cur := self.sql("INSERT INTO artists (name, mbid) VALUES (?, ?)",
|
||||
name, mbid):
|
||||
return self.sql("SELECT * FROM artists_view WHERE artistid=?",
|
||||
cur.lastrowid)
|
||||
|
||||
def do_sql_select_all(self) -> sqlite3.Cursor:
|
||||
"""Load artists from the database."""
|
||||
return self.sql("SELECT * FROM artists_view")
|
||||
|
||||
def do_sql_select_one(self, name: str | None = None,
|
||||
*, mbid: str = "") -> sqlite3.Cursor:
|
||||
"""Look up an artist by name and mbid."""
|
||||
where = "mbid=? AND CASEFOLD(name)=?" if name else "mbid=?"
|
||||
args = [mbid.lower(), name.casefold()] if name else [mbid.lower()]
|
||||
return self.sql(f"SELECT artistid FROM artists WHERE {where}", *args)
|
||||
|
||||
def do_sql_update(self, artist: Artist,
|
||||
column: str, newval) -> sqlite3.Cursor:
|
||||
"""Update an artist."""
|
||||
return self.sql(f"UPDATE artists SET {column}=? WHERE artistid=?",
|
||||
newval, artist.artistid)
|
|
@ -70,6 +70,43 @@ CREATE TRIGGER playlists_delete_trigger AFTER DELETE ON playlists
|
|||
DELETE FROM playlist_properties WHERE propertyid = OLD.propertyid;
|
||||
END;
|
||||
|
||||
|
||||
/*************************
|
||||
* *
|
||||
* Artists *
|
||||
* *
|
||||
*************************/
|
||||
|
||||
CREATE TABLE artists (
|
||||
artistid INTEGER PRIMARY KEY,
|
||||
propertyid INTEGER REFERENCES playlist_properties (propertyid)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
mbid TEXT NOT NULL DEFAULT "" COLLATE NOCASE,
|
||||
UNIQUE (name, mbid)
|
||||
);
|
||||
|
||||
CREATE VIEW artists_view AS
|
||||
SELECT artistid, propertyid, name, mbid, active
|
||||
FROM artists
|
||||
JOIN playlist_properties USING (propertyid);
|
||||
|
||||
|
||||
CREATE TRIGGER artists_insert_trigger AFTER INSERT ON artists
|
||||
BEGIN
|
||||
INSERT INTO playlist_properties (active) VALUES (False);
|
||||
UPDATE artists SET propertyid = last_insert_rowid(),
|
||||
mbid = LOWER(NEW.mbid)
|
||||
WHERE artistid = NEW.artistid;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER artists_delete_trigger AFTER DELETE ON artists
|
||||
BEGIN
|
||||
DELETE FROM playlist_properties WHERE propertyid = OLD.propertyid;
|
||||
END;
|
||||
|
||||
|
||||
/******************************************
|
||||
* *
|
||||
* Create Default Playlists *
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""Tests our artist Gio.ListModel."""
|
||||
import emmental.db
|
||||
import tests.util
|
||||
|
||||
|
||||
class TestArtistObject(tests.util.TestCase):
|
||||
"""Tests our artist object."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
tests.util.TestCase.setUp(self)
|
||||
self.table = self.sql.artists
|
||||
self.artist = emmental.db.artists.Artist(table=self.table,
|
||||
artistid=123, propertyid=456,
|
||||
name="Test Artist")
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the Artist is set up properly."""
|
||||
self.assertIsInstance(self.artist, emmental.db.playlist.Playlist)
|
||||
self.assertEqual(self.artist.table, self.table)
|
||||
self.assertEqual(self.artist.propertyid, 456)
|
||||
self.assertEqual(self.artist.artistid, 123)
|
||||
self.assertEqual(self.artist.primary_key, 123)
|
||||
self.assertEqual(self.artist.mbid, "")
|
||||
self.assertIsNone(self.artist.parent)
|
||||
|
||||
|
||||
class TestArtistTable(tests.util.TestCase):
|
||||
"""Tests our artist table."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
tests.util.TestCase.setUp(self)
|
||||
self.table = self.sql.artists
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the artist model is configured correctly."""
|
||||
self.assertIsInstance(self.table, emmental.db.playlist.Table)
|
||||
self.assertEqual(len(self.table), 0)
|
||||
|
||||
def test_construct(self):
|
||||
"""Test constructing a new artist playlist."""
|
||||
artist = self.table.construct(artistid=1, propertyid=1,
|
||||
name="Test Artist", mbid="ab-cd-ef")
|
||||
self.assertIsInstance(artist, emmental.db.artists.Artist)
|
||||
self.assertEqual(artist.table, self.table)
|
||||
self.assertEqual(artist.propertyid, 1)
|
||||
self.assertEqual(artist.artistid, 1)
|
||||
self.assertEqual(artist.name, "Test Artist")
|
||||
self.assertEqual(artist.mbid, "ab-cd-ef")
|
||||
self.assertFalse(artist.active)
|
||||
|
||||
def test_create(self):
|
||||
"""Test creating an artist playlist."""
|
||||
artist1 = self.table.create("Test Artist")
|
||||
self.assertIsInstance(artist1, emmental.db.artists.Artist)
|
||||
self.assertEqual(artist1.name, "Test Artist")
|
||||
self.assertEqual(artist1.mbid, "")
|
||||
self.assertEqual(self.table[0], artist1)
|
||||
|
||||
cur = self.sql("SELECT COUNT(name) FROM artists")
|
||||
self.assertEqual(cur.fetchone()["COUNT(name)"], 1)
|
||||
|
||||
row = self.sql("""SELECT COUNT(*) FROM playlist_properties
|
||||
WHERE propertyid=?""", artist1.propertyid).fetchone()
|
||||
self.assertEqual(row["COUNT(*)"], 1)
|
||||
|
||||
artist2 = self.table.create("Test Artist", mbid="AB-CD-EF")
|
||||
self.assertEqual(artist2.mbid, "ab-cd-ef")
|
||||
self.assertEqual(self.table[0], artist2)
|
||||
self.assertEqual(self.table[1], artist1)
|
||||
|
||||
cur = self.sql("SELECT COUNT(name) FROM artists")
|
||||
self.assertEqual(cur.fetchone()["COUNT(name)"], 2)
|
||||
|
||||
self.assertIsNone(self.table.create("Test Artist"))
|
||||
|
||||
def test_delete(self):
|
||||
"""Test deleting an artist playlist."""
|
||||
artist = self.table.create("Test Artist")
|
||||
self.assertTrue(artist.delete())
|
||||
self.assertIsNone(self.table.index(artist))
|
||||
|
||||
cur = self.sql("SELECT COUNT(name) FROM artists")
|
||||
self.assertEqual(cur.fetchone()["COUNT(name)"], 0)
|
||||
self.assertEqual(len(self.table), 0)
|
||||
self.assertIsNone(self.table.get_item(0))
|
||||
|
||||
row = self.sql("""SELECT COUNT(*) FROM playlist_properties
|
||||
WHERE propertyid=?""", artist.propertyid).fetchone()
|
||||
self.assertEqual(row["COUNT(*)"], 0)
|
||||
|
||||
self.assertFalse(artist.delete())
|
||||
|
||||
def test_filter(self):
|
||||
"""Test filtering an artist playlist."""
|
||||
self.table.create("Artist 1")
|
||||
self.table.create("Artist 2")
|
||||
|
||||
self.table.filter("*1", now=True)
|
||||
self.assertSetEqual(self.table.get_filter().keys, {1})
|
||||
self.table.filter("artist*", now=True)
|
||||
self.assertSetEqual(self.table.get_filter().keys, {1, 2})
|
||||
|
||||
def test_get_sort_key(self):
|
||||
"""Test the get_sort_key() function."""
|
||||
artist1 = self.table.create("Artist 1")
|
||||
artist2 = self.table.create("Artist 2", mbid="ab-cd-ef")
|
||||
|
||||
self.assertTupleEqual(self.table.get_sort_key(artist1),
|
||||
(("artist", "1"), True, ""))
|
||||
self.assertTupleEqual(self.table.get_sort_key(artist2),
|
||||
(("artist", "2"), False, "ab-cd-ef"))
|
||||
|
||||
def test_load(self):
|
||||
"""Test loading the artist table."""
|
||||
self.table.create("Artist 1")
|
||||
self.table.create("Artist 2", mbid="ab-cd-ef")
|
||||
|
||||
artists2 = emmental.db.artists.Table(self.sql)
|
||||
self.assertEqual(len(artists2), 0)
|
||||
|
||||
artists2.load(now=True)
|
||||
self.assertEqual(len(artists2), 2)
|
||||
|
||||
self.assertEqual(artists2.get_item(0).name, "Artist 1")
|
||||
self.assertEqual(artists2.get_item(0).mbid, "")
|
||||
|
||||
self.assertEqual(artists2.get_item(1).name, "Artist 2")
|
||||
self.assertEqual(artists2.get_item(1).mbid, "ab-cd-ef")
|
||||
|
||||
def test_lookup(self):
|
||||
"""Test looking up artist playlists."""
|
||||
artist1 = self.table.create("Test Artist")
|
||||
artist2 = self.table.create("Test Artist", mbid="ab-cd-ef")
|
||||
|
||||
self.assertEqual(self.table.lookup("Test Artist"), artist1)
|
||||
self.assertEqual(self.table.lookup("test artist"), artist1)
|
||||
self.assertEqual(self.table.lookup("Test Artist", mbid="ab-cd-ef"),
|
||||
artist2)
|
||||
self.assertEqual(self.table.lookup("test artist", mbid="AB-CD-EF"),
|
||||
artist2)
|
||||
self.assertIsNone(self.table.lookup("No Artist"))
|
||||
|
||||
self.assertEqual(self.table.lookup(mbid="ab-cd-ef"), artist2)
|
||||
self.assertEqual(self.table.lookup(mbid="AB-CD-EF"), artist2)
|
||||
self.assertIsNone(self.table.lookup(mbid="gh-ij-kl"))
|
||||
|
||||
def test_update(self):
|
||||
"""Test updating artist attributes."""
|
||||
artist = self.table.create("Test Artist")
|
||||
artist.active = True
|
||||
|
||||
row = self.sql("""SELECT active FROM playlist_properties
|
||||
WHERE propertyid=?""", artist.propertyid).fetchone()
|
||||
self.assertTrue(row["active"])
|
|
@ -41,9 +41,10 @@ class TestConnection(tests.util.TestCase):
|
|||
"""Check that the connection has pointers to our tables."""
|
||||
self.assertIsInstance(self.sql.settings, emmental.db.settings.Table)
|
||||
self.assertIsInstance(self.sql.playlists, emmental.db.playlists.Table)
|
||||
self.assertIsInstance(self.sql.artists, emmental.db.artists.Table)
|
||||
|
||||
self.assertListEqual([tbl for tbl in self.sql.playlist_tables()],
|
||||
[self.sql.playlists])
|
||||
[self.sql.playlists, self.sql.artists])
|
||||
|
||||
def test_load(self):
|
||||
"""Check that calling load() loads the tables."""
|
||||
|
|
Loading…
Reference in New Issue