MediaLibrary.cpp 38.5 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
    Folder::createTriggers( m_dbConnection.get() );
166
    Album::createTriggers( m_dbConnection.get() );
167
    AlbumTrack::createTriggers( m_dbConnection.get() );
168
    Artist::createTriggers( m_dbConnection.get(), dbModelVersion );
169
    Media::createTriggers( m_dbConnection.get() );
170
    File::createTriggers( m_dbConnection.get() );
171
172
    Genre::createTriggers( m_dbConnection.get() );
    Playlist::createTriggers( m_dbConnection.get() );
173
    History::createTriggers( m_dbConnection.get() );
174
    Label::createTriggers( m_dbConnection.get() );
175
176
}

177
template <typename T>
178
static void propagateDeletionToCache( sqlite::Connection::HookReason reason, int64_t rowId )
179
{
180
    if ( reason != sqlite::Connection::HookReason::Delete )
181
182
183
184
        return;
    T::removeFromCache( rowId );
}

185
186
void MediaLibrary::registerEntityHooks()
{
187
    if ( m_modificationNotifier == nullptr )
188
189
        return;

190
    m_dbConnection->registerUpdateHook( policy::MediaTable::Name,
191
192
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
193
194
            return;
        Media::removeFromCache( rowId );
195
        m_modificationNotifier->notifyMediaRemoval( rowId );
196
    });
197
    m_dbConnection->registerUpdateHook( policy::ArtistTable::Name,
198
199
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
200
201
202
203
            return;
        Artist::removeFromCache( rowId );
        m_modificationNotifier->notifyArtistRemoval( rowId );
    });
204
    m_dbConnection->registerUpdateHook( policy::AlbumTable::Name,
205
206
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
207
208
209
210
            return;
        Album::removeFromCache( rowId );
        m_modificationNotifier->notifyAlbumRemoval( rowId );
    });
211
    m_dbConnection->registerUpdateHook( policy::AlbumTrackTable::Name,
212
213
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
214
215
216
217
            return;
        AlbumTrack::removeFromCache( rowId );
        m_modificationNotifier->notifyAlbumTrackRemoval( rowId );
    });
218
    m_dbConnection->registerUpdateHook( policy::PlaylistTable::Name,
219
220
                                        [this]( sqlite::Connection::HookReason reason, int64_t rowId ) {
        if ( reason != sqlite::Connection::HookReason::Delete )
221
222
223
224
            return;
        Playlist::removeFromCache( rowId );
        m_modificationNotifier->notifyPlaylistRemoval( rowId );
    });
225
226
227
228
229
230
231
232
233
234
    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> );
235
236
}

237
238
239
240
241
bool MediaLibrary::validateSearchPattern( const std::string& pattern )
{
    return pattern.size() >= 3;
}

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

278
279
280
    // 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
281
282
    registerEntityHooks();

283
    auto res = InitializeResult::Success;
284
    try
285
    {
286
        auto t = m_dbConnection->newTransaction();
287
        createAllTables();
288
        if ( m_settings.load() == false )
289
290
        {
            LOG_ERROR( "Failed to load settings" );
291
            return InitializeResult::Failed;
292
        }
293
294
295
        createAllTriggers();
        t->commit();

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

bool MediaLibrary::start()
{
318
    assert( m_initialized == true );
319
320
321
    if ( m_parser != nullptr )
        return false;

322
323
    for ( auto& fsFactory : m_fsFactories )
        refreshDevices( *fsFactory );
324
    startDiscoverer();
325
    startParser();
326
    return true;
327
328
}

329
void MediaLibrary::setVerbosity( LogLevel v )
330
331
332
333
334
{
    m_verbosity = v;
    Log::setLogLevel( v );
}

335
336
337
338
339
MediaPtr MediaLibrary::media( int64_t mediaId ) const
{
    return Media::fetch( this, mediaId );
}

340
341
MediaPtr MediaLibrary::media( const std::string& mrl ) const
{
342
    LOG_INFO( "Fetching media from mrl: ", mrl );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
343
    auto file = File::fromExternalMrl( this, mrl );
344
345
346
347
348
    if ( file != nullptr )
    {
        LOG_INFO( "Found external media: ", mrl );
        return file->media();
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
349
    auto fsFactory = fsFactoryForMrl( mrl );
350
    if ( fsFactory == nullptr )
351
    {
352
353
        LOG_WARN( "Failed to create FS factory for path ", mrl );
        return nullptr;
354
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
355
    auto device = fsFactory->createDeviceFromMrl( mrl );
356
357
358
359
360
361
    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
362
        file = File::fromMrl( this, mrl );
363
364
    else
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
365
        auto folder = Folder::fromMrl( this, utils::file::directory( mrl ) );
366
367
368
369
370
371
372
373
374
375
376
377
378
379
        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 )
    {
380
        LOG_WARN( "Failed to fetch file for ", mrl, " (device ", device->uuid(), " was ",
381
                  device->isRemovable() ? "" : "NOT ", "removable)");
382
383
384
385
386
        return nullptr;
    }
    return file->media();
}

387
388
MediaPtr MediaLibrary::addMedia( const std::string& mrl )
{
389
390
391
392
    try
    {
        return sqlite::Tools::withRetries( 3, [this, &mrl]() -> MediaPtr {
            auto t = m_dbConnection->newTransaction();
393
            auto media = Media::create( this, IMedia::Type::External, utils::file::fileName( mrl ) );
394
395
396
397
398
399
400
401
402
403
404
405
406
            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;
    }
407
408
}

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

414
std::vector<MediaPtr> MediaLibrary::videoFiles( SortingCriteria sort, bool desc ) const
415
{
416
    return Media::listAll( this, IMedia::Type::Video, sort, desc );
417
418
}

419
420
421
422
423
424
425
426
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;
        });
}

427
428
void MediaLibrary::addDiscoveredFile( std::shared_ptr<fs::IFile> fileFs,
                                      std::shared_ptr<Folder> parentFolder,
429
430
                                      std::shared_ptr<fs::IDirectory> parentFolderFs,
                                      std::pair<std::shared_ptr<Playlist>, unsigned int> parentPlaylist )
431
{
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
    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" );
    }
447
448
}

449
bool MediaLibrary::deleteFolder( const Folder& folder )
450
{
451
    LOG_INFO( "deleting folder ", folder.mrl() );
452
    if ( Folder::destroy( this, folder.id() ) == false )
453
        return false;
454
    Media::clear();
455
    return true;
456
457
}

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

bool MediaLibrary::deleteLabel( LabelPtr label )
{
473
474
475
476
477
478
479
480
481
    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
482
}
483

484
AlbumPtr MediaLibrary::album( int64_t id ) const
485
{
486
    return Album::fetch( this, id );
487
488
}

489
std::shared_ptr<Album> MediaLibrary::createAlbum( const std::string& title, const std::string& artworkMrl )
490
{
491
    return Album::create( this, title, artworkMrl );
492
493
}

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

499
std::vector<GenrePtr> MediaLibrary::genres( SortingCriteria sort, bool desc ) const
500
{
501
    return Genre::listAll( this, sort, desc );
502
503
}

504
GenrePtr MediaLibrary::genre( int64_t id ) const
505
506
507
508
{
    return Genre::fetch( this, id );
}

509
ShowPtr MediaLibrary::show( const std::string& name ) const
510
511
512
{
    static const std::string req = "SELECT * FROM " + policy::ShowTable::Name
            + " WHERE name = ?";
513
    return Show::fetch( this, req, name );
514
515
}

516
std::shared_ptr<Show> MediaLibrary::createShow( const std::string& name )
517
{
518
    return Show::create( this, name );
519
520
}

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

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

536
ArtistPtr MediaLibrary::artist( int64_t id ) const
537
{
538
    return Artist::fetch( this, id );
539
540
541
}

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

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

561
562
563
std::vector<ArtistPtr> MediaLibrary::artists( bool includeAll,
                                              SortingCriteria sort,
                                              bool desc ) const
564
{
565
    return Artist::listAll( this, includeAll, sort, desc );
566
567
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
568
569
PlaylistPtr MediaLibrary::createPlaylist( const std::string& name )
{
570
571
572
573
574
575
576
577
578
    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
579
580
}

581
std::vector<PlaylistPtr> MediaLibrary::playlists( SortingCriteria sort, bool desc )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
582
{
583
    return Playlist::listAll( this, sort, desc );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
584
585
}

586
587
588
589
590
PlaylistPtr MediaLibrary::playlist( int64_t id ) const
{
    return Playlist::fetch( this, id );
}

591
bool MediaLibrary::deletePlaylist( int64_t playlistId )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
592
{
593
594
595
596
597
598
599
600
601
    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
602
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
603

604
bool MediaLibrary::addToStreamHistory( MediaPtr media )
605
{
606
607
608
609
610
611
612
613
614
    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;
    }
615
616
617
618
619
620
621
}

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

622
623
std::vector<MediaPtr> MediaLibrary::lastMediaPlayed() const
{
624
    return Media::fetchHistory( this );
625
626
}

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

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

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
673
674
std::vector<PlaylistPtr> MediaLibrary::searchPlaylists( const std::string& name ) const
{
675
676
    if ( validateSearchPattern( name ) == false )
        return {};
677
    return Playlist::search( this, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
678
679
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
680
681
std::vector<AlbumPtr> MediaLibrary::searchAlbums( const std::string& pattern ) const
{
682
683
    if ( validateSearchPattern( pattern ) == false )
        return {};
684
    return Album::search( this, pattern );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
685
686
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
687
688
std::vector<GenrePtr> MediaLibrary::searchGenre( const std::string& genre ) const
{
689
690
    if ( validateSearchPattern( genre ) == false )
        return {};
691
    return Genre::search( this, genre );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
692
693
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
694
695
696
697
std::vector<ArtistPtr> MediaLibrary::searchArtists(const std::string& name ) const
{
    if ( validateSearchPattern( name ) == false )
        return {};
698
    return Artist::search( this, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
699
700
}

701
SearchAggregate MediaLibrary::search( const std::string& pattern ) const
702
{
703
    SearchAggregate res;
704
705
    res.albums = searchAlbums( pattern );
    res.artists = searchArtists( pattern );
706
    res.genres = searchGenre( pattern );
707
708
709
710
711
    res.media = searchMedia( pattern );
    res.playlists = searchPlaylists( pattern );
    return res;
}

712
713
void MediaLibrary::startParser()
{
714
    m_parser.reset( new Parser( this ) );
715

716
    auto vlcService = std::unique_ptr<VLCMetadataService>( new VLCMetadataService );
717
    auto metadataService = std::unique_ptr<MetadataParser>( new MetadataParser );
718
    auto thumbnailerService = std::unique_ptr<VLCThumbnailer>( new VLCThumbnailer );
719
    m_parser->addService( std::move( vlcService ) );
720
    m_parser->addService( std::move( metadataService ) );
721
    m_parser->addService( std::move( thumbnailerService ) );
722
723
724
    m_parser->start();
}

725
void MediaLibrary::startDiscoverer()
726
{
727
    m_discovererWorker.reset( new DiscovererWorker( this ) );
728
    for ( const auto& fsFactory : m_fsFactories )
729
730
731
732
733
    {
        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 ) ) ) );
    }
734
735
}

736
737
void MediaLibrary::startDeletionNotifier()
{
738
739
    m_modificationNotifier.reset( new ModificationNotifier( this ) );
    m_modificationNotifier->start();
740
741
}

742
743
744
745
746
void MediaLibrary::addLocalFsFactory()
{
    m_fsFactories.insert( begin( m_fsFactories ), std::make_shared<factory::FileSystemFactory>( m_deviceLister ) );
}

747
InitializeResult MediaLibrary::updateDatabaseModel( unsigned int previousVersion,
748
                                        const std::string& dbPath )
749
{
750
751
752
    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
753
    // In case of downgrade, just recreate the database
754
    for ( auto i = 0u; i < 3; ++i )
755
    {
756
757
758
759
760
        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
761
762
            // We might also have some special cases for failed upgrade (see
            // comments below for per-version details)
763
            if ( previousVersion < 3 ||
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
764
765
                 previousVersion > Settings::DbModelVersion ||
                 previousVersion == 4 )
766
            {
767
                if( recreateDatabase( dbPath ) == false )
768
                    throw std::runtime_error( "Failed to recreate the database" );
769
                return InitializeResult::DbReset;
770
            }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
771
772
773
774
775
776
777
778
            /**
             * 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.
             */
779
780
            if ( previousVersion == 3 )
            {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
781
782
783
                if ( migrateModel3to5() == false )
                    throw std::logic_error( "Failed to migrate from 3 to 5" );
                previousVersion = 5;
784
            }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
785
786
787
788
789
790
            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
791
792
            if ( previousVersion == 6 )
            {
793
794
795
                // 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
796
797
                previousVersion = 7;
            }
798
799
800
801
802
803
804
805
806
            /**
             * 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;
            }
807
808
809
810
811
812
813
814
815
816
817
818
            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;
            }
819
820
821
822
823
            // 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 );
824
            if ( m_settings.save() == false )
825
826
                return InitializeResult::Failed;
            return InitializeResult::Success;
827
828
829
830
831
832
833
834
835
836
837
        }
        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" );
838
    }
839
840
    LOG_ERROR( "Failed to upgrade database, recreating it" );
    for ( auto i = 0u; i < 3; ++i )
841
    {
842
843
        try
        {
844
            if( recreateDatabase( dbPath ) == true )
845
                return InitializeResult::DbReset;
846
847
848
849
850
851
852
853
854
855
        }
        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" );
856
    }
857
    return InitializeResult::Failed;
858
859
}

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

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
873
bool MediaLibrary::migrateModel3to5()
874
875
{
    /*
876
877
     * Disable Foreign Keys & recursive triggers to avoid cascading deletion
     * while remodeling the database into the transaction.
878
     */
879
    sqlite::Connection::WeakDbContext weakConnCtx{ getConn() };
880
    auto t = getConn()->newTransaction();
881
882
883
884
    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
885
#               include "database/migrations/migration3-5.sql"
886
887
888
    };

    for ( const auto& req : reqs )
889
        sqlite::Tools::executeRequest( getConn(), req );
890
    // Re-create triggers removed in the process
891
892
    Media::createTriggers( getConn() );
    Playlist::createTriggers( getConn() );
893
894
895
896
    t->commit();
    return true;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
897
898
bool MediaLibrary::migrateModel5to6()
{
899
900
901
    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
902
903
    sqlite::Connection::WeakDbContext weakConnCtx{ getConn() };
    using namespace policy;
904
    req = "UPDATE " + MediaTable::Name + " SET is_present = 1 WHERE is_present != 0";
905
    sqlite::Tools::executeRequest( getConn(), req );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
906

Alexandre Fernandez's avatar
Alexandre Fernandez committed
907
908
909
    return true;
}

910
911
912
913
914
915
916
917
918
919
920
921
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
922
    Artist::createTriggers( getConn(), 8u );
923
924
    Media::createTriggers( getConn() );
    File::createTriggers( getConn() );
925
926
927
    t->commit();
}

928
929
void MediaLibrary::reload()
{
930
931
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->reload();
932
933
}

934
935
void MediaLibrary::reload( const std::string& entryPoint )
{
936
937
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->reload( entryPoint );
938
939
}

940
bool MediaLibrary::forceParserRetry()
941
{
942
943
    try
    {
944
        parser::Task::resetRetryCount( this );