MediaLibrary.cpp 26.9 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 <sys/stat.h>
30

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

56
57
58
// Discoverers:
#include "discoverer/FsDiscoverer.h"

59
60
61
// Metadata services:
#include "metadata_services/vlc/VLCMetadataService.h"
#include "metadata_services/vlc/VLCThumbnailer.h"
62
#include "metadata_services/MetadataParser.h"
63

64
65
// FileSystem
#include "factory/DeviceListerFactory.h"
66
#include "factory/FileSystemFactory.h"
67
#include "factory/NetworkFileSystemFactory.h"
68
#include "filesystem/IDevice.h"
69

70
71
72
namespace medialibrary
{

73
74
75
76
77
78
79
80
81
82
const char* const MediaLibrary::supportedExtensions[] = {
    "3gp", "a52", "aac", "ac3", "aiff", "amr", "amv", "aob", "ape",
    "asf", "avi", "divx", "dts", "dv", "flac", "flv", "gxf", "iso",
    "it", "m1v", "m2t", "m2ts", "m2v", "m4a", "m4b", "m4p", "m4v",
    "mid", "mka", "mkv", "mlp", "mod", "mov", "mp1", "mp2", "mp2",
    "mp3", "mp4", "mpc", "mpeg", "mpeg1", "mpeg2", "mpeg4", "mpg",
    "mts", "mxf", "nsv", "nuv", "oga", "ogg", "ogm", "ogv", "ogx",
    "oma", "opus", "ps", "rec", "rm", "rmi", "rmvb", "s3m", "spx",
    "tod", "trp", "ts", "tta", "vob", "voc", "vqf", "vro", "w64",
    "wav", "webm", "wma", "wmv", "wv", "xa", "xm"
83
84
};

85
const uint32_t MediaLibrary::DbModelVersion = 2;
86

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

95
96
MediaLibrary::~MediaLibrary()
{
97
    // Explicitely stop the discoverer, to avoid it writting while tearing down.
98
99
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->stop();
100
101
    if ( m_parser != nullptr )
        m_parser->stop();
102
    Media::clear();
103
    Folder::clear();
104
105
106
107
108
    Label::clear();
    Album::clear();
    AlbumTrack::clear();
    Show::clear();
    ShowEpisode::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
109
    Movie::clear();
110
    VideoTrack::clear();
111
    AudioTrack::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
112
    Artist::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
113
    Device::clear();
114
    File::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
115
    Playlist::clear();
116
    History::clear();
117
    Genre::clear();
118
119
}

120
121
bool MediaLibrary::createAllTables()
{
122
123
124
125
126
    // 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.

127
    auto t = m_dbConnection->newTransaction();
128
    auto res = Device::createTable( m_dbConnection.get() ) &&
129
        Folder::createTable( m_dbConnection.get() ) &&
130
        Media::createTable( m_dbConnection.get() ) &&
131
        File::createTable( m_dbConnection.get() ) &&
132
        Label::createTable( m_dbConnection.get() ) &&
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
133
        Playlist::createTable( m_dbConnection.get() ) &&
134
        Genre::createTable( m_dbConnection.get() ) &&
135
136
        Album::createTable( m_dbConnection.get() ) &&
        AlbumTrack::createTable( m_dbConnection.get() ) &&
137
        Album::createTriggers( m_dbConnection.get() ) &&
138
139
140
141
142
        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() ) &&
143
        Artist::createTable( m_dbConnection.get() ) &&
144
        Artist::createDefaultArtists( m_dbConnection.get() ) &&
145
        Artist::createTriggers( m_dbConnection.get() ) &&
146
        Media::createTriggers( m_dbConnection.get() ) &&
147
        Genre::createTriggers( m_dbConnection.get() ) &&
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
148
        Playlist::createTriggers( m_dbConnection.get() ) &&
149
        History::createTable( m_dbConnection.get() ) &&
150
151
152
153
154
155
156
        Settings::createTable( m_dbConnection.get() );
    if ( res == false )
        return false;
    t->commit();
    return true;
}

157
158
159
160
161
162
163
164
template <typename T>
static void propagateDeletionToCache( SqliteConnection::HookReason reason, int64_t rowId )
{
    if ( reason != SqliteConnection::HookReason::Delete )
        return;
    T::removeFromCache( rowId );
}

165
166
void MediaLibrary::registerEntityHooks()
{
167
    if ( m_modificationNotifier == nullptr )
168
169
        return;

170
171
172
173
174
    m_dbConnection->registerUpdateHook( policy::MediaTable::Name,
                                        [this]( SqliteConnection::HookReason reason, int64_t rowId ) {
        if ( reason != SqliteConnection::HookReason::Delete )
            return;
        Media::removeFromCache( rowId );
175
        m_modificationNotifier->notifyMediaRemoval( rowId );
176
    });
177
178
179
180
181
182
183
    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 );
    });
184
185
186
187
188
189
190
    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 );
    });
191
192
193
194
195
196
197
    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 );
    });
198
199
200
201
202
203
204
    m_dbConnection->registerUpdateHook( policy::PlaylistTable::Name,
                                        [this]( SqliteConnection::HookReason reason, int64_t rowId ) {
        if ( reason != SqliteConnection::HookReason::Delete )
            return;
        Playlist::removeFromCache( rowId );
        m_modificationNotifier->notifyPlaylistRemoval( rowId );
    });
205
206
207
208
209
210
211
212
213
214
    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> );
215
216
}

217
218
219
220
221
bool MediaLibrary::validateSearchPattern( const std::string& pattern )
{
    return pattern.size() >= 3;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
222
bool MediaLibrary::initialize( const std::string& dbPath, const std::string& thumbnailPath, IMediaLibraryCb* mlCallback )
223
{
224
    LOG_INFO( "Initializing medialibrary..." );
225
    if ( m_initialized == true )
226
227
    {
        LOG_INFO( "...Already initialized" );
228
        return true;
229
    }
230
231
232
233
    if ( m_deviceLister == nullptr )
    {
        m_deviceLister = factory::createDeviceLister();
        if ( m_deviceLister == nullptr )
234
235
        {
            LOG_ERROR( "No available IDeviceLister was found." );
236
            return false;
237
        }
238
    }
239
    addLocalFsFactory();
240
241
242
#ifdef _WIN32
    if ( mkdir( thumbnailPath.c_str() ) != 0 )
#else
243
    if ( mkdir( thumbnailPath.c_str(), S_IRWXU ) != 0 )
244
#endif
245
246
247
248
249
    {
        if ( errno != EEXIST )
            throw std::runtime_error( std::string( "Failed to create thumbnail directory: " ) +
                                      strerror( errno ) );
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
250
    m_thumbnailPath = thumbnailPath;
251
252
253
    m_callback = mlCallback;
    m_dbConnection.reset( new SqliteConnection( dbPath ) );

254
255
256
    // 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
257
258
    registerEntityHooks();

259
    if ( createAllTables() == false )
260
    {
261
        LOG_ERROR( "Failed to create database structure" );
262
263
        return false;
    }
264
    if ( m_settings.load( m_dbConnection.get() ) == false )
265
266
    {
        LOG_ERROR( "Failed to load settings" );
267
        return false;
268
    }
269
    if ( m_settings.dbModelVersion() != DbModelVersion )
270
271
    {
        if ( updateDatabaseModel( m_settings.dbModelVersion() ) == false )
272
273
        {
            LOG_ERROR( "Failed to update database model" );
274
            return false;
275
        }
276
    }
277
278
    for ( auto& fsFactory : m_fsFactories )
        refreshDevices( *fsFactory );
279
    startDiscoverer();
280
    startParser();
281
    m_initialized = true;
282
    LOG_INFO( "Successfuly initialized" );
283
    return true;
284
285
}

286
void MediaLibrary::setVerbosity( LogLevel v )
287
288
289
290
291
{
    m_verbosity = v;
    Log::setLogLevel( v );
}

292
293
294
295
296
MediaPtr MediaLibrary::media( int64_t mediaId ) const
{
    return Media::fetch( this, mediaId );
}

297
298
MediaPtr MediaLibrary::media( const std::string& mrl ) const
{
299
    LOG_INFO( "Fetching media from mrl: ", mrl );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
300
    auto file = File::fromExternalMrl( this, mrl );
301
302
303
304
305
    if ( file != nullptr )
    {
        LOG_INFO( "Found external media: ", mrl );
        return file->media();
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
306
    auto fsFactory = fsFactoryForMrl( mrl );
307
    if ( fsFactory == nullptr )
308
    {
309
310
        LOG_WARN( "Failed to create FS factory for path ", mrl );
        return nullptr;
311
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
312
    auto device = fsFactory->createDeviceFromMrl( mrl );
313
314
315
316
317
318
    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
319
        file = File::fromMrl( this, mrl );
320
321
    else
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
322
        auto folder = Folder::fromMrl( this, utils::file::directory( mrl ) );
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
        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 )
    {
        LOG_WARN( "Failed to fetch file for ", mrl, "(device ", device->uuid(), " was ",
                  device->isRemovable() ? "NOT" : "", "removable)");
        return nullptr;
    }
    return file->media();
}

344
345
MediaPtr MediaLibrary::addMedia( const std::string& mrl )
{
346
    auto t = m_dbConnection->newTransaction();
347
348
349
    auto media = Media::create( this, IMedia::Type::Unknown, utils::file::fileName( mrl ) );
    if ( media == nullptr )
        return nullptr;
350
351
352
    if ( media->addExternalMrl( mrl, IFile::Type::Main ) == nullptr )
        return nullptr;
    t->commit();
353
354
355
    return media;
}

356
std::vector<MediaPtr> MediaLibrary::audioFiles( SortingCriteria sort, bool desc ) const
357
{
358
    return Media::listAll( this, IMedia::Type::Audio, sort, desc );
359
360
}

361
std::vector<MediaPtr> MediaLibrary::videoFiles( SortingCriteria sort, bool desc ) const
362
{
363
    return Media::listAll( this, IMedia::Type::Video, sort, desc );
364
365
}

366
std::shared_ptr<Media> MediaLibrary::addFile( const fs::IFile& fileFs, Folder& parentFolder, fs::IDirectory& parentFolderFs )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
367
{
368
    auto type = IMedia::Type::Unknown;
369

370
371
372
373
    if ( std::binary_search( std::begin( supportedExtensions ), std::end( supportedExtensions ),
                             fileFs.extension().c_str(),
                             [](const char* l, const char* r) { return strcasecmp( l, r ) < 0; }
                            ) == false )
374
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
375
        LOG_INFO( "Rejecting file ", fileFs.mrl(), " due to its extension" );
376
        return nullptr;
377
    }
378

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
379
    LOG_INFO( "Adding ", fileFs.mrl() );
380
    auto mptr = Media::create( this, type, fileFs.name() );
381
382
    if ( mptr == nullptr )
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
383
        LOG_ERROR( "Failed to add media ", fileFs.mrl(), " to the media library" );
384
385
        return nullptr;
    }
386
    // For now, assume all media are made of a single file
387
    auto file = mptr->addFile( fileFs, parentFolder, parentFolderFs, File::Type::Main );
388
    if ( file == nullptr )
389
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
390
        LOG_ERROR( "Failed to add file ", fileFs.mrl(), " to media #", mptr->id() );
391
        Media::destroy( this, mptr->id() );
392
393
        return nullptr;
    }
394
    if ( m_parser != nullptr )
395
396
        m_parser->parse( mptr, file );
    return mptr;
397
398
}

399
bool MediaLibrary::deleteFolder( const Folder& folder )
400
{
401
    if ( Folder::destroy( this, folder.id() ) == false )
402
        return false;
403
    Media::clear();
404
    return true;
405
406
}

407
408
LabelPtr MediaLibrary::createLabel( const std::string& label )
{
409
    return Label::create( this, label );
410
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
411
412
413

bool MediaLibrary::deleteLabel( LabelPtr label )
{
414
    return Label::destroy( this, label->id() );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
415
}
416

417
AlbumPtr MediaLibrary::album( int64_t id ) const
418
{
419
    return Album::fetch( this, id );
420
421
}

422
std::shared_ptr<Album> MediaLibrary::createAlbum( const std::string& title, const std::string& artworkMrl )
423
{
424
    return Album::create( this, title, artworkMrl );
425
426
}

427
std::vector<AlbumPtr> MediaLibrary::albums( SortingCriteria sort, bool desc ) const
428
{
429
    return Album::listAll( this, sort, desc );
430
431
}

432
std::vector<GenrePtr> MediaLibrary::genres( SortingCriteria sort, bool desc ) const
433
{
434
    return Genre::listAll( this, sort, desc );
435
436
}

437
GenrePtr MediaLibrary::genre( int64_t id ) const
438
439
440
441
{
    return Genre::fetch( this, id );
}

442
ShowPtr MediaLibrary::show( const std::string& name ) const
443
444
445
{
    static const std::string req = "SELECT * FROM " + policy::ShowTable::Name
            + " WHERE name = ?";
446
    return Show::fetch( this, req, name );
447
448
}

449
std::shared_ptr<Show> MediaLibrary::createShow( const std::string& name )
450
{
451
    return Show::create( this, name );
452
453
}

454
MoviePtr MediaLibrary::movie( const std::string& title ) const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
455
456
457
{
    static const std::string req = "SELECT * FROM " + policy::MovieTable::Name
            + " WHERE title = ?";
458
    return Movie::fetch( this, req, title );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
459
460
}

461
std::shared_ptr<Movie> MediaLibrary::createMovie( Media& media, const std::string& title )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
462
{
463
    auto movie = Movie::create( this, media.id(), title );
464
    media.setMovie( movie );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
465
    media.save();
466
    return movie;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
467
468
}

469
ArtistPtr MediaLibrary::artist( int64_t id ) const
470
{
471
    return Artist::fetch( this, id );
472
473
474
}

ArtistPtr MediaLibrary::artist( const std::string& name )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
475
476
{
    static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name
477
            + " WHERE name = ? AND is_present = 1";
478
    return Artist::fetch( this, req, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
479
480
}

481
std::shared_ptr<Artist> MediaLibrary::createArtist( const std::string& name )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
482
{
483
484
    try
    {
485
        return Artist::create( this, name );
486
487
488
489
490
491
    }
    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
492
493
}

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

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
499
500
PlaylistPtr MediaLibrary::createPlaylist( const std::string& name )
{
501
    return Playlist::create( this, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
502
503
}

504
std::vector<PlaylistPtr> MediaLibrary::playlists( SortingCriteria sort, bool desc )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
505
{
506
    return Playlist::listAll( this, sort, desc );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
507
508
}

509
510
511
512
513
PlaylistPtr MediaLibrary::playlist( int64_t id ) const
{
    return Playlist::fetch( this, id );
}

514
bool MediaLibrary::deletePlaylist( int64_t playlistId )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
515
{
516
    return Playlist::destroy( this, playlistId );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
517
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
518

519
bool MediaLibrary::addToStreamHistory( MediaPtr media )
520
{
521
    return History::insert( getConn(), media->id() );
522
523
524
525
526
527
528
}

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

529
530
std::vector<MediaPtr> MediaLibrary::lastMediaPlayed() const
{
531
    return Media::fetchHistory( this );
532
533
}

534
535
bool MediaLibrary::clearHistory()
{
536
    auto t = getConn()->newTransaction();
537
    Media::clearHistory( this );
538
539
540
    if ( History::clearStreams( this ) == false )
        return false;
    t->commit();
541
542
543
    return true;
}

544
MediaSearchAggregate MediaLibrary::searchMedia( const std::string& title ) const
545
{
546
547
    if ( validateSearchPattern( title ) == false )
        return {};
548
    auto tmp = Media::search( this, title );
549
    MediaSearchAggregate res;
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
    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;
569
570
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
571
572
std::vector<PlaylistPtr> MediaLibrary::searchPlaylists( const std::string& name ) const
{
573
574
    if ( validateSearchPattern( name ) == false )
        return {};
575
    return Playlist::search( this, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
576
577
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
578
579
std::vector<AlbumPtr> MediaLibrary::searchAlbums( const std::string& pattern ) const
{
580
581
    if ( validateSearchPattern( pattern ) == false )
        return {};
582
    return Album::search( this, pattern );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
583
584
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
585
586
std::vector<GenrePtr> MediaLibrary::searchGenre( const std::string& genre ) const
{
587
588
    if ( validateSearchPattern( genre ) == false )
        return {};
589
    return Genre::search( this, genre );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
590
591
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
592
593
594
595
std::vector<ArtistPtr> MediaLibrary::searchArtists(const std::string& name ) const
{
    if ( validateSearchPattern( name ) == false )
        return {};
596
    return Artist::search( this, name );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
597
598
}

599
SearchAggregate MediaLibrary::search( const std::string& pattern ) const
600
{
601
    SearchAggregate res;
602
603
    res.albums = searchAlbums( pattern );
    res.artists = searchArtists( pattern );
604
    res.genres = searchGenre( pattern );
605
606
607
608
609
    res.media = searchMedia( pattern );
    res.playlists = searchPlaylists( pattern );
    return res;
}

610
611
void MediaLibrary::startParser()
{
612
    m_parser.reset( new Parser( this ) );
613

614
    auto vlcService = std::unique_ptr<VLCMetadataService>( new VLCMetadataService );
615
    auto metadataService = std::unique_ptr<MetadataParser>( new MetadataParser );
616
    auto thumbnailerService = std::unique_ptr<VLCThumbnailer>( new VLCThumbnailer );
617
    m_parser->addService( std::move( vlcService ) );
618
    m_parser->addService( std::move( metadataService ) );
619
    m_parser->addService( std::move( thumbnailerService ) );
620
621
622
    m_parser->start();
}

623
void MediaLibrary::startDiscoverer()
624
{
625
    m_discovererWorker.reset( new DiscovererWorker( this ) );
626
    for ( const auto& fsFactory : m_fsFactories )
627
        m_discovererWorker->addDiscoverer( std::unique_ptr<IDiscoverer>( new FsDiscoverer( fsFactory, this, m_callback ) ) );
628
    m_discovererWorker->reload();
629
630
}

631
632
void MediaLibrary::startDeletionNotifier()
{
633
634
    m_modificationNotifier.reset( new ModificationNotifier( this ) );
    m_modificationNotifier->start();
635
636
}

637
638
639
640
641
void MediaLibrary::addLocalFsFactory()
{
    m_fsFactories.insert( begin( m_fsFactories ), std::make_shared<factory::FileSystemFactory>( m_deviceLister ) );
}

642
643
644
645
646
647
648
649
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;";
650
        if ( sqlite::Tools::executeRequest( getConn(), req ) == false )
651
652
653
654
655
656
657
658
659
660
661
662
663
664
            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;
}

665
666
void MediaLibrary::reload()
{
667
668
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->reload();
669
670
}

671
672
void MediaLibrary::reload( const std::string& entryPoint )
{
673
674
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->reload( entryPoint );
675
676
}

677
678
679
680
681
void MediaLibrary::forceParserRetry()
{
    File::resetRetryCount( this );
}

682
683
void MediaLibrary::pauseBackgroundOperations()
{
684
685
    if ( m_parser != nullptr )
        m_parser->pause();
686
687
688
689
}

void MediaLibrary::resumeBackgroundOperations()
{
690
691
    if ( m_parser != nullptr )
        m_parser->resume();
692
693
}

694
695
696
697
698
699
700
701
702
703
DBConnection MediaLibrary::getConn() const
{
    return m_dbConnection.get();
}

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

704
705
706
707
708
std::shared_ptr<ModificationNotifier> MediaLibrary::getNotifier() const
{
    return m_modificationNotifier;
}

709
710
711
712
713
714
IDeviceListerCb* MediaLibrary::setDeviceLister( DeviceListerPtr lister )
{
    m_deviceLister = lister;
    return static_cast<IDeviceListerCb*>( this );
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
715
std::shared_ptr<factory::IFileSystem> MediaLibrary::fsFactoryForMrl( const std::string& mrl ) const
716
{
717
718
    for ( const auto& f : m_fsFactories )
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
719
        if ( f->isMrlSupported( mrl ) )
720
721
722
            return f;
    }
    return nullptr;
723
724
}

725
726
void MediaLibrary::discover( const std::string &entryPoint )
{
727
728
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->discover( entryPoint );
729
730
}

731
732
733
734
735
736
737
738
739
740
741
742
void MediaLibrary::setDiscoverNetworkEnabled( bool enabled )
{
    if ( enabled )
    {
        auto it = std::find_if( begin( m_fsFactories ), end( m_fsFactories ), []( const std::shared_ptr<factory::IFileSystem> fs ) {
            return fs->isNetworkFileSystem();
        });
        if ( it == end( m_fsFactories ) )
            m_fsFactories.push_back( std::make_shared<factory::NetworkFileSystemFactory>( "smb", "dsm-sd" ) );
    }
    else
    {
743
        m_fsFactories.erase( std::remove_if( begin( m_fsFactories ), end( m_fsFactories ), []( const std::shared_ptr<factory::IFileSystem> fs ) {
744
            return fs->isNetworkFileSystem();
745
        }), end( m_fsFactories ) );
746
747
748
    }
}

749
750
751
752
753
754
755
std::vector<FolderPtr> MediaLibrary::entryPoints() const
{
    static const std::string req = "SELECT * FROM " + policy::FolderTable::Name + " WHERE parent_id IS NULL"
            " AND is_blacklisted = 0";
    return Folder::fetchAll<IFolder>( this, req );
}

756
void MediaLibrary::removeEntryPoint( const std::string& entryPoint )
757
{
758
759
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->remove( entryPoint );
760
761
}

762
void MediaLibrary::banFolder( const std::string& entryPoint )
763
{
764
765
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->ban( entryPoint );
766
767
}

768
void MediaLibrary::unbanFolder( const std::string& entryPoint )
769
{
770
771
    if ( m_discovererWorker != nullptr )
        m_discovererWorker->unban( entryPoint );
772
773
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
774
const std::string& MediaLibrary::thumbnailPath() const
775
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
776
    return m_thumbnailPath;
777
778
}

779
780
781
782
783
void MediaLibrary::setLogger( ILogger* logger )
{
    Log::SetLogger( logger );
}

784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
bool MediaLibrary::onDeviceChanged( factory::IFileSystem& fsFactory, Device& device )
{
    auto deviceFs = fsFactory.createDevice( device.uuid() );
    auto fsDevicePresent = deviceFs != nullptr && deviceFs->isPresent();
    if ( device.isPresent() != fsDevicePresent )
    {
        LOG_INFO( "Device ", device.uuid(), " changed presence state: ",
                  device.isPresent(), " -> ", fsDevicePresent );
        device.setPresent( fsDevicePresent );
        return true;
    }
    LOG_INFO( "Device ", device.uuid(), " unchanged" );
    return false;
}

799
800
801
802
803
void MediaLibrary::refreshDevices( factory::IFileSystem& fsFactory )
{
    // Don't refuse to process devices when none seem to be present, it might be a valid case
    // if the user only discovered removable storages, and we would still need to mark those
    // as "not present"
804
    fsFactory.refreshDevices();
805
806
    auto devices = Device::fetchAll( this );
    for ( auto& d : devices )
807
        onDeviceChanged( fsFactory, *d );
808
809
}

810
bool MediaLibrary::onDevicePlugged( const std::string& uuid, const std::string& mountpoint )
811
{
812
    auto currentDevice = Device::fromUuid( this, uuid );
813
    LOG_INFO( "Device ", uuid, " was plugged and mounted on ", mountpoint );
814
    assert( currentDevice == nullptr || currentDevice->isPresent() == false );
815
816
817
818
    // If we don't know the device yet, simply postpone the device creation to the first
    // discovery/reload, and inform the caller that we didn't know this device yet.
    if ( currentDevice == nullptr )
        return true;
819
820
    for ( const auto& fsFactory : m_fsFactories )
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
821
        if ( fsFactory->isMrlSupported( "file://" ) )
822
        {
823
824
825
            auto res = onDeviceChanged( *fsFactory, *currentDevice );
            // Ensure the device actually changed.
            assert( res == true );
826
827
828
#ifdef NDEBUG
            (void)res; // Silence unused variable warning
#endif
829
830
831
            break;
        }
    }
832
    return false;
833
834
}

835
void MediaLibrary::onDeviceUnplugged( const std::string& uuid )
836
{
837
838
839
840
841
842
    auto device = Device::fromUuid( this, uuid );
    if ( device == nullptr )
    {
        LOG_WARN( "Unknown device ", uuid, " was unplugged. Ignoring." );
        return;
    }
843
    LOG_INFO( "Device ", uuid, " was unplugged" );
844
845
    for ( const auto& fsFactory : m_fsFactories )
    {
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
846
        if ( fsFactory->isMrlSupported( "file://" ) )
847
        {
848
849
            auto res = onDeviceChanged( *fsFactory, *device );
            assert( res == true );
850
851
852
#ifdef NDEBUG
            (void)res; // Silence unused variable warning
#endif
853
854
855
            break;
        }
    }
856
857
}

858
}