MediaLibrary.cpp 38.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
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

27
28
#include <algorithm>
#include <functional>
29
#include <utility>
30
#include <sys/stat.h>
31
#include <unistd.h>
32

33
34
#include "Album.h"
#include "AlbumTrack.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
35
#include "Artist.h"
36
#include "AudioTrack.h"
37
#include "discoverer/DiscovererWorker.h"
38
#include "discoverer/probe/CrawlerProbe.h"
39
#include "utils/ModificationsNotifier.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
40
#include "Device.h"
41
#include "File.h"
42
#include "Folder.h"
43
#include "Genre.h"
44
#include "History.h"
45
#include "Media.h"
46
#include "MediaLibrary.h"
47
#include "Label.h"
48
#include "logging/Logger.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
49
#include "Movie.h"
50
#include "parser/Parser.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
51
#include "Playlist.h"
52
53
#include "Show.h"
#include "ShowEpisode.h"
54
#include "database/SqliteTools.h"
55
#include "database/SqliteConnection.h"
56
#include "utils/Filename.h"
57
#include "VideoTrack.h"
58

59
60
61
// Discoverers:
#include "discoverer/FsDiscoverer.h"

62
63
64
// Metadata services:
#include "metadata_services/vlc/VLCMetadataService.h"
#include "metadata_services/vlc/VLCThumbnailer.h"
65
#include "metadata_services/MetadataParser.h"
66

67
68
// FileSystem
#include "factory/DeviceListerFactory.h"
69
#include "factory/FileSystemFactory.h"
70
#include "factory/NetworkFileSystemFactory.h"
71
#include "filesystem/IDevice.h"
72

73
74
75
namespace medialibrary
{

76
const char* const MediaLibrary::supportedExtensions[] = {
77
78
    "3gp", "a52", "aac", "ac3", "aif", "aifc", "aiff", "alac", "amr",
    "amv", "aob", "ape", "asf", "asx", "avi", "b4s", "conf", /*"cue",*/
79
80
81
82
83
84
85
86
87
88
    "divx", "dts", "dv", "flac", "flv", "gxf", "ifo", "iso",
    "it", "itml",  "m1v", "m2t", "m2ts", "m2v", "m3u", "m3u8",
    "m4a", "m4b", "m4p", "m4v", "mid", "mka", "mkv", "mlp",
    "mod", "mov", "mp1", "mp2", "mp3", "mp4", "mpc", "mpeg",
    "mpeg1", "mpeg2", "mpeg4", "mpg", "mts", "mxf", "nsv",
    "nuv", "oga", "ogg", "ogm", "ogv", "ogx", "oma", "opus",
    "pls", "ps", "qtl", "ram", "rec", "rm", "rmi", "rmvb",
    "s3m", "sdp", "spx", "tod", "trp", "ts", "tta", "vlc",
    "vob", "voc", "vqf", "vro", "w64", "wav", "wax", "webm",
    "wma", "wmv", "wmx", "wpl", "wv", "wvx", "xa", "xm", "xspf"
89
90
};

91
92
const size_t MediaLibrary::NbSupportedExtensions = sizeof(supportedExtensions) / sizeof(supportedExtensions[0]);

93
MediaLibrary::MediaLibrary()
94
95
    : m_callback( nullptr )
    , m_verbosity( LogLevel::Error )
96
    , m_settings( this )
97
    , m_initialized( false )
98
99
    , m_discovererIdle( true )
    , m_parserIdle( true )
100
{
101
    Log::setLogLevel( m_verbosity );
102
103
}

104
105
MediaLibrary::~MediaLibrary()
{
106
    // Explicitely stop the discoverer, to avoid it writting while tearing down.
107
108
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->stop();
109
110
    if ( m_parser != nullptr )
        m_parser->stop();
111
112
113
114
115
    clearCache();
}

void MediaLibrary::clearCache()
{
116
    Media::clear();
117
    Folder::clear();
118
119
120
121
122
    Label::clear();
    Album::clear();
    AlbumTrack::clear();
    Show::clear();
    ShowEpisode::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
123
    Movie::clear();
124
    VideoTrack::clear();
125
    AudioTrack::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
126
    Artist::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
127
    Device::clear();
128
    File::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
129
    Playlist::clear();
130
    History::clear();
131
    Genre::clear();
132
133
}

134
void MediaLibrary::createAllTables()
135
{
136
137
138
139
140
    // 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.

141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
    Device::createTable( m_dbConnection.get() );
    Folder::createTable( m_dbConnection.get() );
    Media::createTable( m_dbConnection.get() );
    File::createTable( m_dbConnection.get() );
    Label::createTable( m_dbConnection.get() );
    Playlist::createTable( m_dbConnection.get() );
    Genre::createTable( m_dbConnection.get() );
    Album::createTable( m_dbConnection.get() );
    AlbumTrack::createTable( m_dbConnection.get() );
    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() );
    Artist::createTable( m_dbConnection.get() );
    Artist::createDefaultArtists( m_dbConnection.get() );
157
158
    History::createTable( m_dbConnection.get() );
    Settings::createTable( m_dbConnection.get() );
159
    parser::Task::createTable( m_dbConnection.get() );
160
161
162
163
}

void MediaLibrary::createAllTriggers()
{
164
    auto dbModelVersion = m_settings.dbModelVersion();
165
    Album::createTriggers( m_dbConnection.get() );
166
    Artist::createTriggers( m_dbConnection.get(), dbModelVersion );
167
168
169
    Media::createTriggers( m_dbConnection.get() );
    Genre::createTriggers( m_dbConnection.get() );
    Playlist::createTriggers( m_dbConnection.get() );
170
171
}

172
template <typename T>
173
static void propagateDeletionToCache( sqlite::Connection::HookReason reason, int64_t rowId )
174
{
175
    if ( reason != sqlite::Connection::HookReason::Delete )
176
177
178
179
        return;
    T::removeFromCache( rowId );
}

180
181
void MediaLibrary::registerEntityHooks()
{
182
    if ( m_modificationNotifier == nullptr )
183
184
        return;

185
    m_dbConnection->registerUpdateHook( policy::MediaTable::Name,
186
187
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
188
189
            return;
        Media::removeFromCache( rowId );
190
        m_modificationNotifier->notifyMediaRemoval( rowId );
191
    });
192
    m_dbConnection->registerUpdateHook( policy::ArtistTable::Name,
193
194
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
195
196
197
198
            return;
        Artist::removeFromCache( rowId );
        m_modificationNotifier->notifyArtistRemoval( rowId );
    });
199
    m_dbConnection->registerUpdateHook( policy::AlbumTable::Name,
200
201
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
202
203
204
205
            return;
        Album::removeFromCache( rowId );
        m_modificationNotifier->notifyAlbumRemoval( rowId );
    });
206
    m_dbConnection->registerUpdateHook( policy::AlbumTrackTable::Name,
207
208
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
209
210
211
212
            return;
        AlbumTrack::removeFromCache( rowId );
        m_modificationNotifier->notifyAlbumTrackRemoval( rowId );
    });
213
    m_dbConnection->registerUpdateHook( policy::PlaylistTable::Name,
214
215
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
216
217
218
219
            return;
        Playlist::removeFromCache( rowId );
        m_modificationNotifier->notifyPlaylistRemoval( rowId );
    });
220
221
222
223
224
225
226
227
228
229
    m_dbConnection->registerUpdateHook( policy::DeviceTable::Name, &propagateDeletionToCache<Device> );
    m_dbConnection->registerUpdateHook( policy::FileTable::Name, &propagateDeletionToCache<File> );
    m_dbConnection->registerUpdateHook( policy::FolderTable::Name, &propagateDeletionToCache<Folder> );
    m_dbConnection->registerUpdateHook( policy::GenreTable::Name, &propagateDeletionToCache<Genre> );
    m_dbConnection->registerUpdateHook( policy::LabelTable::Name, &propagateDeletionToCache<Label> );
    m_dbConnection->registerUpdateHook( policy::MovieTable::Name, &propagateDeletionToCache<Movie> );
    m_dbConnection->registerUpdateHook( policy::ShowTable::Name, &propagateDeletionToCache<Show> );
    m_dbConnection->registerUpdateHook( policy::ShowEpisodeTable::Name, &propagateDeletionToCache<ShowEpisode> );
    m_dbConnection->registerUpdateHook( policy::AudioTrackTable::Name, &propagateDeletionToCache<AudioTrack> );
    m_dbConnection->registerUpdateHook( policy::VideoTrackTable::Name, &propagateDeletionToCache<VideoTrack> );
230
231
}

232
233
234
235
236
bool MediaLibrary::validateSearchPattern( const std::string& pattern )
{
    return pattern.size() >= 3;
}

237
238
239
InitializeResult MediaLibrary::initialize( const std::string& dbPath,
                                           const std::string& thumbnailPath,
                                           IMediaLibraryCb* mlCallback )
240
{
241
    LOG_INFO( "Initializing medialibrary..." );
242
    if ( m_initialized == true )
243
244
    {
        LOG_INFO( "...Already initialized" );
245
        return InitializeResult::AlreadyInitialized;
246
    }
247
248
249
250
    if ( m_deviceLister == nullptr )
    {
        m_deviceLister = factory::createDeviceLister();
        if ( m_deviceLister == nullptr )
251
252
        {
            LOG_ERROR( "No available IDeviceLister was found." );
253
            return InitializeResult::Failed;
254
        }
255
    }
256
    addLocalFsFactory();
257
258
259
#ifdef _WIN32
    if ( mkdir( thumbnailPath.c_str() ) != 0 )
#else
260
    if ( mkdir( thumbnailPath.c_str(), S_IRWXU ) != 0 )
261
#endif
262
263
    {
        if ( errno != EEXIST )
264
265
        {
            LOG_ERROR( "Failed to create thumbnail directory: ", strerror( errno ) );
266
            return InitializeResult::Failed;
267
        }
268
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
269
    m_thumbnailPath = thumbnailPath;
270
    m_callback = mlCallback;
271
    m_dbConnection = sqlite::Connection::connect( dbPath );
272

273
274
275
    // 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
276
277
    registerEntityHooks();

278
    auto res = InitializeResult::Success;
279
    try
280
    {
281
        auto t = m_dbConnection->newTransaction();
282
        createAllTables();
283
        if ( m_settings.load() == false )
284
285
        {
            LOG_ERROR( "Failed to load settings" );
286
            return InitializeResult::Failed;
287
        }
288
289
290
        createAllTriggers();
        t->commit();

291
292
        if ( m_settings.dbModelVersion() != Settings::DbModelVersion )
        {
293
294
            res = updateDatabaseModel( m_settings.dbModelVersion(), dbPath );
            if ( res == InitializeResult::Failed )
295
296
            {
                LOG_ERROR( "Failed to update database model" );
297
                return res;
298
299
300
301
302
303
            }
        }
    }
    catch ( const sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Can't initialize medialibrary: ", ex.what() );
304
        return InitializeResult::Failed;
305
    }
306
307
    m_initialized = true;
    LOG_INFO( "Successfuly initialized" );
308
    return res;
309
310
311
312
}

bool MediaLibrary::start()
{
313
    assert( m_initialized == true );
314
315
316
    if ( m_parser != nullptr )
        return false;

317
318
    for ( auto& fsFactory : m_fsFactories )
        refreshDevices( *fsFactory );
319
    startDiscoverer();
320
    startParser();
321
    return true;
322
323
}

324
void MediaLibrary::setVerbosity( LogLevel v )
325
326
327
328
329
{
    m_verbosity = v;
    Log::setLogLevel( v );
}

330
331
332
333
334
MediaPtr MediaLibrary::media( int64_t mediaId ) const
{
    return Media::fetch( this, mediaId );
}

335
336
MediaPtr MediaLibrary::media( const std::string& mrl ) const
{
337
    LOG_INFO( "Fetching media from mrl: ", mrl );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
338
    auto file = File::fromExternalMrl( this, mrl );
339
340
341
342
343
    if ( file != nullptr )
    {
        LOG_INFO( "Found external media: ", mrl );
        return file->media();
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
344
    auto fsFactory = fsFactoryForMrl( mrl );
345
    if ( fsFactory == nullptr )
346
    {
347
348
        LOG_WARN( "Failed to create FS factory for path ", mrl );
        return nullptr;
349
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
350
    auto device = fsFactory->createDeviceFromMrl( mrl );
351
352
353
354
355
356
    if ( device == nullptr )
    {
        LOG_WARN( "Failed to create a device associated with mrl ", mrl );
        return nullptr;
    }
    if ( device->isRemovable() == false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
357
        file = File::fromMrl( this, mrl );
358
359
    else
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
360
        auto folder = Folder::fromMrl( this, utils::file::directory( mrl ) );
361
362
363
364
365
366
367
368
369
370
371
372
373
374
        if ( folder == nullptr )
        {
            LOG_WARN( "Failed to find folder containing ", mrl );
            return nullptr;
        }
        if ( folder->isPresent() == false )
        {
            LOG_INFO( "Found a folder containing ", mrl, " but it is not present" );
            return nullptr;
        }
        file = File::fromFileName( this, utils::file::fileName( mrl ), folder->id() );
    }
    if ( file == nullptr )
    {
375
        LOG_WARN( "Failed to fetch file for ", mrl, " (device ", device->uuid(), " was ",
376
                  device->isRemovable() ? "" : "NOT ", "removable)");
377
378
379
380
381
        return nullptr;
    }
    return file->media();
}

382
383
MediaPtr MediaLibrary::addMedia( const std::string& mrl )
{
384
385
386
387
    try
    {
        return sqlite::Tools::withRetries( 3, [this, &mrl]() -> MediaPtr {
            auto t = m_dbConnection->newTransaction();
388
            auto media = Media::create( this, IMedia::Type::External, utils::file::fileName( mrl ) );
389
390
391
392
393
394
395
396
397
398
399
400
401
            if ( media == nullptr )
                return nullptr;
            if ( media->addExternalMrl( mrl, IFile::Type::Main ) == nullptr )
                return nullptr;
            t->commit();
            return media;
        });
    }
    catch ( const sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Failed to create external media: ", ex.what() );
        return nullptr;
    }
402
403
}

404
std::vector<MediaPtr> MediaLibrary::audioFiles( SortingCriteria sort, bool desc ) const
405
{
406
    return Media::listAll( this, IMedia::Type::Audio, sort, desc );
407
408
}

409
std::vector<MediaPtr> MediaLibrary::videoFiles( SortingCriteria sort, bool desc ) const
410
{
411
    return Media::listAll( this, IMedia::Type::Video, sort, desc );
412
413
}

414
415
416
417
418
419
420
421
bool MediaLibrary::isExtensionSupported( const char* ext )
{
    return std::binary_search( std::begin( supportedExtensions ),
        std::end( supportedExtensions ), ext, [](const char* l, const char* r) {
            return strcasecmp( l, r ) < 0;
        });
}

422
423
void MediaLibrary::addDiscoveredFile( std::shared_ptr<fs::IFile> fileFs,
                                      std::shared_ptr<Folder> parentFolder,
424
425
                                      std::shared_ptr<fs::IDirectory> parentFolderFs,
                                      std::pair<std::shared_ptr<Playlist>, unsigned int> parentPlaylist )
426
{
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
    try
    {
        // Don't move the file as we might need it for error handling
        auto task = parser::Task::create( this, fileFs, std::move( parentFolder ),
                                          std::move( parentFolderFs ), std::move( parentPlaylist ) );
        if ( task != nullptr && m_parser != nullptr )
            m_parser->parse( task );
    }
    catch(sqlite::errors::ConstraintViolation& ex)
    {
        // Most likely the file is already scheduled and we restarted the
        // discovery after a crash.
        LOG_WARN( "Failed to insert ", fileFs->mrl(), ": ", ex.what(), ". "
                  "Assuming the file is already scheduled for discovery" );
    }
442
443
}

444
bool MediaLibrary::deleteFolder( const Folder& folder )
445
{
446
    LOG_INFO( "deleting folder ", folder.mrl() );
447
    if ( Folder::destroy( this, folder.id() ) == false )
448
        return false;
449
    Media::clear();
450
    return true;
451
452
}

453
454
LabelPtr MediaLibrary::createLabel( const std::string& label )
{
455
456
457
458
459
460
461
462
463
    try
    {
        return Label::create( this, label );
    }
    catch ( const sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Failed to create a label: ", ex.what() );
        return nullptr;
    }
464
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
465
466
467

bool MediaLibrary::deleteLabel( LabelPtr label )
{
468
469
470
471
472
473
474
475
476
    try
    {
        return Label::destroy( this, label->id() );
    }
    catch ( const sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Failed to delete label: ", ex.what() );
        return false;
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
477
}
478

479
AlbumPtr MediaLibrary::album( int64_t id ) const
480
{
481
    return Album::fetch( this, id );
482
483
}

484
std::shared_ptr<Album> MediaLibrary::createAlbum( const std::string& title, const std::string& artworkMrl )
485
{
486
    return Album::create( this, title, artworkMrl );
487
488
}

489
std::vector<AlbumPtr> MediaLibrary::albums( SortingCriteria sort, bool desc ) const
490
{
491
    return Album::listAll( this, sort, desc );
492
493
}

494
std::vector<GenrePtr> MediaLibrary::genres( SortingCriteria sort, bool desc ) const
495
{
496
    return Genre::listAll( this, sort, desc );
497
498
}

499
GenrePtr MediaLibrary::genre( int64_t id ) const
500
501
502
503
{
    return Genre::fetch( this, id );
}

504
ShowPtr MediaLibrary::show( const std::string& name ) const
505
506
507
{
    static const std::string req = "SELECT * FROM " + policy::ShowTable::Name
            + " WHERE name = ?";
508
    return Show::fetch( this, req, name );
509
510
}

511
std::shared_ptr<Show> MediaLibrary::createShow( const std::string& name )
512
{
513
    return Show::create( this, name );
514
515
}

516
MoviePtr MediaLibrary::movie( const std::string& title ) const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
517
518
519
{
    static const std::string req = "SELECT * FROM " + policy::MovieTable::Name
            + " WHERE title = ?";
520
    return Movie::fetch( this, req, title );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
521
522
}

523
std::shared_ptr<Movie> MediaLibrary::createMovie( Media& media, const std::string& title )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
524
{
525
    auto movie = Movie::create( this, media.id(), title );
526
    media.setMovie( movie );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
527
    media.save();
528
    return movie;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
529
530
}

531
ArtistPtr MediaLibrary::artist( int64_t id ) const
532
{
533
    return Artist::fetch( this, id );
534
535
536
}

ArtistPtr MediaLibrary::artist( const std::string& name )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
537
538
{
    static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name
539
            + " WHERE name = ? AND is_present != 0";
540
    return Artist::fetch( this, req, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
541
542
}

543
std::shared_ptr<Artist> MediaLibrary::createArtist( const std::string& name )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
544
{
545
546
    try
    {
547
        return Artist::create( this, name );
548
549
550
551
552
553
    }
    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 ) );
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
554
555
}

556
557
558
std::vector<ArtistPtr> MediaLibrary::artists( bool includeAll,
                                              SortingCriteria sort,
                                              bool desc ) const
559
{
560
    return Artist::listAll( this, includeAll, sort, desc );
561
562
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
563
564
PlaylistPtr MediaLibrary::createPlaylist( const std::string& name )
{
565
566
567
568
569
570
571
572
573
    try
    {
        return Playlist::create( this, name );
    }
    catch ( const sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Failed to create a playlist: ", ex.what() );
        return nullptr;
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
574
575
}

576
std::vector<PlaylistPtr> MediaLibrary::playlists( SortingCriteria sort, bool desc )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
577
{
578
    return Playlist::listAll( this, sort, desc );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
579
580
}

581
582
583
584
585
PlaylistPtr MediaLibrary::playlist( int64_t id ) const
{
    return Playlist::fetch( this, id );
}

586
bool MediaLibrary::deletePlaylist( int64_t playlistId )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
587
{
588
589
590
591
592
593
594
595
596
    try
    {
        return Playlist::destroy( this, playlistId );
    }
    catch ( const sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Failed to delete playlist: ", ex.what() );
        return false;
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
597
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
598

599
bool MediaLibrary::addToStreamHistory( MediaPtr media )
600
{
601
602
603
604
605
606
607
608
609
    try
    {
        return History::insert( getConn(), media->id() );
    }
    catch ( const sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Failed to add stream to history: ", ex.what() );
        return false;
    }
610
611
612
613
614
615
616
}

std::vector<HistoryPtr> MediaLibrary::lastStreamsPlayed() const
{
    return History::fetch( this );
}

617
618
std::vector<MediaPtr> MediaLibrary::lastMediaPlayed() const
{
619
    return Media::fetchHistory( this );
620
621
}

622
623
bool MediaLibrary::clearHistory()
{
624
625
626
627
628
    try
    {
        return sqlite::Tools::withRetries( 3, [this]() {
            auto t = getConn()->newTransaction();
            Media::clearHistory( this );
629
            History::clearStreams( this );
630
631
632
633
634
635
636
637
638
            t->commit();
            return true;
        });
    }
    catch ( sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Failed to clear history: ", ex.what() );
        return false;
    }
639
640
}

641
MediaSearchAggregate MediaLibrary::searchMedia( const std::string& title ) const
642
{
643
644
    if ( validateSearchPattern( title ) == false )
        return {};
645
    auto tmp = Media::search( this, title );
646
    MediaSearchAggregate res;
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
    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;
666
667
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
668
669
std::vector<PlaylistPtr> MediaLibrary::searchPlaylists( const std::string& name ) const
{
670
671
    if ( validateSearchPattern( name ) == false )
        return {};
672
    return Playlist::search( this, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
673
674
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
675
676
std::vector<AlbumPtr> MediaLibrary::searchAlbums( const std::string& pattern ) const
{
677
678
    if ( validateSearchPattern( pattern ) == false )
        return {};
679
    return Album::search( this, pattern );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
680
681
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
682
683
std::vector<GenrePtr> MediaLibrary::searchGenre( const std::string& genre ) const
{
684
685
    if ( validateSearchPattern( genre ) == false )
        return {};
686
    return Genre::search( this, genre );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
687
688
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
689
690
691
692
std::vector<ArtistPtr> MediaLibrary::searchArtists(const std::string& name ) const
{
    if ( validateSearchPattern( name ) == false )
        return {};
693
    return Artist::search( this, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
694
695
}

696
SearchAggregate MediaLibrary::search( const std::string& pattern ) const
697
{
698
    SearchAggregate res;
699
700
    res.albums = searchAlbums( pattern );
    res.artists = searchArtists( pattern );
701
    res.genres = searchGenre( pattern );
702
703
704
705
706
    res.media = searchMedia( pattern );
    res.playlists = searchPlaylists( pattern );
    return res;
}

707
708
void MediaLibrary::startParser()
{
709
    m_parser.reset( new Parser( this ) );
710

711
    auto vlcService = std::unique_ptr<VLCMetadataService>( new VLCMetadataService );
712
    auto metadataService = std::unique_ptr<MetadataParser>( new MetadataParser );
713
    auto thumbnailerService = std::unique_ptr<VLCThumbnailer>( new VLCThumbnailer );
714
    m_parser->addService( std::move( vlcService ) );
715
    m_parser->addService( std::move( metadataService ) );
716
    m_parser->addService( std::move( thumbnailerService ) );
717
718
719
    m_parser->start();
}

720
void MediaLibrary::startDiscoverer()
721
{
722
    m_discovererWorker.reset( new DiscovererWorker( this ) );
723
    for ( const auto& fsFactory : m_fsFactories )
724
725
726
727
728
    {
        auto probePtr = std::unique_ptr<prober::CrawlerProbe>( new prober::CrawlerProbe{} );
        m_discovererWorker->addDiscoverer( std::unique_ptr<IDiscoverer>( new FsDiscoverer( fsFactory, this, m_callback,
                                                                                           std::move ( probePtr ) ) ) );
    }
729
730
}

731
732
void MediaLibrary::startDeletionNotifier()
{
733
734
    m_modificationNotifier.reset( new ModificationNotifier( this ) );
    m_modificationNotifier->start();
735
736
}

737
738
739
740
741
void MediaLibrary::addLocalFsFactory()
{
    m_fsFactories.insert( begin( m_fsFactories ), std::make_shared<factory::FileSystemFactory>( m_deviceLister ) );
}

742
InitializeResult MediaLibrary::updateDatabaseModel( unsigned int previousVersion,
743
                                        const std::string& dbPath )
744
{
745
746
747
    LOG_INFO( "Updating database model from ", previousVersion, " to ", Settings::DbModelVersion );
    // Up until model 3, it's safer (and potentially more efficient with index changes) to drop the DB
    // It's also way simpler to implement
748
    // In case of downgrade, just recreate the database
749
    for ( auto i = 0u; i < 3; ++i )
750
    {
751
752
753
754
755
        try
        {
            // Up until model 3, it's safer (and potentially more efficient with index changes) to drop the DB
            // It's also way simpler to implement
            // In case of downgrade, just recreate the database
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
756
757
            // We might also have some special cases for failed upgrade (see
            // comments below for per-version details)
758
            if ( previousVersion < 3 ||
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
759
760
                 previousVersion > Settings::DbModelVersion ||
                 previousVersion == 4 )
761
            {
762
                if( recreateDatabase( dbPath ) == false )
763
                    throw std::runtime_error( "Failed to recreate the database" );
764
                return InitializeResult::DbReset;
765
            }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
766
767
768
769
770
771
772
773
            /**
             * Migration from 3 to 4 didn't happen so well and broke a few
             * users DB. So:
             * - Any v4 database will be dropped and recreated in v5
             * - Any v3 database will be upgraded to v5
             * V4 database is only used by VLC-android 2.5.6 // 2.5.8, which are
             * beta versions.
             */
774
775
            if ( previousVersion == 3 )
            {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
776
777
778
                if ( migrateModel3to5() == false )
                    throw std::logic_error( "Failed to migrate from 3 to 5" );
                previousVersion = 5;
779
            }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
780
781
782
783
784
785
            if ( previousVersion == 5 )
            {
                if ( migrateModel5to6() == false )
                    throw std::logic_error( "Failed to migrate from 5 to 6" );
                previousVersion = 6;
            }
Alexandre Fernandez's avatar
Alexandre Fernandez committed
786
787
            if ( previousVersion == 6 )
            {
788
789
790
                // Force a rescan to solve metadata analysis problems.
                // The insertion is fixed, but won't edit already inserted data.
                forceRescan();
Alexandre Fernandez's avatar
Alexandre Fernandez committed
791
792
                previousVersion = 7;
            }
793
794
795
796
797
798
799
800
801
            /**
             * V7 introduces artist.nb_tracks and an associated trigger to delete
             * artists when it has no track/album left.
             */
            if ( previousVersion == 7 )
            {
                migrateModel7to8();
                previousVersion = 8;
            }
802
803
804
805
806
807
808
809
810
811
812
813
            if ( previousVersion == 8 )
            {
                // Multiple changes justify the rescan:
                // - Changes in the way we chose to encode or not MRL, meaning
                //   potentially all MRL are wrong (more precisely, will
                //   mismatch what VLC expects, which makes playlist analysis
                //   break.
                // - Fix in the way we chose album candidates, meaning some
                //   albums were likely to be wrongfully created.
                forceRescan();
                previousVersion = 9;
            }
814
815
816
817
818
            // To be continued in the future!

            // Safety check: ensure we didn't forget a migration along the way
            assert( previousVersion == Settings::DbModelVersion );
            m_settings.setDbModelVersion( Settings::DbModelVersion );
819
            if ( m_settings.save() == false )
820
821
                return InitializeResult::Failed;
            return InitializeResult::Success;
822
823
824
825
826
827
828
829
830
831
832
        }
        catch( const std::exception& ex )
        {
            LOG_ERROR( "An error occured during the database upgrade: ",
                       ex.what() );
        }
        catch( ... )
        {
            LOG_ERROR( "An unknown error occured during the database upgrade." );
        }
        LOG_WARN( "Retrying database migration, attempt ", i + 1, " / 3" );
833
    }
834
835
    LOG_ERROR( "Failed to upgrade database, recreating it" );
    for ( auto i = 0u; i < 3; ++i )
836
    {
837
838
        try
        {
839
            if( recreateDatabase( dbPath ) == true )
840
                return InitializeResult::DbReset;
841
842
843
844
845
846
847
848
849
850
        }
        catch( const std::exception& ex )
        {
            LOG_ERROR( "Failed to recreate database: ", ex.what() );
        }
        catch(...)
        {
            LOG_ERROR( "Unknown error while trying to recreate the database." );
        }
        LOG_WARN( "Retrying to recreate the database, attempt ", i + 1, " / 3" );
851
    }
852
    return InitializeResult::Failed;
853
854
}

855
bool MediaLibrary::recreateDatabase( const std::string& dbPath )
856
{
857
858
859
860
    // Close all active connections, flushes all previously run statements.
    m_dbConnection.reset();
    unlink( dbPath.c_str() );
    m_dbConnection = sqlite::Connection::connect( dbPath );
861
    createAllTables();
862
863
864
    // We dropped the database, there is no setting to be read anymore
    if( m_settings.load() == false )
        return false;
865
866
867
    return true;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
868
bool MediaLibrary::migrateModel3to5()
869
870
{
    /*
871
872
     * Disable Foreign Keys & recursive triggers to avoid cascading deletion
     * while remodeling the database into the transaction.
873
     */
874
    sqlite::Connection::WeakDbContext weakConnCtx{ getConn() };
875
    auto t = getConn()->newTransaction();
876
877
878
879
    using namespace policy;
    // As SQLite do not allow us to remove or add some constraints,
    // we use the method described here https://www.sqlite.org/faq.html#q11
    std::string reqs[] = {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
880
#               include "database/migrations/migration3-5.sql"
881
882
883
    };

    for ( const auto& req : reqs )
884
        sqlite::Tools::executeRequest( getConn(), req );
885
    // Re-create triggers removed in the process
886
887
    Media::createTriggers( getConn() );
    Playlist::createTriggers( getConn() );
888
889
890
891
    t->commit();
    return true;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
892
893
bool MediaLibrary::migrateModel5to6()
{
894
895
896
    std::string req = "DELETE FROM " + policy::MediaTable::Name + " WHERE type = ?";
    sqlite::Tools::executeRequest( getConn(), req, Media::Type::Unknown );

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
897
898
    sqlite::Connection::WeakDbContext weakConnCtx{ getConn() };
    using namespace policy;
899
    req = "UPDATE " + MediaTable::Name + " SET is_present = 1 WHERE is_present != 0";
900
    sqlite::Tools::executeRequest( getConn(), req );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
901

Alexandre Fernandez's avatar
Alexandre Fernandez committed
902
903
904
    return true;
}

905
906
907
908
909
910
911
912
913
914
915
916
void MediaLibrary::migrateModel7to8()
{
    sqlite::Connection::WeakDbContext weakConnCtx{ getConn() };
    auto t = getConn()->newTransaction();
    using namespace policy;
    std::string reqs[] = {
#               include "database/migrations/migration7-8.sql"
    };

    for ( const auto& req : reqs )
        sqlite::Tools::executeRequest( getConn(), req );
    // Re-create triggers removed in the process
917
    Artist::createTriggers( getConn(), 8u );
918
919
920
    t->commit();
}

921
922
void MediaLibrary::reload()
{
923
924
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->reload();
925
926
}

927
928
void MediaLibrary::reload( const std::string& entryPoint )
{
929
930
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->reload( entryPoint );
931
932
}

933
bool MediaLibrary::forceParserRetry()
934
{
935
936
    try
    {
937
        parser::Task::resetRetryCount( this );
938
939
940
941
942
943
944
        return true;
    }
    catch ( const sqlite::errors::Generic& ex )
    {
        LOG_ERROR( "Failed to force parser retry: ", ex.what() );
        return false;
    }
945
946
}

947
948
void MediaLibrary::pauseBackgroundOperations()
{
949
950
    if ( m_parser != nullptr )
        m_parser->pause();
951
952
953
954
}

void MediaLibrary::resumeBackgroundOperations()
{
955
956
    if ( m_parser != nullptr )
        m_parser