diff --git a/emmental/db/__init__.py b/emmental/db/__init__.py index ea117d2..350413c 100644 --- a/emmental/db/__init__.py +++ b/emmental/db/__init__.py @@ -6,6 +6,7 @@ from typing import Generator from . import albums from . import artists from . import connection +from . import genres from . import playlist from . import media from . import playlists @@ -35,6 +36,7 @@ class Connection(connection.Connection): self.artists = artists.Table(self) self.albums = albums.Table(self, queue=self.artists.queue) self.media = media.Table(self, queue=self.artists.queue) + self.genres = genres.Table(self) def close(self) -> None: """Close the database connection.""" @@ -57,7 +59,8 @@ class Connection(connection.Connection): def playlist_tables(self) -> Generator[playlist.Table, None, None]: """Iterate over each playlist table.""" - for tbl in [self.playlists, self.artists, self.albums, self.media]: + for tbl in [self.playlists, self.artists, self.albums, self.media, + self.genres]: yield tbl def set_active_playlist(self, plist: playlist.Playlist) -> None: diff --git a/emmental/db/emmental.sql b/emmental/db/emmental.sql index da018fa..9a1c17d 100644 --- a/emmental/db/emmental.sql +++ b/emmental/db/emmental.sql @@ -210,6 +210,38 @@ CREATE VIEW album_artist_view AS LEFT JOIN media USING (albumid); +/************************ + * * + * Genres * + * * + ************************/ + +CREATE TABLE genres ( + genreid INTEGER PRIMARY KEY, + propertyid INTEGER REFERENCES playlist_properties (propertyid) + ON DELETE CASCADE + ON UPDATE CASCADE, + name TEXT NOT NULL UNIQUE COLLATE NOCASE +); + +CREATE VIEW genres_view AS + SELECT genreid, propertyid, name, active + FROM genres + JOIN playlist_properties USING (propertyid); + +CREATE TRIGGER genres_insert_trigger AFTER INSERT ON genres + BEGIN + INSERT INTO playlist_properties (active) VALUES (False); + UPDATE genres SET propertyid = last_insert_rowid() + WHERE genreid = NEW.genreid; + END; + +CREATE TRIGGER genres_delete_trigger AFTER DELETE ON genres + BEGIN + DELETE FROM playlist_properties WHERE propertyid = OLD.propertyid; + END; + + /****************************************** * * * Create Default Playlists * diff --git a/emmental/db/genres.py b/emmental/db/genres.py new file mode 100644 index 0000000..4662986 --- /dev/null +++ b/emmental/db/genres.py @@ -0,0 +1,59 @@ +# Copyright 2022 (c) Anna Schumaker +"""A custom Gio.ListModel for genres.""" +import sqlite3 +from gi.repository import GObject +from .. import format +from . import playlist + + +class Genre(playlist.Playlist): + """Our custom Genre object representing a single genre.""" + + genreid = GObject.Property(type=int) + + @property + def primary_key(self) -> int: + """Get this Gener's primary key.""" + return self.genreid + + +class Table(playlist.Table): + """Our Genre Table.""" + + def do_construct(self, **kwargs) -> Genre: + """Construct a new Genre.""" + return Genre(**kwargs) + + def do_get_sort_key(self, genre: Genre) -> tuple[tuple[str], int]: + """Get a sort key for the Genre.""" + return (format.sort_key(genre.name), genre.genreid) + + def do_sql_delete(self, genre: Genre) -> sqlite3.Cursor: + """Delete a genre.""" + return self.sql("DELETE FROM genres WHERE genreid=?", genre.genreid) + + def do_sql_glob(self, glob: str) -> sqlite3.Cursor: + """Search for genres matching the search text.""" + return self.sql("""SELECT genreid FROM genres + WHERE CASEFOLD(name) GLOB ?""", glob) + + def do_sql_insert(self, name: str) -> sqlite3.Cursor | None: + """Create a new genre.""" + if cur := self.sql("INSERT INTO genres (name) VALUES (?)", name): + return self.sql("SELECT * FROM genres_view WHERE genreid=?", + cur.lastrowid) + + def do_sql_select_all(self) -> sqlite3.Cursor: + """Load genres from the database.""" + return self.sql("SELECT * FROM genres_view") + + def do_sql_select_one(self, name: str) -> sqlite3.Cursor: + """Look up a genre by name.""" + return self.sql("SELECT genreid FROM genres WHERE CASEFOLD(name)=?", + name.casefold()) + + def do_sql_update(self, genre: playlist.Playlist, + column: str, newval) -> sqlite3.Cursor: + """Update a genre.""" + return self.sql(f"UPDATE genres SET {column}=? WHERE genreid=?", + newval, genre.genreid) diff --git a/tests/db/test_db.py b/tests/db/test_db.py index 50844a4..dd0f243 100644 --- a/tests/db/test_db.py +++ b/tests/db/test_db.py @@ -44,13 +44,15 @@ class TestConnection(tests.util.TestCase): self.assertIsInstance(self.sql.artists, emmental.db.artists.Table) self.assertIsInstance(self.sql.albums, emmental.db.albums.Table) self.assertIsInstance(self.sql.media, emmental.db.media.Table) + self.assertIsInstance(self.sql.genres, emmental.db.genres.Table) self.assertEqual(self.sql.albums.queue, self.sql.artists.queue) self.assertEqual(self.sql.media.queue, self.sql.artists.queue) self.assertListEqual([tbl for tbl in self.sql.playlist_tables()], [self.sql.playlists, self.sql.artists, - self.sql.albums, self.sql.media]) + self.sql.albums, self.sql.media, + self.sql.genres]) def test_load(self): """Check that calling load() loads the tables.""" diff --git a/tests/db/test_genres.py b/tests/db/test_genres.py new file mode 100644 index 0000000..169155d --- /dev/null +++ b/tests/db/test_genres.py @@ -0,0 +1,125 @@ +# Copyright 2022 (c) Anna Schumaker +"""Tests our genre Gio.ListModel.""" +import emmental.db +import tests.util + + +class TestGenreObject(tests.util.TestCase): + """Tests our genre object.""" + + def setUp(self): + """Set up common variables.""" + tests.util.TestCase.setUp(self) + self.table = self.sql.genres + self.genre = emmental.db.genres.Genre(table=self.table, genreid=123, + propertyid=456, name="Genre") + + def test_init(self): + """Test that the Genre is set up properly.""" + self.assertIsInstance(self.genre, emmental.db.playlist.Playlist) + self.assertEqual(self.genre.table, self.table) + self.assertEqual(self.genre.propertyid, 456) + self.assertEqual(self.genre.genreid, 123) + self.assertEqual(self.genre.primary_key, 123) + self.assertIsNone(self.genre.parent) + + +class TestGenreTable(tests.util.TestCase): + """Tests our genre table.""" + + def setUp(self): + """Set up common variables.""" + tests.util.TestCase.setUp(self) + self.table = self.sql.genres + + def test_init(self): + """Test that the genre 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 genre playlist.""" + genre = self.table.construct(genreid=1, propertyid=1, name="Genre") + self.assertIsInstance(genre, emmental.db.genres.Genre) + self.assertEqual(genre.table, self.table) + self.assertEqual(genre.propertyid, 1) + self.assertEqual(genre.genreid, 1) + self.assertEqual(genre.name, "Genre") + self.assertFalse(genre.active) + + def test_create(self): + """Test creating a genre playlist.""" + genre = self.table.create("Test Genre") + self.assertIsInstance(genre, emmental.db.genres.Genre) + self.assertEqual(genre.name, "Test Genre") + + cur = self.sql("SELECT COUNT(name) FROM genres") + self.assertEqual(cur.fetchone()["COUNT(name)"], 1) + + row = self.sql("""SELECT COUNT(*) FROM playlist_properties + WHERE propertyid=?""", genre.propertyid).fetchone() + self.assertEqual(row["COUNT(*)"], 1) + + self.assertIsNone(self.table.create("Test Genre")) + + def test_delete(self): + """Test deleting a genre playlist.""" + genre = self.table.create("Test Genre") + self.assertTrue(genre.delete()) + self.assertIsNone(self.table.index(genre)) + + cur = self.sql("SELECT COUNT(name) FROM genres") + 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=?""", genre.propertyid).fetchone() + self.assertEqual(row["COUNT(*)"], 0) + + self.assertFalse(genre.delete()) + + def test_filter(self): + """Test filtering a genre playlist.""" + self.table.create("Genre 1") + self.table.create("Genre 2") + + self.table.filter("*1", now=True) + self.assertSetEqual(self.table.get_filter().keys, {1}) + self.table.filter("genre*", now=True) + self.assertSetEqual(self.table.get_filter().keys, {1, 2}) + + def test_get_sort_key(self): + """Test the get_sort_key() function.""" + genre = self.table.create("Genre 1") + self.assertTupleEqual(self.table.get_sort_key(genre), + (("genre", "1"), genre.genreid)) + + def test_load(self): + """Test loading genres from the database.""" + self.table.create("Genre 1") + self.table.create("Genre 2") + + genres2 = emmental.db.genres.Table(self.sql) + self.assertEqual(len(genres2), 0) + + genres2.load(now=True) + self.assertEqual(len(genres2), 2) + self.assertEqual(genres2.get_item(0).name, "Genre 1") + self.assertEqual(genres2.get_item(1).name, "Genre 2") + + def test_lookup(self): + """Test looking up genre playlists.""" + genre = self.table.create("Test Genre") + self.assertEqual(self.table.lookup("Test Genre"), genre) + self.assertEqual(self.table.lookup("test genre"), genre) + self.assertIsNone(self.table.lookup("No Genre")) + + def test_update(self): + """Test updating genre attributes.""" + genre = self.table.create("Test Genre") + genre.active = True + + row = self.sql("""SELECT active FROM playlist_properties + WHERE propertyid=?""", genre.propertyid).fetchone() + self.assertEqual(row["active"], True)