Artist.cpp 17.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/*****************************************************************************
 * Media Library
 *****************************************************************************
 * Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
 *
 * Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

23 24 25 26
#if HAVE_CONFIG_H
# include "config.h"
#endif

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
27
#include "Artist.h"
28
#include "Album.h"
29
#include "AlbumTrack.h"
30
#include "Media.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
31 32

#include "database/SqliteTools.h"
33
#include "database/SqliteQuery.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
34

35 36 37
namespace medialibrary
{

38
const std::string policy::ArtistTable::Name = "Artist";
39
const std::string policy::ArtistTable::PrimaryKeyColumn = "id_artist";
40
int64_t Artist::*const policy::ArtistTable::PrimaryKey = &Artist::m_id;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
41

42 43
Artist::Artist( MediaLibraryPtr ml, sqlite::Row& row )
    : m_ml( ml )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
44
{
45 46
    row >> m_id
        >> m_name
47
        >> m_shortBio
48
        >> m_thumbnailId
49
        >> m_nbAlbums
50
        >> m_nbTracks
51
        >> m_mbId
52
        >> m_isPresent;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
53 54
}

55 56 57
Artist::Artist( MediaLibraryPtr ml, const std::string& name )
    : m_ml( ml )
    , m_id( 0 )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
58
    , m_name( name )
59
    , m_thumbnailId( 0 )
60
    , m_nbAlbums( 0 )
61
    , m_nbTracks( 0 )
62
    , m_isPresent( true )
63 64 65
{
}

66
int64_t Artist::id() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
67 68 69 70
{
    return m_id;
}

71
const std::string& Artist::name() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
72 73 74 75
{
    return m_name;
}

76
const std::string& Artist::shortBio() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
77 78 79 80
{
    return m_shortBio;
}

81
bool Artist::setShortBio(const std::string& shortBio)
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
82 83 84
{
    static const std::string req = "UPDATE " + policy::ArtistTable::Name
            + " SET shortbio = ? WHERE id_artist = ?";
85
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, shortBio, m_id ) == false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
86 87 88 89 90
        return false;
    m_shortBio = shortBio;
    return true;
}

91
Query<IAlbum> Artist::albums( const QueryParameters* params ) const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
92
{
93
    return Album::fromArtist( m_ml, m_id, params );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
94 95
}

96 97 98 99 100 101
Query<IAlbum> Artist::searchAlbums( const std::string& pattern,
                                    const QueryParameters* params ) const
{
    return Album::searchFromArtist( m_ml, pattern, m_id, params );
}

102
Query<IMedia> Artist::tracks( const QueryParameters* params ) const
103
{
104
    std::string req = "FROM " + policy::MediaTable::Name + " med ";
105

106 107
    SortingCriteria sort = params != nullptr ? params->sort : SortingCriteria::Default;
    bool desc = params != nullptr ? params->desc : false;
108 109 110 111 112 113
    // Various artist is a special artist that doesn't have tracks per-se.
    // Rather, it's a virtual artist for albums with many artist but no declared
    // album artist. When listing its tracks, we need to list those by albums
    // instead of listing all tracks by this artist, as there will be none.
    if ( m_id != VariousArtistID )
    {
114 115 116 117
        req += "INNER JOIN MediaArtistRelation mar ON mar.media_id = med.id_media ";
        if ( sort == SortingCriteria::Album )
            req += "INNER JOIN AlbumTrack atr ON atr.media_id = med.id_media ";
        req += "WHERE mar.artist_id = ? ";
118 119 120 121 122 123 124 125 126
    }
    else
    {
        req += "INNER JOIN AlbumTrack atr ON atr.media_id = med.id_media "
               "INNER JOIN Album alb ON alb.id_album = atr.album_id "
               "WHERE alb.artist_id = ? ";
    }

    req += "AND med.is_present != 0 ORDER BY ";
127 128
    switch ( sort )
    {
129
    case SortingCriteria::Duration:
130 131
        req += "med.duration";
        break;
132
    case SortingCriteria::InsertionDate:
133 134
        req += "med.insertion_date";
        break;
135
    case SortingCriteria::ReleaseDate:
136 137
        req += "med.release_date";
        break;
138 139
    case SortingCriteria::Album:
        if ( desc == true )
140
            req += "atr.album_id DESC, atr.disc_number DESC, atr.track_number";
141
        else
142
            req += "atr.album_id, atr.disc_number, atr.track_number";
143
        break;
144 145 146 147 148 149 150
    default:
        req += "med.title";
        break;
    }

    if ( desc == true )
        req += " DESC";
151
    return make_query<Media, IMedia>( m_ml, "med.*", std::move( req ), m_id );
152 153
}

154 155 156 157 158
Query<IMedia> Artist::searchTracks( const std::string& pattern, const QueryParameters* params ) const
{
    return Media::searchArtistTracks( m_ml, pattern, m_id, params );
}

159
bool Artist::addMedia( Media& media )
160 161 162 163
{
    static const std::string req = "INSERT INTO MediaArtistRelation VALUES(?, ?)";
    // If track's ID is 0, the request will fail due to table constraints
    sqlite::ForeignKey artistForeignKey( m_id );
164
    return sqlite::Tools::executeInsert( m_ml->getConn(), req, media.id(), artistForeignKey ) != 0;
165 166
}

167
const std::string& Artist::artworkMrl() const
168
{
169 170 171 172 173 174 175 176 177 178 179 180
    if ( m_thumbnailId == 0 )
        return Thumbnail::EmptyMrl;

    auto lock = m_thumbnail.lock();
    if ( m_thumbnail.isCached() == false )
    {
        auto thumbnail = Thumbnail::fetch( m_ml, m_thumbnailId );
        if ( thumbnail == nullptr )
            return Thumbnail::EmptyMrl;
        m_thumbnail = std::move( thumbnail );
    }
    return m_thumbnail.get()->mrl();
181 182
}

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
std::shared_ptr<Thumbnail> Artist::thumbnail()
{
    if ( m_thumbnailId == 0 )
        return nullptr;
    auto lock = m_thumbnail.lock();
    if ( m_thumbnail.isCached() == false )
    {
        auto thumbnail = Thumbnail::fetch( m_ml, m_thumbnailId );
        if ( thumbnail == nullptr )
            return nullptr;
        m_thumbnail = std::move( thumbnail );
    }
    return m_thumbnail.get();
}

198
bool Artist::setArtworkMrl( const std::string& artworkMrl, Thumbnail::Origin origin )
199
{
200 201
    if ( m_thumbnailId != 0 )
        return Thumbnail::setMrlFromPrimaryKey( m_ml, m_thumbnail, m_thumbnailId,
202
                                                artworkMrl, origin );
203 204 205 206 207 208 209 210

    std::unique_ptr<sqlite::Transaction> t;
    if ( sqlite::Transaction::transactionInProgress() == false )
        t = m_ml->getConn()->newTransaction();
    auto lock = m_thumbnail.lock();
    m_thumbnail = Thumbnail::create( m_ml, artworkMrl, Thumbnail::Origin::Artist );
    if ( m_thumbnail.get() == nullptr )
        return false;
211
    static const std::string req = "UPDATE " + policy::ArtistTable::Name +
212 213
            " SET thumbnail_id = ? WHERE id_artist = ?";
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, m_thumbnail.get()->id(), m_id ) == false )
214
        return false;
215 216 217
    m_thumbnailId = m_thumbnail.get()->id();
    if ( t != nullptr )
        t->commit();
218 219 220
    return true;
}

221
bool Artist::updateNbAlbum( int increment )
222
{
223 224 225
    assert( increment != 0 );
    assert( increment > 0 || ( increment < 0 && m_nbAlbums >= 1 ) );

226
    static const std::string req = "UPDATE " + policy::ArtistTable::Name +
227
            " SET nb_albums = nb_albums + ? WHERE id_artist = ?";
228
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, increment, m_id ) == false )
229
        return false;
230
    m_nbAlbums += increment;
231 232 233
    return true;
}

234 235 236 237 238 239 240 241 242 243 244 245
bool Artist::updateNbTrack(int increment)
{
    assert( increment != 0 );
    assert( increment > 0 || ( increment < 0 && m_nbTracks >= 1 ) );
    static const std::string req = "UPDATE " + policy::ArtistTable::Name +
            " SET nb_tracks = nb_tracks + ? WHERE id_artist = ?";
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, increment, m_id ) == false )
        return false;
    m_nbTracks += increment;
    return true;
}

246 247 248 249
std::shared_ptr<Album> Artist::unknownAlbum()
{
    static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
                        " WHERE artist_id = ? AND title IS NULL";
250
    auto album = Album::fetch( m_ml, req, m_id );
251 252
    if ( album == nullptr )
    {
253
        album = Album::createUnknownAlbum( m_ml, this );
254 255 256 257
        if ( album == nullptr )
            return nullptr;
        if ( updateNbAlbum( 1 ) == false )
        {
258
            Album::destroy( m_ml, album->id() );
259 260
            return nullptr;
        }
261 262 263 264
    }
    return album;
}

265 266 267 268 269 270 271 272 273 274 275
const std::string& Artist::musicBrainzId() const
{
    return m_mbId;
}

bool Artist::setMusicBrainzId( const std::string& mbId )
{
    static const std::string req = "UPDATE " + policy::ArtistTable::Name
            + " SET mb_id = ? WHERE id_artist = ?";
    if ( mbId == m_mbId )
        return true;
276
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, mbId, m_id ) == false )
277 278 279 280 281
        return false;
    m_mbId = mbId;
    return true;
}

282 283 284 285 286
unsigned int Artist::nbAlbums() const
{
    return m_nbAlbums;
}

287 288 289 290 291
unsigned int Artist::nbTracks() const
{
    return m_nbTracks;
}

292
void Artist::createTable( sqlite::Connection* dbConnection )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
293
{
294
    const std::string req = "CREATE TABLE IF NOT EXISTS " +
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
295 296 297
            policy::ArtistTable::Name +
            "("
                "id_artist INTEGER PRIMARY KEY AUTOINCREMENT,"
298
                "name TEXT COLLATE NOCASE UNIQUE ON CONFLICT FAIL,"
299
                "shortbio TEXT,"
300
                "thumbnail_id TEXT,"
301
                "nb_albums UNSIGNED INT DEFAULT 0,"
302
                "nb_tracks UNSIGNED INT DEFAULT 0,"
303
                "mb_id TEXT,"
304 305 306
                "is_present BOOLEAN NOT NULL DEFAULT 1,"
                "FOREIGN KEY(thumbnail_id) REFERENCES " + policy::ThumbnailTable::Name
                + "(id_thumbnail)"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
307
            ")";
308
    const std::string reqRel = "CREATE TABLE IF NOT EXISTS MediaArtistRelation("
309 310 311 312
                "media_id INTEGER NOT NULL,"
                "artist_id INTEGER,"
                "PRIMARY KEY (media_id, artist_id),"
                "FOREIGN KEY(media_id) REFERENCES " + policy::MediaTable::Name +
313
                "(id_media) ON DELETE CASCADE,"
314
                "FOREIGN KEY(artist_id) REFERENCES " + policy::ArtistTable::Name + "("
315
                    + policy::ArtistTable::PrimaryKeyColumn + ") ON DELETE CASCADE"
316
            ")";
317
    const std::string reqFts = "CREATE VIRTUAL TABLE IF NOT EXISTS " +
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
318 319 320
                policy::ArtistTable::Name + "Fts USING FTS3("
                "name"
            ")";
321 322 323
    sqlite::Tools::executeRequest( dbConnection, req );
    sqlite::Tools::executeRequest( dbConnection, reqRel );
    sqlite::Tools::executeRequest( dbConnection, reqFts );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
324 325
}

326
void Artist::createTriggers( sqlite::Connection* dbConnection, uint32_t dbModelVersion )
327 328 329 330 331
{
    static const std::string triggerReq = "CREATE TRIGGER IF NOT EXISTS has_album_present AFTER UPDATE OF "
            "is_present ON " + policy::AlbumTable::Name +
            " BEGIN "
            " UPDATE " + policy::ArtistTable::Name + " SET is_present="
332 333
                "(SELECT EXISTS("
                    "SELECT id_album FROM " + policy::AlbumTable::Name +
334
                    " WHERE artist_id=new.artist_id AND is_present != 0 LIMIT 1"
335
                ") )"
336 337
                "WHERE id_artist=new.artist_id;"
            " END";
338 339 340 341 342
    // Automatically delete the artists that don't have any albums left, except the 2 special artists.
    // Those are assumed to always exist, and deleting them would cause a constaint violation error
    // when inserting an album with unknown/various artist(s).
    // The alternative would be to always check the special artists for existence, which would be much
    // slower when inserting an unknown artist album
343
    static const std::string autoDeleteAlbumTriggerReq = "CREATE TRIGGER IF NOT EXISTS has_album_remaining"
344 345 346
            " AFTER DELETE ON " + policy::AlbumTable::Name +
            " BEGIN"
            " UPDATE " + policy::ArtistTable::Name + " SET nb_albums = nb_albums - 1 WHERE id_artist = old.artist_id;"
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
            " DELETE FROM " + policy::ArtistTable::Name + " WHERE id_artist = old.artist_id "
            " AND nb_albums = 0 "
            " AND nb_tracks = 0 "
            " AND old.artist_id != " + std::to_string( UnknownArtistID ) +
            " AND old.artist_id != " + std::to_string( VariousArtistID ) + ";"
            " END";

    static const std::string autoDeleteTrackTriggerReq = "CREATE TRIGGER IF NOT EXISTS has_track_remaining"
            " AFTER DELETE ON " + policy::AlbumTrackTable::Name +
            " BEGIN"
            " UPDATE " + policy::ArtistTable::Name + " SET nb_tracks = nb_tracks - 1 WHERE id_artist = old.artist_id;"
            " DELETE FROM " + policy::ArtistTable::Name + " WHERE id_artist = old.artist_id "
            " AND nb_albums = 0 "
            " AND nb_tracks = 0 "
            " AND old.artist_id != " + std::to_string( UnknownArtistID ) +
362
            " AND old.artist_id != " + std::to_string( VariousArtistID ) + ";"
363 364
            " END";

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
365 366
    static const std::string ftsInsertTrigger = "CREATE TRIGGER IF NOT EXISTS insert_artist_fts"
            " AFTER INSERT ON " + policy::ArtistTable::Name +
367
            " WHEN new.name IS NOT NULL"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
368 369 370 371 372
            " BEGIN"
            " INSERT INTO " + policy::ArtistTable::Name + "Fts(rowid,name) VALUES(new.id_artist, new.name);"
            " END";
    static const std::string ftsDeleteTrigger = "CREATE TRIGGER IF NOT EXISTS delete_artist_fts"
            " BEFORE DELETE ON " + policy::ArtistTable::Name +
373
            " WHEN old.name IS NOT NULL"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
374 375 376
            " BEGIN"
            " DELETE FROM " + policy::ArtistTable::Name + "Fts WHERE rowid=old.id_artist;"
            " END";
377
    sqlite::Tools::executeRequest( dbConnection, triggerReq );
378
    sqlite::Tools::executeRequest( dbConnection, autoDeleteAlbumTriggerReq );
379 380 381 382 383 384 385 386 387 388 389
    // Don't create this trigger if the database is about to be migrated.
    // This could make earlier migration fail, and needs to be done when
    // migrating to v7 to v8.
    // While the has_album_remaining trigger now also references the nb_tracks
    // field, it was present from before version 3, so it wouldn't be recreated.
    // As we don't support any model before 3 (or rather we just recreate
    // everything), we don't have to bother here.
    if ( dbModelVersion >= 8 )
    {
        sqlite::Tools::executeRequest( dbConnection, autoDeleteTrackTriggerReq );
    }
390 391
    sqlite::Tools::executeRequest( dbConnection, ftsInsertTrigger );
    sqlite::Tools::executeRequest( dbConnection, ftsDeleteTrigger );
392 393
}

394
bool Artist::createDefaultArtists( sqlite::Connection* dbConnection )
395 396 397 398 399
{
    // Don't rely on Artist::create, since we want to insert or do nothing here.
    // This will skip the cache for those new entities, but they will be inserted soon enough anyway.
    static const std::string req = "INSERT OR IGNORE INTO " + policy::ArtistTable::Name +
            "(id_artist) VALUES(?),(?)";
400 401
    sqlite::Tools::executeInsert( dbConnection, req, UnknownArtistID,
                                          VariousArtistID );
402 403 404
    // Always return true. The insertion might succeed, but we consider it a failure when 0 row
    // gets inserted, while we are explicitely specifying "OR IGNORE" here.
    return true;
405 406
}

407
std::shared_ptr<Artist> Artist::create( MediaLibraryPtr ml, const std::string& name )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
408
{
409
    auto artist = std::make_shared<Artist>( ml, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
410 411
    static const std::string req = "INSERT INTO " + policy::ArtistTable::Name +
            "(id_artist, name) VALUES(NULL, ?)";
412
    if ( insert( ml, artist, req, name ) == false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
413 414 415 416
        return nullptr;
    return artist;
}

417
Query<IArtist> Artist::search( MediaLibraryPtr ml, const std::string& name,
418
                               const QueryParameters* params )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
419
{
420
    std::string req = "FROM " + policy::ArtistTable::Name + " WHERE id_artist IN "
421
            "(SELECT rowid FROM " + policy::ArtistTable::Name + "Fts WHERE name MATCH '*' || ? || '*')"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
422
            "AND is_present != 0";
423
    req += sortRequest( params );
424
    return make_query<Artist, IArtist>( ml, "*", std::move( req ), name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
425 426
}

427
Query<IArtist> Artist::listAll( MediaLibraryPtr ml, bool includeAll,
428
                                const QueryParameters* params )
429
{
430
    std::string req = "FROM " + policy::ArtistTable::Name + " WHERE ";
431 432 433 434 435
    if ( includeAll == true )
        req += "( nb_albums > 0 OR nb_tracks > 0 )";
    else
        req += "nb_albums > 0";

436
    req += " AND is_present != 0";
437
    req += sortRequest( params );
438
    return make_query<Artist, IArtist>( ml, "*", std::move( req ) );
439 440
}

441 442 443 444 445 446 447 448 449 450 451 452
Query<IArtist> Artist::searchByGenre( MediaLibraryPtr ml, const std::string& pattern,
                                      const QueryParameters* params, int64_t genreId )
{
    std::string req = "FROM " + policy::ArtistTable::Name + " a "
                "INNER JOIN " + policy::AlbumTrackTable::Name + " att ON att.artist_id = a.id_artist "
                "WHERE id_artist IN "
                    "(SELECT rowid FROM " + policy::ArtistTable::Name + "Fts WHERE name MATCH '*' || ? || '*')"
                "AND att.genre_id = ? "
                "GROUP BY att.artist_id "
                "ORDER BY a.name";
    if ( params != nullptr && params->desc == true )
        req += " DESC";
453
    return make_query<Artist, IArtist>( ml, "a.*", std::move( req ), pattern, genreId );
454 455
}

456
std::string Artist::sortRequest( const QueryParameters* params )
457
{
458 459
    std::string req = " ORDER BY name";
    if ( params != nullptr && params->desc == true )
460
        req +=  " DESC";
461
    return req;
462 463
}

464
}