Commit 8e925e01 authored by Hugo Beauzée-Luyssen's avatar Hugo Beauzée-Luyssen

Genre: Store & expose the number of tracks of a given genre

Automatically delete genres with no tracks
parent 6d392f18
......@@ -35,6 +35,7 @@ public:
virtual ~IGenre() = default;
virtual int64_t id() const = 0;
virtual const std::string& name() const = 0;
virtual uint32_t nbTracks() const = 0;
virtual std::vector<ArtistPtr> artists( SortingCriteria sort = SortingCriteria::Default, bool desc = false ) const = 0;
virtual std::vector<MediaPtr> tracks( SortingCriteria sort = SortingCriteria::Default, bool desc = false ) const = 0;
virtual std::vector<AlbumPtr> albums( SortingCriteria sort = SortingCriteria::Default, bool desc = false ) const = 0;
......
......@@ -239,6 +239,8 @@ std::shared_ptr<AlbumTrack> Album::addTrack( std::shared_ptr<Media> media, unsig
if ( track == nullptr )
return nullptr;
media->setAlbumTrack( track );
if ( genre != nullptr )
genre->updateCachedNbTracks( 1 );
// Assume the media will be saved by the caller
m_nbTracks++;
if ( media->duration() > 0 )
......
......@@ -197,15 +197,33 @@ GenrePtr AlbumTrack::genre()
bool AlbumTrack::setGenre( std::shared_ptr<Genre> genre )
{
// We need to fetch the old genre entity now, in case it gets deleted through
// the nbTracks reaching 0 trigger.
if ( m_genreId > 0 )
{
auto l = m_genre.lock();
if ( m_genre.isCached() == false )
m_genre = Genre::fetch( m_ml, m_genreId );
}
static const std::string req = "UPDATE " + policy::AlbumTrackTable::Name
+ " SET genre_id = ? WHERE id_track = ?";
if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req,
sqlite::ForeignKey( genre != nullptr ? genre->id() : 0 ),
m_id ) == false )
return false;
auto l = m_genre.lock();
m_genre = genre;
m_genreId = genre != nullptr ? genre->id() : 0;
{
auto l = m_genre.lock();
if ( m_genreId > 0 )
m_genre.get()->updateCachedNbTracks( -1 );
m_genre = genre;
}
if ( genre != nullptr )
{
genre->updateCachedNbTracks( 1 );
m_genreId = genre->id();
}
else
m_genreId = 0;
return true;
}
......
......@@ -44,13 +44,15 @@ Genre::Genre( MediaLibraryPtr ml, sqlite::Row& row )
: m_ml( ml )
{
row >> m_id
>> m_name;
>> m_name
>> m_nbTracks;
}
Genre::Genre( MediaLibraryPtr ml, const std::string& name )
: m_ml( ml )
, m_id( 0 )
, m_name( name )
, m_nbTracks( 0 )
{
}
......@@ -64,6 +66,16 @@ const std::string& Genre::name() const
return m_name;
}
uint32_t Genre::nbTracks() const
{
return m_nbTracks;
}
void Genre::updateCachedNbTracks( int increment )
{
m_nbTracks += increment;
}
std::vector<ArtistPtr> Genre::artists( SortingCriteria, bool desc ) const
{
std::string req = "SELECT a.* FROM " + policy::ArtistTable::Name + " a "
......@@ -90,7 +102,8 @@ bool Genre::createTable( DBConnection dbConn )
const std::string req = "CREATE TABLE IF NOT EXISTS " + policy::GenreTable::Name +
"("
"id_genre INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT UNIQUE ON CONFLICT FAIL"
"name TEXT UNIQUE ON CONFLICT FAIL,"
"nb_tracks INTEGER NOT NULL DEFAULT 0"
")";
const std::string vtableReq = "CREATE VIRTUAL TABLE IF NOT EXISTS "
+ policy::GenreTable::Name + "Fts USING FTS3("
......@@ -113,6 +126,34 @@ bool Genre::createTable( DBConnection dbConn )
sqlite::Tools::executeRequest( dbConn, vtableDeleteTrigger );
}
bool Genre::createTriggers( DBConnection dbConn )
{
const std::string onGenreChanged = "CREATE TRIGGER IF NOT EXISTS on_track_genre_changed AFTER UPDATE OF "
" genre_id ON " + policy::AlbumTrackTable::Name +
" BEGIN"
" UPDATE " + policy::GenreTable::Name + " SET nb_tracks = nb_tracks + 1 WHERE id_genre = new.genre_id;"
" UPDATE " + policy::GenreTable::Name + " SET nb_tracks = nb_tracks - 1 WHERE id_genre = old.genre_id;"
" DELETE FROM " + policy::GenreTable::Name + " WHERE nb_tracks = 0;"
" END";
const std::string onTrackCreated = "CREATE TRIGGER IF NOT EXISTS update_genre_on_new_track"
" AFTER INSERT ON " + policy::AlbumTrackTable::Name +
" WHEN new.genre_id IS NOT NULL"
" BEGIN"
" UPDATE " + policy::GenreTable::Name + " SET nb_tracks = nb_tracks + 1 WHERE id_genre = new.genre_id;"
" END";
const std::string onTrackDeleted = "CREATE TRIGGER IF NOT EXISTS update_genre_on_track_deleted"
" AFTER DELETE ON " + policy::AlbumTrackTable::Name +
" WHEN old.genre_id IS NOT NULL"
" BEGIN"
" UPDATE " + policy::GenreTable::Name + " SET nb_tracks = nb_tracks - 1 WHERE id_genre = old.genre_id;"
" DELETE FROM " + policy::GenreTable::Name + " WHERE nb_tracks = 0;"
" END";
return sqlite::Tools::executeRequest( dbConn, onGenreChanged ) &&
sqlite::Tools::executeRequest( dbConn, onTrackCreated ) &&
sqlite::Tools::executeRequest( dbConn, onTrackDeleted );
}
std::shared_ptr<Genre> Genre::create( MediaLibraryPtr ml, const std::string& name )
{
static const std::string req = "INSERT INTO " + policy::GenreTable::Name + "(name)"
......
......@@ -48,11 +48,14 @@ public:
Genre( MediaLibraryPtr ml, const std::string& name );
virtual int64_t id() const override;
virtual const std::string& name() const override;
virtual uint32_t nbTracks() const override;
void updateCachedNbTracks( int increment );
virtual std::vector<ArtistPtr> artists( SortingCriteria sort, bool desc ) const override;
virtual std::vector<MediaPtr> tracks(SortingCriteria sort, bool desc) const override;
virtual std::vector<AlbumPtr> albums( SortingCriteria sort, bool desc ) const override;
static bool createTable( DBConnection dbConn );
static bool createTriggers( DBConnection dbConn );
static std::shared_ptr<Genre> create( MediaLibraryPtr ml, const std::string& name );
static std::shared_ptr<Genre> fromName( MediaLibraryPtr ml, const std::string& name );
static std::vector<GenrePtr> search( MediaLibraryPtr ml, const std::string& name );
......@@ -63,6 +66,7 @@ private:
int64_t m_id;
std::string m_name;
uint32_t m_nbTracks;
friend policy::GenreTable;
};
......
......@@ -150,6 +150,7 @@ bool MediaLibrary::createAllTables()
Artist::createDefaultArtists( m_dbConnection.get() ) &&
Artist::createTriggers( m_dbConnection.get() ) &&
Media::createTriggers( m_dbConnection.get() ) &&
Genre::createTriggers( m_dbConnection.get() ) &&
Playlist::createTriggers( m_dbConnection.get() ) &&
History::createTable( m_dbConnection.get() ) &&
Settings::createTable( m_dbConnection.get() );
......
......@@ -200,3 +200,37 @@ TEST_F( Genres, Sort )
ASSERT_EQ( g->id(), genres[1]->id() );
ASSERT_EQ( g2->id(), genres[0]->id() );
}
TEST_F( Genres, NbTracks )
{
ASSERT_EQ( 0u, g->nbTracks() );
auto a = ml->createAlbum( "album" );
auto m = std::static_pointer_cast<Media>( ml->addMedia( "track.mp3" ) );
auto t = a->addTrack( m, 1, 1, g->id(), g.get() );
ASSERT_EQ( 1u, g->nbTracks() );
Reload();
g = std::static_pointer_cast<Genre>( ml->genre( g->id() ) );
ASSERT_EQ( 1u, g->nbTracks() );
auto g2 = ml->createGenre( "otter metal" );
t = std::static_pointer_cast<AlbumTrack>( ml->albumTrack( t->id() ) );
t->setGenre( g2 );
ASSERT_EQ( 1u, g2->nbTracks() );
// check that the cache got updated, check for DB deletion afterward
ASSERT_EQ( 0u, g->nbTracks() );
Reload();
g = std::static_pointer_cast<Genre>( ml->genre( g->id() ) );
g2 = std::static_pointer_cast<Genre>( ml->genre( g2->id() ) );
ASSERT_EQ( nullptr, g );
ASSERT_EQ( 1u, g2->nbTracks() );
ml->deleteTrack( t->id() );
Reload();
g2 = std::static_pointer_cast<Genre>( ml->genre( g2->id() ) );
ASSERT_EQ( nullptr, g2 );
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment