Album.cpp 17.1 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

27 28
#include <algorithm>

29
#include "Album.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
30
#include "AlbumTrack.h"
31
#include "Artist.h"
32
#include "medialibrary/IGenre.h"
33
#include "Media.h"
34

35
#include "database/SqliteTools.h"
36

37 38 39
namespace medialibrary
{

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
40
const std::string policy::AlbumTable::Name = "Album";
41
const std::string policy::AlbumTable::PrimaryKeyColumn = "id_album";
42
int64_t Album::* const policy::AlbumTable::PrimaryKey = &Album::m_id;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
43

44 45
Album::Album(MediaLibraryPtr ml, sqlite::Row& row)
    : m_ml( ml )
46
{
47 48
    row >> m_id
        >> m_title
49
        >> m_artistId
50
        >> m_releaseYear
51
        >> m_shortSummary
52
        >> m_artworkMrl
53
        >> m_nbTracks
54
        >> m_duration
55
        >> m_isPresent;
56 57
}

58 59 60
Album::Album( MediaLibraryPtr ml, const std::string& title )
    : m_ml( ml )
    , m_id( 0 )
61
    , m_title( title )
62
    , m_artistId( 0 )
63
    , m_releaseYear( ~0u )
64
    , m_nbTracks( 0 )
65
    , m_duration( 0 )
66
    , m_isPresent( true )
67 68 69
{
}

70 71 72
Album::Album( MediaLibraryPtr ml, const Artist* artist )
    : m_ml( ml )
    , m_id( 0 )
73 74 75
    , m_artistId( artist->id() )
    , m_releaseYear( ~0u )
    , m_nbTracks( 0 )
76
    , m_duration( 0 )
77
    , m_isPresent( true )
78 79 80
{
}

81
int64_t Album::id() const
82 83
{
    return m_id;
84 85
}

86
const std::string& Album::title() const
87
{
88
    return m_title;
89 90
}

91
unsigned int Album::releaseYear() const
92
{
93 94
    if ( m_releaseYear == ~0U )
        return 0;
95 96 97
    return m_releaseYear;
}

98
bool Album::setReleaseYear( unsigned int date, bool force )
99
{
100 101 102 103 104 105 106 107 108 109 110 111
    if ( date == m_releaseYear )
        return true;
    if ( force == false )
    {
        if ( m_releaseYear != ~0u && date != m_releaseYear )
        {
            // If we already have set the date back to 0, don't do it again.
            if ( m_releaseYear == 0 )
                return true;
            date = 0;
        }
    }
112 113
    static const std::string req = "UPDATE " + policy::AlbumTable::Name
            + " SET release_year = ? WHERE id_album = ?";
114
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, date, m_id ) == false )
115 116 117 118 119
        return false;
    m_releaseYear = date;
    return true;
}

120
const std::string& Album::shortSummary() const
121 122 123 124
{
    return m_shortSummary;
}

125 126
bool Album::setShortSummary( const std::string& summary )
{
127
    static const std::string req = "UPDATE " + policy::AlbumTable::Name
128
            + " SET short_summary = ? WHERE id_album = ?";
129
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, summary, m_id ) == false )
130 131 132 133 134
        return false;
    m_shortSummary = summary;
    return true;
}

135
const std::string& Album::artworkMrl() const
136
{
137
    return m_artworkMrl;
138 139
}

140
bool Album::setArtworkMrl( const std::string& artworkMrl )
141
{
142
    static const std::string req = "UPDATE " + policy::AlbumTable::Name
143
            + " SET artwork_mrl = ? WHERE id_album = ?";
144
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, artworkMrl, m_id ) == false )
145
        return false;
146
    m_artworkMrl = artworkMrl;
147 148 149
    return true;
}

150
std::string Album::orderTracksBy( SortingCriteria sort, bool desc )
151 152 153 154
{
    std::string req = " ORDER BY ";
    switch ( sort )
    {
155
    case SortingCriteria::Alpha:
156 157
        req += "med.title";
        break;
158
    case SortingCriteria::Duration:
159 160
        req += "med.duration";
        break;
161
    case SortingCriteria::ReleaseDate:
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
162
        req += "med.release_date";
163 164
        break;
    default:
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
165 166 167 168
        if ( desc == true )
            req += "att.disc_number DESC, att.track_number DESC, med.filename";
        else
            req += "att.disc_number, att.track_number, med.filename";
169 170 171 172 173 174 175 176
        break;
    }

    if ( desc == true )
        req += " DESC";
    return req;
}

177
std::string Album::orderBy( SortingCriteria sort, bool desc )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
178 179 180 181
{
    std::string req = " ORDER BY ";
    switch ( sort )
    {
182
    case SortingCriteria::ReleaseDate:
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
183 184 185 186 187
        if ( desc == true )
            req += "release_year DESC, title";
        else
            req += "release_year, title";
        break;
188
    case SortingCriteria::Duration:
189 190 191 192
        req += "duration";
        if ( desc == true )
            req += " DESC";
        break;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
193 194 195 196 197 198 199 200 201
    default:
        req += "title";
        if ( desc == true )
            req += " DESC";
        break;
    }
    return req;
}

202
std::vector<MediaPtr> Album::tracks( SortingCriteria sort, bool desc ) const
203
{
204 205
    // This doesn't return the cached version, because it would be fairly complicated, if not impossible or
    // counter productive, to maintain a cache that respects all orderings.
206 207 208
    std::string req = "SELECT med.* FROM " + policy::MediaTable::Name + " med "
        " INNER JOIN " + policy::AlbumTrackTable::Name + " att ON att.media_id = med.id_media "
        " WHERE att.album_id = ? AND med.is_present = 1";
209
    req += orderTracksBy( sort, desc );
210
    return Media::fetchAll<IMedia>( m_ml, req, m_id );
211 212
}

213
std::vector<MediaPtr> Album::tracks( GenrePtr genre, SortingCriteria sort, bool desc ) const
214 215 216
{
    if ( genre == nullptr )
        return {};
217
    std::string req = "SELECT med.* FROM " + policy::MediaTable::Name + " med "
218 219
            " INNER JOIN " + policy::AlbumTrackTable::Name + " att ON att.media_id = med.id_media "
            " WHERE att.album_id = ? AND med.is_present = 1"
220
            " AND genre_id = ?";
221
    req += orderTracksBy( sort, desc );
222
    return Media::fetchAll<IMedia>( m_ml, req, m_id, genre->id() );
223 224
}

225
std::vector<MediaPtr> Album::cachedTracks() const
226 227 228
{
    auto lock = m_tracks.lock();
    if ( m_tracks.isCached() == false )
229
        m_tracks = tracks( SortingCriteria::Default, false );
230 231 232
    return m_tracks.get();
}

233 234
std::shared_ptr<AlbumTrack> Album::addTrack( std::shared_ptr<Media> media, unsigned int trackNb,
                                             unsigned int discNumber, int64_t artistId, int64_t genreId )
235
{
236
    auto t = m_ml->getConn()->newTransaction();
237

238
    auto track = AlbumTrack::create( m_ml, m_id, media, trackNb, discNumber, artistId, genreId );
239 240
    if ( track == nullptr )
        return nullptr;
241
    media->setAlbumTrack( track );
242
    // Assume the media will be saved by the caller
243
    m_nbTracks++;
244
    m_duration += media->duration();
245
    t->commit();
246
    auto lock = m_tracks.lock();
247 248 249 250 251 252 253
    // Don't assume we have always have a valid value in m_tracks.
    // While it's ok to assume that if we are currently parsing the album, we
    // have a valid cache tracks, this isn't true when restarting an interrupted parsing.
    // The nbTracks value will be correct however. If it's equal to one, it means we're inserting
    // the first track in this album
    if ( m_tracks.isCached() == false && m_nbTracks == 1 )
        m_tracks.markCached();
254 255
    if ( m_tracks.isCached() == true )
        m_tracks.get().push_back( media );
256
    return track;
257 258
}

259 260 261 262 263
unsigned int Album::nbTracks() const
{
    return m_nbTracks;
}

264 265 266 267 268
unsigned int Album::duration() const
{
    return m_duration;
}

269
ArtistPtr Album::albumArtist() const
270 271 272
{
    if ( m_artistId == 0 )
        return nullptr;
273 274 275 276
    auto lock = m_albumArtist.lock();
    if ( m_albumArtist.isCached() == false )
        m_albumArtist = Artist::fetch( m_ml, m_artistId );
    return m_albumArtist.get();
277 278
}

279
bool Album::setAlbumArtist( std::shared_ptr<Artist> artist )
280 281 282 283 284 285 286
{
    if ( m_artistId == artist->id() )
        return true;
    if ( artist->id() == 0 )
        return false;
    static const std::string req = "UPDATE " + policy::AlbumTable::Name + " SET "
            "artist_id = ? WHERE id_album = ?";
287
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, artist->id(), m_id ) == false )
288
        return false;
289 290
    if ( m_artistId != 0 )
    {
291 292 293
        if ( m_albumArtist.isCached() == false )
            albumArtist();
        m_albumArtist.get()->updateNbAlbum( -1 );
294
    }
295
    m_artistId = artist->id();
296
    m_albumArtist = artist;
297
    artist->updateNbAlbum( 1 );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
298 299
    static const std::string ftsReq = "UPDATE " + policy::AlbumTable::Name + "Fts SET "
            " artist = ? WHERE rowid = ?";
300
    sqlite::Tools::executeUpdate( m_ml->getConn(), ftsReq, artist->name(), m_id );
301 302 303
    return true;
}

304
std::vector<ArtistPtr> Album::artists( bool desc ) const
305
{
306
    std::string req = "SELECT art.* FROM " + policy::ArtistTable::Name + " art "
307
            "INNER JOIN AlbumArtistRelation aar ON aar.artist_id = art.id_artist "
308 309 310
            "WHERE aar.album_id = ? ORDER BY art.name";
    if ( desc == true )
        req += " DESC";
311
    return Artist::fetchAll<IArtist>( m_ml, req, m_id );
312 313
}

314
bool Album::addArtist( std::shared_ptr<Artist> artist )
315
{
316
    static const std::string req = "INSERT OR IGNORE INTO AlbumArtistRelation VALUES(?, ?)";
317 318
    if ( m_id == 0 || artist->id() == 0 )
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
319
        LOG_ERROR("Both artist & album need to be inserted in database before being linked together" );
320 321
        return false;
    }
322
    return sqlite::Tools::executeInsert( m_ml->getConn(), req, m_id, artist->id() ) != 0;
323 324
}

325 326
bool Album::removeArtist(Artist* artist)
{
327
    static const std::string req = "DELETE FROM AlbumArtistRelation WHERE album_id = ? "
328
            "AND id_artist = ?";
329
    return sqlite::Tools::executeDelete( m_ml->getConn(), req, m_id, artist->id() );
330 331
}

332
bool Album::createTable(DBConnection dbConnection )
333
{
334
    const std::string req = "CREATE TABLE IF NOT EXISTS " +
335 336
            policy::AlbumTable::Name +
            "("
337
                "id_album INTEGER PRIMARY KEY AUTOINCREMENT,"
338
                "title TEXT COLLATE NOCASE,"
339
                "artist_id UNSIGNED INTEGER,"
340
                "release_year UNSIGNED INTEGER,"
341
                "short_summary TEXT,"
342
                "artwork_mrl TEXT,"
343
                "nb_tracks UNSIGNED INTEGER DEFAULT 0,"
344
                "duration UNSIGNED INTEGER NOT NULL DEFAULT 0,"
345 346 347
                "is_present BOOLEAN NOT NULL DEFAULT 1,"
                "FOREIGN KEY( artist_id ) REFERENCES " + policy::ArtistTable::Name
                + "(id_artist) ON DELETE CASCADE"
348
            ")";
349
    const std::string reqRel = "CREATE TABLE IF NOT EXISTS AlbumArtistRelation("
350 351 352 353
                "album_id INTEGER,"
                "artist_id INTEGER,"
                "PRIMARY KEY (album_id, artist_id),"
                "FOREIGN KEY(album_id) REFERENCES " + policy::AlbumTable::Name + "("
354
                    + policy::AlbumTable::PrimaryKeyColumn + ") ON DELETE CASCADE,"
355
                "FOREIGN KEY(artist_id) REFERENCES " + policy::ArtistTable::Name + "("
356
                    + policy::ArtistTable::PrimaryKeyColumn + ") ON DELETE CASCADE"
357
            ")";
358
    const std::string vtableReq = "CREATE VIRTUAL TABLE IF NOT EXISTS "
359
                + policy::AlbumTable::Name + "Fts USING FTS3("
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
360 361 362
                "title,"
                "artist"
            ")";
363
    const std::string indexReq = "CREATE INDEX IF NOT EXISTS album_artist_id_idx ON " +
364
            policy::AlbumTable::Name + "(artist_id)";
365
    return sqlite::Tools::executeRequest( dbConnection, req ) &&
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
366
            sqlite::Tools::executeRequest( dbConnection, reqRel ) &&
367 368
            sqlite::Tools::executeRequest( dbConnection, vtableReq ) &&
            sqlite::Tools::executeRequest( dbConnection, indexReq );
369 370
}

371 372 373 374 375 376 377 378 379
bool Album::createTriggers(DBConnection dbConnection)
{
    static const std::string triggerReq = "CREATE TRIGGER IF NOT EXISTS is_album_present AFTER UPDATE OF "
            "is_present ON " + policy::AlbumTrackTable::Name +
            " BEGIN "
            " UPDATE " + policy::AlbumTable::Name + " SET is_present="
                "(SELECT COUNT(id_track) FROM " + policy::AlbumTrackTable::Name + " WHERE album_id=new.album_id AND is_present=1) "
                "WHERE id_album=new.album_id;"
            " END";
380
    static const std::string deleteTriggerReq = "CREATE TRIGGER IF NOT EXISTS delete_album_track AFTER DELETE ON "
381 382
             + policy::AlbumTrackTable::Name +
            " BEGIN "
383
            " UPDATE " + policy::AlbumTable::Name + " SET nb_tracks = nb_tracks - 1 WHERE id_album = old.album_id;"
384
            " DELETE FROM " + policy::AlbumTable::Name +
385
                " WHERE id_album=old.album_id AND nb_tracks = 0;"
386
            " END";
387
    static const std::string updateAddTrackTriggerReq = "CREATE TRIGGER IF NOT EXISTS add_album_track"
388 389 390
            " AFTER INSERT ON " + policy::AlbumTrackTable::Name +
            " BEGIN"
            " UPDATE " + policy::AlbumTable::Name +
391 392
            " SET duration = duration + (SELECT duration FROM " + policy::MediaTable::Name + " WHERE id_media=new.media_id),"
            " nb_tracks = nb_tracks + 1"
393 394
            " WHERE id_album = new.album_id;"
            " END";
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
    static const std::string vtriggerInsert = "CREATE TRIGGER IF NOT EXISTS insert_album_fts AFTER INSERT ON "
            + policy::AlbumTable::Name +
            // Skip unknown albums
            " WHEN new.title IS NOT NULL"
            " BEGIN"
            " INSERT INTO " + policy::AlbumTable::Name + "Fts(rowid, title) VALUES(new.id_album, new.title);"
            " END";
    static const std::string vtriggerDelete = "CREATE TRIGGER IF NOT EXISTS delete_album_fts BEFORE DELETE ON "
            + policy::AlbumTable::Name +
            // Unknown album probably won't be deleted, but better safe than sorry
            " WHEN old.title IS NOT NULL"
            " BEGIN"
            " DELETE FROM " + policy::AlbumTable::Name + "Fts WHERE rowid = old.id_album;"
            " END";
    return sqlite::Tools::executeRequest( dbConnection, triggerReq ) &&
410
            sqlite::Tools::executeRequest( dbConnection, deleteTriggerReq ) &&
411
            sqlite::Tools::executeRequest( dbConnection, updateAddTrackTriggerReq ) &&
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
412 413
            sqlite::Tools::executeRequest( dbConnection, vtriggerInsert ) &&
            sqlite::Tools::executeRequest( dbConnection, vtriggerDelete );
414 415
}

416
std::shared_ptr<Album> Album::create( MediaLibraryPtr ml, const std::string& title )
417
{
418
    auto album = std::make_shared<Album>( ml, title );
419
    static const std::string req = "INSERT INTO " + policy::AlbumTable::Name +
420
            "(id_album, title) VALUES(NULL, ?)";
421
    if ( insert( ml, album, req, title ) == false )
422 423
        return nullptr;
    return album;
424
}
425

426
std::shared_ptr<Album> Album::createUnknownAlbum( MediaLibraryPtr ml, const Artist* artist )
427
{
428
    auto album = std::make_shared<Album>( ml, artist );
429 430
    static const std::string req = "INSERT INTO " + policy::AlbumTable::Name +
            "(id_album, artist_id) VALUES(NULL, ?)";
431
    if ( insert( ml, album, req, artist->id() ) == false )
432 433 434
        return nullptr;
    return album;
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
435

436
std::vector<AlbumPtr> Album::search( MediaLibraryPtr ml, const std::string& pattern )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
437 438 439
{
    static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name + " WHERE id_album IN "
            "(SELECT rowid FROM " + policy::AlbumTable::Name + "Fts WHERE " +
440 441
            policy::AlbumTable::Name + "Fts MATCH ?)"
            "AND is_present = 1";
442
    return fetchAll<IAlbum>( ml, req, pattern + "*" );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
443
}
444

445
std::vector<AlbumPtr> Album::fromArtist( MediaLibraryPtr ml, int64_t artistId, SortingCriteria sort, bool desc )
446 447 448 449 450
{
    std::string req = "SELECT * FROM " + policy::AlbumTable::Name + " alb "
                    "WHERE artist_id = ? AND is_present=1 ORDER BY ";
    switch ( sort )
    {
451
    case SortingCriteria::Alpha:
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
        req += "title";
        if ( desc == true )
            req += " DESC";
        break;
    default:
        // When listing albums of an artist, default order is by descending year (with album title
        // discrimination in case 2+ albums went out the same year)
        // This leads to DESC being used for "non-desc" case
        if ( desc == true )
            req += "release_year, title";
        else
            req += "release_year DESC, title";
        break;
    }

    return fetchAll<IAlbum>( ml, req, artistId );
}

470
std::vector<AlbumPtr> Album::fromGenre( MediaLibraryPtr ml, int64_t genreId, SortingCriteria sort, bool desc)
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
471 472 473 474 475 476 477 478
{
    std::string req = "SELECT a.* FROM " + policy::AlbumTable::Name + " a "
            "INNER JOIN " + policy::AlbumTrackTable::Name + " att ON att.album_id = a.id_album "
            "WHERE att.genre_id = ? GROUP BY att.album_id";
    req += orderBy( sort, desc );
    return fetchAll<IAlbum>( ml, req, genreId );
}

479
std::vector<AlbumPtr> Album::listAll( MediaLibraryPtr ml, SortingCriteria sort, bool desc )
480 481
{
    std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
482 483
                    " WHERE is_present=1";
    req += orderBy( sort, desc );
484 485
    return fetchAll<IAlbum>( ml, req );
}
486 487

}