Media.cpp 17 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
#include <algorithm>
24
#include <cassert>
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
25 26
#include <cstdlib>
#include <cstring>
27
#include <ctime>
28

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
29
#include "Album.h"
30
#include "AlbumTrack.h"
31
#include "Artist.h"
32
#include "AudioTrack.h"
33
#include "Media.h"
34
#include "File.h"
35
#include "Folder.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
36
#include "Label.h"
37
#include "logging/Logger.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
38
#include "Movie.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
39
#include "ShowEpisode.h"
40
#include "database/SqliteTools.h"
41
#include "VideoTrack.h"
42
#include "filesystem/IFile.h"
43 44
#include "filesystem/IDirectory.h"
#include "filesystem/IDevice.h"
45
#include "utils/Filename.h"
46

47
const std::string policy::MediaTable::Name = "Media";
48
const std::string policy::MediaTable::PrimaryKeyColumn = "id_media";
49
unsigned int Media::* const policy::MediaTable::PrimaryKey = &Media::m_id;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
50

51 52
Media::Media( MediaLibraryPtr ml, sqlite::Row& row )
    : m_ml( ml )
53
    , m_changed( false )
54
{
55 56
    row >> m_id
        >> m_type
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
57
        >> m_subType
58 59
        >> m_duration
        >> m_playCount
60
        >> m_lastPlayedDate
61 62
        >> m_progress
        >> m_rating
63
        >> m_insertionDate
64
        >> m_releaseDate
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
65
        >> m_thumbnail
66
        >> m_title
67 68
        >> m_isFavorite
        >> m_isPresent;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
69 70
}

71 72 73
Media::Media( MediaLibraryPtr ml, const std::string& title, Type type )
    : m_ml( ml )
    , m_id( 0 )
74
    , m_type( type )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
75
    , m_subType( SubType::Unknown )
76
    , m_duration( -1 )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
77
    , m_playCount( 0 )
78
    , m_lastPlayedDate( 0 )
79 80
    , m_progress( .0f )
    , m_rating( -1 )
81
    , m_insertionDate( time( nullptr ) )
82
    , m_releaseDate( 0 )
83
    , m_title( title )
84
    , m_isFavorite( false )
85
    , m_changed( false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
86
{
87 88
}

89
std::shared_ptr<Media> Media::create( MediaLibraryPtr ml, Type type, const fs::IFile& file )
90
{
91
    auto self = std::make_shared<Media>( ml, file.name(), type );
92
    static const std::string req = "INSERT INTO " + policy::MediaTable::Name +
93
            "(type, insertion_date, title) VALUES(?, ?, ?)";
94

95
    if ( insert( ml, self, req, type, self->m_insertionDate, self->m_title ) == false )
96
        return nullptr;
97
    return self;
98 99
}

100
AlbumTrackPtr Media::albumTrack() const
101
{
102 103
    if ( m_subType != SubType::AlbumTrack )
        return nullptr;
104 105
    auto lock = m_albumTrack.lock();

106
    if ( m_albumTrack.isCached() == false )
107
        m_albumTrack = AlbumTrack::fromMedia( m_ml, m_id );
108
    return m_albumTrack.get();
109 110
}

111
void Media::setAlbumTrack( AlbumTrackPtr albumTrack )
112
{
113
    auto lock = m_albumTrack.lock();
114
    m_albumTrack = albumTrack;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
115 116
    m_subType = SubType::AlbumTrack;
    m_changed = true;
117 118
}

119
int64_t Media::duration() const
120 121 122 123
{
    return m_duration;
}

124
void Media::setDuration( int64_t duration )
125
{
126 127
    if ( m_duration == duration )
        return;
128
    m_duration = duration;
129
    m_changed = true;
130 131
}

132
ShowEpisodePtr Media::showEpisode() const
133
{
134 135
    if ( m_subType != SubType::ShowEpisode )
        return nullptr;
136

137 138
    auto lock = m_showEpisode.lock();
    if ( m_showEpisode.isCached() == false )
139
        m_showEpisode = ShowEpisode::fromMedia( m_ml, m_id );
140
    return m_showEpisode.get();
141 142
}

143
void Media::setShowEpisode( ShowEpisodePtr episode )
144
{
145 146
    auto lock = m_showEpisode.lock();
    m_showEpisode = episode;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
147 148
    m_subType = SubType::ShowEpisode;
    m_changed = true;
149 150
}

151
std::vector<LabelPtr> Media::labels()
152
{
153
    static const std::string req = "SELECT l.* FROM " + policy::LabelTable::Name + " l "
154
            "INNER JOIN LabelFileRelation lfr ON lfr.label_id = l.id_label "
155
            "WHERE lfr.media_id = ?";
156
    return Label::fetchAll<ILabel>( m_ml, req, m_id );
157 158
}

159
int Media::playCount() const
160
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
161
    return m_playCount;
162 163
}

164
bool Media::increasePlayCount()
165
{
166 167 168 169 170
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET "
            "play_count = ?, last_played_date = ? WHERE id_media = ?";
    auto lastPlayedDate = time( nullptr );
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, m_playCount + 1, lastPlayedDate, m_id ) == false )
        return false;
171
    m_playCount++;
172 173
    m_lastPlayedDate = lastPlayedDate;
    return true;
174 175
}

176 177 178 179 180
float Media::progress() const
{
    return m_progress;
}

181
bool Media::setProgress( float progress )
182
{
183
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET progress = ? WHERE id_media = ?";
184
    if ( progress == m_progress || progress < 0 || progress > 1.0 )
185 186 187
        return true;
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, progress, m_id ) == false )
        return false;
188
    m_progress = progress;
189
    return true;
190 191 192 193 194 195 196
}

int Media::rating() const
{
    return m_rating;
}

197
bool Media::setRating( int rating )
198
{
199
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET rating = ? WHERE id_media = ?";
200
    if ( m_rating == rating )
201 202 203
        return true;
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, rating, m_id ) == false )
        return false;
204
    m_rating = rating;
205
    return true;
206 207
}

208 209 210 211 212
bool Media::isFavorite() const
{
    return m_isFavorite;
}

213
bool Media::setFavorite( bool favorite )
214
{
215
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET is_favorite = ? WHERE id_media = ?";
216
    if ( m_isFavorite == favorite )
217 218 219
        return true;
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, favorite, m_id ) == false )
        return false;
220
    m_isFavorite = favorite;
221
    return true;
222 223
}

224
const std::vector<FilePtr>& Media::files() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
225
{
226 227 228 229 230
    auto lock = m_files.lock();
    if ( m_files.isCached() == false )
    {
        static const std::string req = "SELECT * FROM " + policy::FileTable::Name
                + " WHERE media_id = ?";
231
        m_files = File::fetchAll<IFile>( m_ml, req, m_id );
232 233
    }
    return m_files;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
234 235
}

236
MoviePtr Media::movie() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
237
{
238 239 240
    if ( m_subType != SubType::Movie )
        return nullptr;

241 242
    auto lock = m_movie.lock();

243
    if ( m_movie.isCached() == false )
244
        m_movie = Movie::fromMedia( m_ml, m_id );
245
    return m_movie.get();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
246 247
}

248
void Media::setMovie(MoviePtr movie)
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
249
{
250
    auto lock = m_movie.lock();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
251
    m_movie = movie;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
252 253
    m_subType = SubType::Movie;
    m_changed = true;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
254 255
}

256
bool Media::addVideoTrack(const std::string& codec, unsigned int width, unsigned int height, float fps)
257
{
258
    return VideoTrack::create( m_ml, codec, width, height, fps, m_id ) != nullptr;
259 260
}

261
std::vector<VideoTrackPtr> Media::videoTracks()
262
{
263 264
    static const std::string req = "SELECT * FROM " + policy::VideoTrackTable::Name +
            " WHERE media_id = ?";
265
    return VideoTrack::fetchAll<IVideoTrack>( m_ml, req, m_id );
266 267
}

268
bool Media::addAudioTrack( const std::string& codec, unsigned int bitrate,
269 270
                          unsigned int sampleRate, unsigned int nbChannels,
                          const std::string& language, const std::string& desc )
271
{
272
    return AudioTrack::create( m_ml, codec, bitrate, sampleRate, nbChannels, language, desc, m_id ) != nullptr;
273 274
}

275
std::vector<AudioTrackPtr> Media::audioTracks()
276
{
277 278
    static const std::string req = "SELECT * FROM " + policy::AudioTrackTable::Name +
            " WHERE media_id = ?";
279
    return AudioTrack::fetchAll<IAudioTrack>( m_ml, req, m_id );
280 281
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
282
const std::string &Media::thumbnail()
283
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
284
    return m_thumbnail;
285 286
}

287 288 289 290 291
unsigned int Media::insertionDate() const
{
    return m_insertionDate;
}

292 293 294 295 296 297 298 299 300 301 302 303 304
unsigned int Media::releaseDate() const
{
    return m_releaseDate;
}

void Media::setReleaseDate( unsigned int date )
{
    if ( m_releaseDate == date )
        return;
    m_releaseDate = date;
    m_changed = true;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
305
void Media::setThumbnail(const std::string& thumbnail )
306
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
307
    if ( m_thumbnail == thumbnail )
308
        return;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
309
    m_thumbnail = thumbnail;
310 311 312 313 314 315
    m_changed = true;
}

bool Media::save()
{
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET "
316
            "type = ?, subtype = ?, duration = ?, progress = ?, release_date = ?,"
317
            "thumbnail = ?, title = ? WHERE id_media = ?";
318 319
    if ( m_changed == false )
        return true;
320
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, m_type, m_subType, m_duration,
321
                                       m_progress, m_releaseDate, m_thumbnail, m_title, m_id ) == false )
322 323 324 325
    {
        return false;
    }
    m_changed = false;
326 327 328
    return true;
}

329
std::shared_ptr<File> Media::addFile( const fs::IFile& fileFs, Folder& parentFolder, fs::IDirectory& parentFolderFs, IFile::Type type )
330
{
331
    auto file = File::create( m_ml, m_id, type, fileFs, parentFolder.id(), parentFolderFs.device()->isRemovable() );
332 333 334 335 336 337
    if ( file == nullptr )
        return nullptr;
    auto lock = m_files.lock();
    if ( m_files.isCached() )
        m_files.get().push_back( file );
    return file;
338 339
}

340
void Media::removeFile( File& file )
341
{
342 343 344
    file.destroy();
    auto lock = m_files.lock();
    if ( m_files.isCached() == false )
345
        return;
346 347 348
    m_files.get().erase( std::remove_if( begin( m_files.get() ), end( m_files.get() ), [&file]( const FilePtr& f ) {
        return f->id() == file.id();
    }));
349 350
}

351
std::vector<MediaPtr> Media::listAll( MediaLibraryPtr ml, IMedia::Type type, medialibrary::SortingCriteria sort, bool desc )
352
{
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    std::string req;
    if ( sort == medialibrary::SortingCriteria::LastModificationDate )
    {
        req = "SELECT m.* FROM " + policy::MediaTable::Name + " m INNER JOIN "
                + policy::FileTable::Name + " f ON m.id_media = f.media_id"
                " WHERE m.type = ?"
                " AND ( f.type = ? OR f.type = ? )"
                " ORDER BY f.last_modification_date";
        if ( desc == true )
            req += " DESC";
        return fetchAll<IMedia>( ml, req, type, File::Type::Entire, File::Type::Main );
    }
    req = "SELECT * FROM " + policy::MediaTable::Name + " WHERE type = ? AND is_present = 1 ORDER BY ";
    switch ( sort )
    {
    case medialibrary::SortingCriteria::Alpha:
    case medialibrary::SortingCriteria::Default:
        req += "title";
        break;
    case medialibrary::SortingCriteria::Duration:
        req += "duration";
        break;
    case medialibrary::SortingCriteria::InsertionDate:
        req += "insertion_date";
        break;
    case medialibrary::SortingCriteria::ReleaseDate:
        req += "release_date";
        break;
    default:
        break;
    }
    if ( desc == true )
        req += " DESC";

    return fetchAll<IMedia>( ml, req, type );
388 389
}

390
unsigned int Media::id() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
391 392 393 394
{
    return m_id;
}

395
IMedia::Type Media::type()
396 397 398 399
{
    return m_type;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
400 401 402 403 404
IMedia::SubType Media::subType() const
{
    return m_subType;
}

405
void Media::setType( Type type )
406
{
407 408
    if ( m_type != type )
        return;
409
    m_type = type;
410
    m_changed = true;
411 412
}

413
const std::string &Media::title() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
414
{
415
    return m_title;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
416 417
}

418
void Media::setTitle( const std::string &title )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
419
{
420 421
    if ( m_title == title )
        return;
422
    m_title = title;
423
    m_changed = true;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
424 425
}

426
bool Media::createTable( DBConnection connection )
427
{
428 429
    std::string req = "CREATE TABLE IF NOT EXISTS " + policy::MediaTable::Name + "("
            "id_media INTEGER PRIMARY KEY AUTOINCREMENT,"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
430
            "type INTEGER,"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
431
            "subtype INTEGER,"
432
            "duration INTEGER DEFAULT -1,"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
433
            "play_count UNSIGNED INTEGER,"
434
            "last_played_date UNSIGNED INTEGER,"
435 436
            "progress REAL,"
            "rating INTEGER DEFAULT -1,"
437
            "insertion_date UNSIGNED INTEGER,"
438
            "release_date UNSIGNED INTEGER,"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
439
            "thumbnail TEXT,"
440
            "title TEXT,"
441
            "is_favorite BOOLEAN NOT NULL DEFAULT 0,"
442
            "is_present BOOLEAN NOT NULL DEFAULT 1"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
443
            ")";
444 445
    static const std::string indexReq = "CREATE INDEX IF NOT EXISTS index_last_played_date ON "
            + policy::MediaTable::Name + "(last_played_date DESC)";
446 447
    static const std::string vtableReq = "CREATE VIRTUAL TABLE IF NOT EXISTS "
                + policy::MediaTable::Name + "Fts USING FTS3("
448 449
                "title,"
                "labels"
450 451
            ")";
    return sqlite::Tools::executeRequest( connection, req ) &&
452
            sqlite::Tools::executeRequest( connection, indexReq ) &&
453
            sqlite::Tools::executeRequest( connection, vtableReq );
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
}

bool Media::createTriggers( DBConnection connection )
{
    static const std::string triggerReq = "CREATE TRIGGER IF NOT EXISTS has_files_present AFTER UPDATE OF "
            "is_present ON " + policy::FileTable::Name +
            " BEGIN "
            " UPDATE " + policy::MediaTable::Name + " SET is_present="
                "(SELECT COUNT(id_file) FROM " + policy::FileTable::Name + " WHERE media_id=new.media_id AND is_present=1) "
                "WHERE id_media=new.media_id;"
            " END;";
    static const std::string triggerReq2 = "CREATE TRIGGER IF NOT EXISTS cascade_file_deletion AFTER DELETE ON "
            + policy::FileTable::Name +
            " BEGIN "
            " DELETE FROM " + policy::MediaTable::Name + " WHERE "
                "(SELECT COUNT(id_file) FROM " + policy::FileTable::Name + " WHERE media_id=old.media_id) = 0"
                " AND id_media=old.media_id;"
            " END;";
472 473 474 475

    static const std::string vtableInsertTrigger = "CREATE TRIGGER IF NOT EXISTS insert_media_fts"
            " AFTER INSERT ON " + policy::MediaTable::Name +
            " BEGIN"
476
            " INSERT INTO " + policy::MediaTable::Name + "Fts(rowid,title,labels) VALUES(new.id_media, new.title, '');"
477 478
            " END";
    static const std::string vtableDeleteTrigger = "CREATE TRIGGER IF NOT EXISTS delete_media_fts"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
479
            " BEFORE DELETE ON " + policy::MediaTable::Name +
480 481 482 483 484 485 486 487
            " BEGIN"
            " DELETE FROM " + policy::MediaTable::Name + "Fts WHERE rowid = old.id_media;"
            " END";
    static const std::string vtableUpdateTitleTrigger2 = "CREATE TRIGGER IF NOT EXISTS update_media_title_fts"
              " AFTER UPDATE OF title ON " + policy::MediaTable::Name +
              " BEGIN"
              " UPDATE " + policy::MediaTable::Name + "Fts SET title = new.title WHERE rowid = new.id_media;"
              " END";
488
    return sqlite::Tools::executeRequest( connection, triggerReq ) &&
489 490 491 492
            sqlite::Tools::executeRequest( connection, triggerReq2 ) &&
            sqlite::Tools::executeRequest( connection, vtableInsertTrigger ) &&
            sqlite::Tools::executeRequest( connection, vtableDeleteTrigger ) &&
            sqlite::Tools::executeRequest( connection, vtableUpdateTitleTrigger2 );
493
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
494

495
bool Media::addLabel( LabelPtr label )
496
{
497
    if ( m_id == 0 || label->id() == 0 )
498
    {
499
        LOG_ERROR( "Both file & label need to be inserted in database before being linked together" );
500 501 502
        return false;
    }
    const char* req = "INSERT INTO LabelFileRelation VALUES(?, ?)";
503
    if ( sqlite::Tools::insert( m_ml->getConn(), req, label->id(), m_id ) == 0 )
504 505 506
        return false;
    const std::string reqFts = "UPDATE " + policy::MediaTable::Name + "Fts "
        "SET labels = labels || ' ' || ? WHERE rowid = ?";
507
    return sqlite::Tools::executeUpdate( m_ml->getConn(), reqFts, label->name(), m_id );
508 509
}

510
bool Media::removeLabel( LabelPtr label )
511
{
512
    if ( m_id == 0 || label->id() == 0 )
513
    {
514
        LOG_ERROR( "Can't unlink a label/file not inserted in database" );
515 516
        return false;
    }
517
    const char* req = "DELETE FROM LabelFileRelation WHERE label_id = ? AND media_id = ?";
518
    if ( sqlite::Tools::executeDelete( m_ml->getConn(), req, label->id(), m_id ) == false )
519 520 521
        return false;
    const std::string reqFts = "UPDATE " + policy::MediaTable::Name + "Fts "
            "SET labels = TRIM(REPLACE(labels, ?, '')) WHERE rowid = ?";
522
    return sqlite::Tools::executeUpdate( m_ml->getConn(), reqFts, label->name(), m_id );
523
}
524 525


526
std::vector<MediaPtr> Media::search( MediaLibraryPtr ml, const std::string& title )
527 528 529
{
    static const std::string req = "SELECT * FROM " + policy::MediaTable::Name + " WHERE"
            " id_media IN (SELECT rowid FROM " + policy::MediaTable::Name + "Fts"
530 531
            " WHERE " + policy::MediaTable::Name + "Fts MATCH ?)"
            "AND is_present = 1";
532
    return Media::fetchAll<IMedia>( ml, req, title + "*" );
533
}
534

535
std::vector<MediaPtr> Media::fetchHistory( MediaLibraryPtr ml )
536 537 538
{
    static const std::string req = "SELECT * FROM " + policy::MediaTable::Name + " WHERE last_played_date IS NOT NULL"
            " ORDER BY last_played_date DESC LIMIT 100";
539
    return fetchAll<IMedia>( ml, req );
540
}