MediaLibrary.cpp 18.7 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
#include <algorithm>
#include <functional>
25 26 27

#include "Fixup.h"

28 29
#include "Album.h"
#include "AlbumTrack.h"
30
#include "Artist.h"
31
#include "AudioTrack.h"
32
#include "discoverer/DiscovererWorker.h"
33
#include "utils/ModificationsNotifier.h"
34
#include "Device.h"
35
#include "File.h"
36
#include "Folder.h"
37
#include "Genre.h"
38
#include "History.h"
39
#include "Media.h"
40
#include "MediaLibrary.h"
41
#include "Label.h"
42
#include "logging/Logger.h"
43
#include "Movie.h"
44
#include "parser/Parser.h"
45
#include "Playlist.h"
46 47
#include "Show.h"
#include "ShowEpisode.h"
48
#include "database/SqliteTools.h"
49
#include "database/SqliteConnection.h"
50
#include "utils/Filename.h"
51
#include "VideoTrack.h"
52

53 54 55
// Discoverers:
#include "discoverer/FsDiscoverer.h"

56 57 58
// Metadata services:
#include "metadata_services/vlc/VLCMetadataService.h"
#include "metadata_services/vlc/VLCThumbnailer.h"
59
#include "metadata_services/MetadataParser.h"
60

61 62
#include "filesystem/IDirectory.h"
#include "filesystem/IFile.h"
63
#include "filesystem/IDevice.h"
64 65
#include "factory/FileSystem.h"

66
const std::vector<std::string> MediaLibrary::supportedVideoExtensions {
67 68 69 70 71
    // Videos
    "avi", "3gp", "amv", "asf", "divx", "dv", "flv", "gxf",
    "iso", "m1v", "m2v", "m2t", "m2ts", "m4v", "mkv", "mov",
    "mp2", "mp4", "mpeg", "mpeg1", "mpeg2", "mpeg4", "mpg",
    "mts", "mxf", "nsv", "nuv", "ogg", "ogm", "ogv", "ogx", "ps",
72 73 74 75
    "rec", "rm", "rmvb", "tod", "ts", "vob", "vro", "webm", "wmv"
};

const std::vector<std::string> MediaLibrary::supportedAudioExtensions {
76 77 78 79 80 81 82 83
    // Audio
    "a52", "aac", "ac3", "aiff", "amr", "aob", "ape",
    "dts", "flac", "it", "m4a", "m4p", "mid", "mka", "mlp",
    "mod", "mp1", "mp2", "mp3", "mpc", "oga", "ogg", "oma",
    "rmi", "s3m", "spx", "tta", "voc", "vqf", "w64", "wav",
    "wma", "wv", "xa", "xm"
};

84
const uint32_t MediaLibrary::DbModelVersion = 2;
85

86
MediaLibrary::MediaLibrary()
87 88
    : m_callback( nullptr )
    , m_verbosity( LogLevel::Error )
89
{
90
    Log::setLogLevel( m_verbosity );
91 92
}

93 94
MediaLibrary::~MediaLibrary()
{
95
    // Explicitely stop the discoverer, to avoid it writting while tearing down.
96 97
    if ( m_discoverer != nullptr )
        m_discoverer->stop();
98
    Media::clear();
99
    Folder::clear();
100 101 102 103 104
    Label::clear();
    Album::clear();
    AlbumTrack::clear();
    Show::clear();
    ShowEpisode::clear();
105
    Movie::clear();
106
    VideoTrack::clear();
107
    AudioTrack::clear();
108
    Artist::clear();
109
    Device::clear();
110
    File::clear();
111
    Playlist::clear();
112
    History::clear();
113
    Genre::clear();
114 115
}

116 117
bool MediaLibrary::createAllTables()
{
118 119 120 121 122
    // We need to create the tables in order of triggers creation
    // Device is the "root of all evil". When a device is modified,
    // we will trigger an update on folder, which will trigger
    // an update on files, and so on.

123
    auto t = m_dbConnection->newTransaction();
124
    auto res = Device::createTable( m_dbConnection.get() ) &&
125
        Folder::createTable( m_dbConnection.get() ) &&
126
        Media::createTable( m_dbConnection.get() ) &&
127
        File::createTable( m_dbConnection.get() ) &&
128
        Label::createTable( m_dbConnection.get() ) &&
129
        Playlist::createTable( m_dbConnection.get() ) &&
130
        Genre::createTable( m_dbConnection.get() ) &&
131 132
        Album::createTable( m_dbConnection.get() ) &&
        AlbumTrack::createTable( m_dbConnection.get() ) &&
133
        Album::createTriggers( m_dbConnection.get() ) &&
134 135 136 137 138
        Show::createTable( m_dbConnection.get() ) &&
        ShowEpisode::createTable( m_dbConnection.get() ) &&
        Movie::createTable( m_dbConnection.get() ) &&
        VideoTrack::createTable( m_dbConnection.get() ) &&
        AudioTrack::createTable( m_dbConnection.get() ) &&
139
        Artist::createTable( m_dbConnection.get() ) &&
140
        Artist::createDefaultArtists( m_dbConnection.get() ) &&
141
        Artist::createTriggers( m_dbConnection.get() ) &&
142
        Media::createTriggers( m_dbConnection.get() ) &&
143
        Playlist::createTriggers( m_dbConnection.get() ) &&
144
        History::createTable( m_dbConnection.get() ) &&
145 146 147 148 149 150 151
        Settings::createTable( m_dbConnection.get() );
    if ( res == false )
        return false;
    t->commit();
    return true;
}

152 153
void MediaLibrary::registerEntityHooks()
{
154
    if ( m_modificationNotifier == nullptr )
155 156
        return;

157 158 159 160 161
    m_dbConnection->registerUpdateHook( policy::MediaTable::Name,
                                        [this]( SqliteConnection::HookReason reason, int64_t rowId ) {
        if ( reason != SqliteConnection::HookReason::Delete )
            return;
        Media::removeFromCache( rowId );
162
        m_modificationNotifier->notifyMediaRemoval( rowId );
163
    });
164 165 166 167 168 169 170
    m_dbConnection->registerUpdateHook( policy::ArtistTable::Name,
                                        [this]( SqliteConnection::HookReason reason, int64_t rowId ) {
        if ( reason != SqliteConnection::HookReason::Delete )
            return;
        Artist::removeFromCache( rowId );
        m_modificationNotifier->notifyArtistRemoval( rowId );
    });
171 172 173 174 175 176 177
    m_dbConnection->registerUpdateHook( policy::AlbumTable::Name,
                                        [this]( SqliteConnection::HookReason reason, int64_t rowId ) {
        if ( reason != SqliteConnection::HookReason::Delete )
            return;
        Album::removeFromCache( rowId );
        m_modificationNotifier->notifyAlbumRemoval( rowId );
    });
178 179 180 181 182 183 184
    m_dbConnection->registerUpdateHook( policy::AlbumTrackTable::Name,
                                        [this]( SqliteConnection::HookReason reason, int64_t rowId ) {
        if ( reason != SqliteConnection::HookReason::Delete )
            return;
        AlbumTrack::removeFromCache( rowId );
        m_modificationNotifier->notifyAlbumTrackRemoval( rowId );
    });
185 186 187
}


188 189 190 191 192
bool MediaLibrary::validateSearchPattern( const std::string& pattern )
{
    return pattern.size() >= 3;
}

193
bool MediaLibrary::initialize( const std::string& dbPath, const std::string& thumbnailPath, IMediaLibraryCb* mlCallback )
194 195 196 197
{
    if ( m_fsFactory == nullptr )
        m_fsFactory.reset( new factory::FileSystemFactory );
    Folder::setFileSystemFactory( m_fsFactory );
198
    m_thumbnailPath = thumbnailPath;
199 200 201
    m_callback = mlCallback;
    m_dbConnection.reset( new SqliteConnection( dbPath ) );

202 203 204
    // Give a chance to test overloads to reject the creation of a notifier
    startDeletionNotifier();
    // Which allows us to register hooks, or not, depending on the presence of a notifier
205 206
    registerEntityHooks();

207
    if ( createAllTables() == false )
208
    {
209
        LOG_ERROR( "Failed to create database structure" );
210 211
        return false;
    }
212 213 214
    if ( m_settings.load( m_dbConnection.get() ) == false )
        return false;
    if ( m_settings.dbModelVersion() != DbModelVersion )
215 216 217 218
    {
        if ( updateDatabaseModel( m_settings.dbModelVersion() ) == false )
            return false;
    }
219 220
    startDiscoverer();
    startParser();
221
    return true;
222 223
}

224 225 226 227 228 229
void MediaLibrary::setVerbosity(LogLevel v)
{
    m_verbosity = v;
    Log::setLogLevel( v );
}

230
std::vector<MediaPtr> MediaLibrary::audioFiles( medialibrary::SortingCriteria sort, bool desc ) const
231
{
232
    return Media::listAll( this, IMedia::Type::AudioType, sort, desc );
233 234
}

235
std::vector<MediaPtr> MediaLibrary::videoFiles( medialibrary::SortingCriteria sort, bool desc ) const
236
{
237
    return Media::listAll( this, IMedia::Type::VideoType, sort, desc );
238 239
}

240
std::shared_ptr<Media> MediaLibrary::addFile( const fs::IFile& fileFs, Folder& parentFolder, fs::IDirectory& parentFolderFs )
241
{
242
    auto type = IMedia::Type::UnknownType;
243
    auto ext = fileFs.extension();
244 245 246 247 248 249
    auto predicate = [ext](const std::string& v) {
        return strcasecmp(v.c_str(), ext.c_str()) == 0;
    };

    if ( std::find_if( begin( supportedVideoExtensions ), end( supportedVideoExtensions ),
                    predicate ) != end( supportedVideoExtensions ) )
250
    {
251
        type = IMedia::Type::VideoType;
252
    }
253 254
    else if ( std::find_if( begin( supportedAudioExtensions ), end( supportedAudioExtensions ),
                         predicate ) != end( supportedAudioExtensions ) )
255
    {
256
        type = IMedia::Type::AudioType;
257
    }
258
    if ( type == IMedia::Type::UnknownType )
259
        return nullptr;
260

261
    LOG_INFO( "Adding ", fileFs.fullPath() );
262
    auto mptr = Media::create( this, type, fileFs );
263 264
    if ( mptr == nullptr )
    {
265
        LOG_ERROR( "Failed to add media ", fileFs.fullPath(), " to the media library" );
266 267
        return nullptr;
    }
268
    // For now, assume all media are made of a single file
269
    auto file = mptr->addFile( fileFs, parentFolder, parentFolderFs, File::Type::Entire );
270
    if ( file == nullptr )
271
    {
272
        LOG_ERROR( "Failed to add file ", fileFs.fullPath(), " to media #", mptr->id() );
273
        Media::destroy( this, mptr->id() );
274 275
        return nullptr;
    }
276
    m_modificationNotifier->notifyMediaCreation( mptr );
277
    if ( m_parser != nullptr )
278 279
        m_parser->parse( mptr, file );
    return mptr;
280 281
}

282
bool MediaLibrary::deleteFolder( const Folder& folder )
283
{
284
    if ( Folder::destroy( this, folder.id() ) == false )
285
        return false;
286
    Media::clear();
287
    return true;
288 289
}

290
std::shared_ptr<Device> MediaLibrary::device( const std::string& uuid )
291
{
292
    return Device::fromUuid( this, uuid );
293 294
}

295 296
LabelPtr MediaLibrary::createLabel( const std::string& label )
{
297
    return Label::create( this, label );
298
}
299 300 301

bool MediaLibrary::deleteLabel( LabelPtr label )
{
302
    return Label::destroy( this, label->id() );
303
}
304

305
AlbumPtr MediaLibrary::album( int64_t id ) const
306
{
307
    return Album::fetch( this, id );
308 309
}

310
std::shared_ptr<Album> MediaLibrary::createAlbum(const std::string& title )
311
{
312
    return Album::create( this, title );
313 314
}

315
std::vector<AlbumPtr> MediaLibrary::albums( medialibrary::SortingCriteria sort, bool desc ) const
316
{
317
    return Album::listAll( this, sort, desc );
318 319
}

320
std::vector<GenrePtr> MediaLibrary::genres( medialibrary::SortingCriteria sort, bool desc ) const
321
{
322
    return Genre::listAll( this, sort, desc );
323 324
}

325 326 327 328 329
GenrePtr MediaLibrary::genre(int64_t id) const
{
    return Genre::fetch( this, id );
}

330
ShowPtr MediaLibrary::show( const std::string& name ) const
331 332 333
{
    static const std::string req = "SELECT * FROM " + policy::ShowTable::Name
            + " WHERE name = ?";
334
    return Show::fetch( this, req, name );
335 336
}

337
std::shared_ptr<Show> MediaLibrary::createShow(const std::string& name)
338
{
339
    return Show::create( this, name );
340 341
}

342
MoviePtr MediaLibrary::movie( const std::string& title ) const
343 344 345
{
    static const std::string req = "SELECT * FROM " + policy::MovieTable::Name
            + " WHERE title = ?";
346
    return Movie::fetch( this, req, title );
347 348
}

349
std::shared_ptr<Movie> MediaLibrary::createMovie( Media& media, const std::string& title )
350
{
351
    auto movie = Movie::create( this, media.id(), title );
352
    media.setMovie( movie );
353
    media.save();
354
    return movie;
355 356
}

357
ArtistPtr MediaLibrary::artist( int64_t id ) const
358
{
359
    return Artist::fetch( this, id );
360 361 362
}

ArtistPtr MediaLibrary::artist( const std::string& name )
363 364
{
    static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name
365
            + " WHERE name = ? AND is_present = 1";
366
    return Artist::fetch( this, req, name );
367 368
}

369
std::shared_ptr<Artist> MediaLibrary::createArtist( const std::string& name )
370
{
371 372
    try
    {
373
        return Artist::create( this, name );
374 375 376 377 378 379
    }
    catch( sqlite::errors::ConstraintViolation &ex )
    {
        LOG_WARN( "ContraintViolation while creating an artist (", ex.what(), ") attempting to fetch it instead" );
        return std::static_pointer_cast<Artist>( artist( name ) );
    }
380 381
}

382
std::vector<ArtistPtr> MediaLibrary::artists(medialibrary::SortingCriteria sort, bool desc) const
383
{
384
    return Artist::listAll( this, sort, desc );
385 386
}

387 388
PlaylistPtr MediaLibrary::createPlaylist( const std::string& name )
{
389
    return Playlist::create( this, name );
390 391
}

392
std::vector<PlaylistPtr> MediaLibrary::playlists(medialibrary::SortingCriteria sort, bool desc)
393
{
394
    return Playlist::listAll( this, sort, desc );
395 396
}

397 398 399 400 401
PlaylistPtr MediaLibrary::playlist( int64_t id ) const
{
    return Playlist::fetch( this, id );
}

402
bool MediaLibrary::deletePlaylist( int64_t playlistId )
403
{
404
    return Playlist::destroy( this, playlistId );
405
}
406 407 408

bool MediaLibrary::addToHistory( const std::string& mrl )
{
409
    return History::insert( getConn(), mrl );
410 411
}

412
std::vector<HistoryPtr> MediaLibrary::lastStreamsPlayed() const
413
{
414
    return History::fetch( this );
415 416
}

417 418
std::vector<MediaPtr> MediaLibrary::lastMediaPlayed() const
{
419
    return Media::fetchHistory( this );
420 421
}

422
medialibrary::MediaSearchAggregate MediaLibrary::searchMedia( const std::string& title ) const
423
{
424 425
    if ( validateSearchPattern( title ) == false )
        return {};
426
    auto tmp = Media::search( this, title );
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
    medialibrary::MediaSearchAggregate res;
    for ( auto& m : tmp )
    {
        switch ( m->subType() )
        {
        case IMedia::SubType::AlbumTrack:
            res.tracks.emplace_back( std::move( m ) );
            break;
        case IMedia::SubType::Movie:
            res.movies.emplace_back( std::move( m ) );
            break;
        case IMedia::SubType::ShowEpisode:
            res.episodes.emplace_back( std::move( m ) );
            break;
        default:
            res.others.emplace_back( std::move( m ) );
            break;
        }
    }
    return res;
447 448
}

449 450
std::vector<PlaylistPtr> MediaLibrary::searchPlaylists( const std::string& name ) const
{
451 452
    if ( validateSearchPattern( name ) == false )
        return {};
453
    return Playlist::search( this, name );
454 455
}

456 457
std::vector<AlbumPtr> MediaLibrary::searchAlbums( const std::string& pattern ) const
{
458 459
    if ( validateSearchPattern( pattern ) == false )
        return {};
460
    return Album::search( this, pattern );
461 462
}

463 464
std::vector<GenrePtr> MediaLibrary::searchGenre( const std::string& genre ) const
{
465 466
    if ( validateSearchPattern( genre ) == false )
        return {};
467
    return Genre::search( this, genre );
468 469
}

470 471 472 473
std::vector<ArtistPtr> MediaLibrary::searchArtists(const std::string& name ) const
{
    if ( validateSearchPattern( name ) == false )
        return {};
474
    return Artist::search( this, name );
475 476
}

477 478 479 480 481
medialibrary::SearchAggregate MediaLibrary::search( const std::string& pattern ) const
{
    medialibrary::SearchAggregate res;
    res.albums = searchAlbums( pattern );
    res.artists = searchArtists( pattern );
482
    res.genres = searchGenre( pattern );
483 484 485 486 487
    res.media = searchMedia( pattern );
    res.playlists = searchPlaylists( pattern );
    return res;
}

488 489
void MediaLibrary::startParser()
{
490
    m_parser.reset( new Parser( this ) );
491

492
    auto vlcService = std::unique_ptr<VLCMetadataService>( new VLCMetadataService );
493
    auto metadataService = std::unique_ptr<MetadataParser>( new MetadataParser );
494
    auto thumbnailerService = std::unique_ptr<VLCThumbnailer>( new VLCThumbnailer );
495
    m_parser->addService( std::move( vlcService ) );
496
    m_parser->addService( std::move( metadataService ) );
497
    m_parser->addService( std::move( thumbnailerService ) );
498 499 500 501
    m_parser->start();
}

void MediaLibrary::startDiscoverer()
502
{
503
    m_discoverer.reset( new DiscovererWorker( this ) );
504
    m_discoverer->addDiscoverer( std::unique_ptr<IDiscoverer>( new FsDiscoverer( m_fsFactory, this ) ) );
505
    m_discoverer->reload();
506 507
}

508 509
void MediaLibrary::startDeletionNotifier()
{
510 511
    m_modificationNotifier.reset( new ModificationNotifier( this ) );
    m_modificationNotifier->start();
512 513
}

514 515 516 517 518 519 520 521
bool MediaLibrary::updateDatabaseModel( unsigned int previousVersion )
{
    if ( previousVersion == 1 )
    {
        // Way too much differences, introduction of devices, and almost unused in the wild, just drop everything
        std::string req = "PRAGMA writable_schema = 1;"
                            "delete from sqlite_master;"
                            "PRAGMA writable_schema = 0;";
522
        if ( sqlite::Tools::executeRequest( getConn(), req ) == false )
523 524 525 526 527 528 529 530 531 532 533 534 535 536
            return false;
        if ( createAllTables() == false )
            return false;
        ++previousVersion;
    }
    // To be continued in the future!

    // Safety check: ensure we didn't forget a migration along the way
    assert( previousVersion == DbModelVersion );
    m_settings.setDbModelVersion( DbModelVersion );
    m_settings.save();
    return true;
}

537 538 539 540 541 542
void MediaLibrary::reload()
{
    if ( m_discoverer != nullptr )
        m_discoverer->reload();
}

543 544 545 546 547 548
void MediaLibrary::reload( const std::string& entryPoint )
{
    if ( m_discoverer != nullptr )
        m_discoverer->reload( entryPoint );
}

549 550
void MediaLibrary::pauseBackgroundOperations()
{
551 552
    if ( m_parser != nullptr )
        m_parser->pause();
553 554 555 556
}

void MediaLibrary::resumeBackgroundOperations()
{
557 558
    if ( m_parser != nullptr )
        m_parser->resume();
559 560
}

561 562 563 564 565 566 567 568 569 570
DBConnection MediaLibrary::getConn() const
{
    return m_dbConnection.get();
}

IMediaLibraryCb* MediaLibrary::getCb() const
{
    return m_callback;
}

571 572 573 574 575
std::shared_ptr<ModificationNotifier> MediaLibrary::getNotifier() const
{
    return m_modificationNotifier;
}

576 577
void MediaLibrary::discover( const std::string &entryPoint )
{
578 579
    if ( m_discoverer != nullptr )
        m_discoverer->discover( entryPoint );
580 581
}

582
bool MediaLibrary::banFolder( const std::string& path )
583
{
584
    return Folder::blacklist( this, path );
585 586
}

587 588
bool MediaLibrary::unbanFolder( const std::string& path )
{
589
    auto folder = Folder::blacklistedFolder( this, path );
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
    if ( folder == nullptr )
        return false;
    deleteFolder( *folder );

    // We are about to refresh the folder we blacklisted earlier, if we don't have a discoverer, stop early
    if ( m_discoverer == nullptr )
        return true;

    auto parentPath = utils::file::parentDirectory( path );
    // If the parent folder was never added to the media library, the discoverer will reject it.
    // We could check it from here, but that would mean fetching the folder twice, which would be a waste.
    m_discoverer->reload( parentPath );
    return true;
}

605
const std::string& MediaLibrary::thumbnailPath() const
606
{
607
    return m_thumbnailPath;
608 609
}

610 611 612 613 614
void MediaLibrary::setLogger( ILogger* logger )
{
    Log::SetLogger( logger );
}