MediaLibrary.cpp 14.1 KB
Newer Older
1
2
#include <algorithm>
#include <functional>
3
4
5
6
#include "Album.h"
#include "AlbumTrack.h"
#include "AudioTrack.h"
#include "File.h"
7
#include "Folder.h"
8
#include "MediaLibrary.h"
9
#include "IMetadataService.h"
10
#include "Label.h"
11
#include "logging/Logger.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
12
#include "Movie.h"
13
#include "Parser.h"
14
15
#include "Show.h"
#include "ShowEpisode.h"
16
#include "database/SqliteTools.h"
17
#include "Utils.h"
18
#include "VideoTrack.h"
19

20
21
22
// Discoverers:
#include "discoverer/FsDiscoverer.h"

23
24
25
26
// Metadata services:
#include "metadata_services/vlc/VLCMetadataService.h"
#include "metadata_services/vlc/VLCThumbnailer.h"

27
28
#include "filesystem/IDirectory.h"
#include "filesystem/IFile.h"
29
30
#include "factory/FileSystem.h"

31
const std::vector<std::string> MediaLibrary::supportedVideoExtensions {
32
33
34
35
36
    // Videos
    "avi", "3gp", "amv", "asf", "divx", "dv", "flv", "gxf",
    "iso", "m1v", "m2v", "m2t", "m2ts", "m4v", "mkv", "mov",
    "mp2", "mp4", "mpeg", "mpeg1", "mpeg2", "mpeg4", "mpg",
    "mts", "mxf", "nsv", "nuv", "ogg", "ogm", "ogv", "ogx", "ps",
37
38
39
40
    "rec", "rm", "rmvb", "tod", "ts", "vob", "vro", "webm", "wmv"
};

const std::vector<std::string> MediaLibrary::supportedAudioExtensions {
41
42
43
44
45
46
47
48
    // Audio
    "a52", "aac", "ac3", "aiff", "amr", "aob", "ape",
    "dts", "flac", "it", "m4a", "m4p", "mid", "mka", "mlp",
    "mod", "mp1", "mp2", "mp3", "mpc", "oga", "ogg", "oma",
    "rmi", "s3m", "spx", "tta", "voc", "vqf", "w64", "wav",
    "wma", "wv", "xa", "xm"
};

49
MediaLibrary::MediaLibrary()
50
    : m_parser( new Parser )
51
52
53
{
}

54
55
MediaLibrary::~MediaLibrary()
{
56
57
58
59
60
61
    // The log callback isn't shared by all VLC::Instance's, yet since
    // they all share a single libvlc_instance_t, any VLC::Instance still alive
    // with a log callback set will try to invoke it.
    // We manually call logUnset to ensure that the callback that is about to be deleted will
    // not be called anymore.
    m_vlcInstance.logUnset();
62
    File::clear();
63
    Folder::clear();
64
65
66
67
68
    Label::clear();
    Album::clear();
    AlbumTrack::clear();
    Show::clear();
    ShowEpisode::clear();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
69
    Movie::clear();
70
    VideoTrack::clear();
71
    AudioTrack::clear();
72
73
}

74
void MediaLibrary::setFsFactory(std::shared_ptr<factory::IFileSystem> fsFactory)
75
{
76
77
78
    m_fsFactory = fsFactory;
}

79
bool MediaLibrary::initialize( const std::string& dbPath, const std::string& snapshotPath, IMediaLibraryCb* mlCallback )
80
81
{
    if ( m_fsFactory == nullptr )
82
        m_fsFactory.reset( new factory::FileSystemDefaultFactory );
83
    m_snapshotPath = snapshotPath;
84
    m_callback = mlCallback;
85

86
    if ( mlCallback != nullptr )
87
88
89
90
    {
        const char* args[] = {
            "-vv",
        };
91
92
        m_vlcInstance = VLC::Instance( sizeof(args) / sizeof(args[0]), args );
        m_vlcInstance.logSet([](int lvl, const libvlc_log_t*, std::string msg) {
93
94
95
96
97
98
99
100
            if ( lvl == LIBVLC_ERROR )
                Log::Error( msg );
            else if ( lvl == LIBVLC_WARNING )
                Log::Warning( msg );
            else
                Log::Info( msg );
        });

101
102
        auto vlcService = std::unique_ptr<VLCMetadataService>( new VLCMetadataService( m_vlcInstance ) );
        auto thumbnailerService = std::unique_ptr<VLCThumbnailer>( new VLCThumbnailer( m_vlcInstance ) );
103
104
105
106
        addMetadataService( std::move( vlcService ) );
        addMetadataService( std::move( thumbnailerService ) );
    }

107
108
    m_discoverers.emplace_back( new FsDiscoverer( m_fsFactory, this ) );

109
110
    sqlite3* dbConnection;
    int res = sqlite3_open( dbPath.c_str(), &dbConnection );
111
112
    if ( res != SQLITE_OK )
        return false;
113
    m_dbConnection.reset( dbConnection, &sqlite3_close );
114
    if ( sqlite::Tools::executeRequest( DBConnection(m_dbConnection), "PRAGMA foreign_keys = ON" ) == false )
115
    {
116
        LOG_ERROR( "Failed to enable foreign keys" );
117
        return false;
118
    }
119
    if ( ! ( File::createTable( m_dbConnection ) &&
120
        Folder::createTable( m_dbConnection ) &&
121
122
123
124
        Label::createTable( m_dbConnection ) &&
        Album::createTable( m_dbConnection ) &&
        AlbumTrack::createTable( m_dbConnection ) &&
        Show::createTable( m_dbConnection ) &&
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
125
        ShowEpisode::createTable( m_dbConnection ) &&
126
        Movie::createTable( m_dbConnection ) &&
127
        VideoTrack::createTable( m_dbConnection ) &&
128
129
        AudioTrack::createTable( m_dbConnection ) ) )
    {
130
        LOG_ERROR( "Failed to create database structure" );
131
132
133
        return false;
    }
    return loadFolders();
134
135
}

136
std::vector<FilePtr> MediaLibrary::files()
137
{
138
    return File::fetchAll( m_dbConnection );
139
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
140

141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
std::vector<FilePtr> MediaLibrary::audioFiles()
{
    static const std::string req = "SELECT * FROM " + policy::FileTable::Name + " WHERE type = ?";
    //FIXME: Replace this with template magic in sqlite's traits
    using type_t = std::underlying_type<IFile::Type>::type;
    return sqlite::Tools::fetchAll<File, IFile>( m_dbConnection, req, static_cast<type_t>( IFile::Type::AudioType ) );
}

std::vector<FilePtr> MediaLibrary::videoFiles()
{
    static const std::string req = "SELECT * FROM " + policy::FileTable::Name + " WHERE type = ?";
    using type_t = std::underlying_type<IFile::Type>::type;
    return sqlite::Tools::fetchAll<File, IFile>( m_dbConnection, req, static_cast<type_t>( IFile::Type::VideoType ) );
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
156
FilePtr MediaLibrary::file( const std::string& path )
157
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
158
    return File::fetch( m_dbConnection, path );
159
160
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
161
FilePtr MediaLibrary::addFile( const std::string& path )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
162
{
163
    auto fsFile = m_fsFactory->createFile( path );
164
    return addFile( fsFile.get(), 0 );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
165
}
166

167
168
169
170
171
FolderPtr MediaLibrary::folder( const std::string& path )
{
    return Folder::fetch( m_dbConnection, path );
}

172
173
174
175
176
177
178
179
180
181
bool MediaLibrary::deleteFile( const std::string& mrl )
{
    return File::destroy( m_dbConnection, mrl );
}

bool MediaLibrary::deleteFile( FilePtr file )
{
    return File::destroy( m_dbConnection, std::static_pointer_cast<File>( file ) );
}

182
183
bool MediaLibrary::deleteFolder( FolderPtr folder )
{
184
185
186
187
    if ( Folder::destroy( m_dbConnection, std::static_pointer_cast<Folder>( folder ) ) == false )
        return false;
    File::clear();
    return true;
188
189
}

190
191
LabelPtr MediaLibrary::createLabel( const std::string& label )
{
192
    return Label::create( m_dbConnection, label );
193
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
194
195
196

bool MediaLibrary::deleteLabel( const std::string& text )
{
197
    return Label::destroy( m_dbConnection, text );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
198
199
200
201
202
203
}

bool MediaLibrary::deleteLabel( LabelPtr label )
{
    return Label::destroy( m_dbConnection, std::static_pointer_cast<Label>( label ) );
}
204

205
AlbumPtr MediaLibrary::album(const std::string& title )
206
207
208
{
    // We can't use Cache helper, since albums are cached by primary keys
    static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
209
            " WHERE title = ?";
210
    return sqlite::Tools::fetchOne<Album>( DBConnection( m_dbConnection ), req, title );
211
212
}

213
AlbumPtr MediaLibrary::createAlbum(const std::string& title )
214
{
215
    return Album::create( m_dbConnection, title );
216
217
}

218
219
220
221
222
std::vector<AlbumPtr> MediaLibrary::albums()
{
    return Album::fetchAll( m_dbConnection );
}

223
224
225
226
ShowPtr MediaLibrary::show(const std::string& name)
{
    static const std::string req = "SELECT * FROM " + policy::ShowTable::Name
            + " WHERE name = ?";
227
    return sqlite::Tools::fetchOne<Show>( m_dbConnection, req, name );
228
229
230
231
232
233
234
}

ShowPtr MediaLibrary::createShow(const std::string& name)
{
    return Show::create( m_dbConnection, name );
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
235
236
237
238
MoviePtr MediaLibrary::movie( const std::string& title )
{
    static const std::string req = "SELECT * FROM " + policy::MovieTable::Name
            + " WHERE title = ?";
239
    return sqlite::Tools::fetchOne<Movie>( m_dbConnection, req, title );
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
240
241
242
243
244
245
246
}

MoviePtr MediaLibrary::createMovie( const std::string& title )
{
    return Movie::create( m_dbConnection, title );
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
247
void MediaLibrary::addMetadataService(std::unique_ptr<IMetadataService> service)
248
{
249
250
251
252
253
    if ( service->initialize( m_parser.get(), this ) == false )
    {
        std::cout << "Failed to initialize service" << std::endl;
        return;
    }
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
254
    m_parser->addService( std::move( service ) );
255
}
256

257
258
void MediaLibrary::discover( const std::string &entryPoint )
{
259
260
261
    std::thread t([this, entryPoint] {
        //FIXME: This will crash if the media library gets deleted while we
        //are discovering.
262
263
264
        if ( m_callback != nullptr )
            m_callback->onDiscoveryStarted( entryPoint );

265
266
        for ( auto& d : m_discoverers )
            d->discover( entryPoint );
267
268
269
270

        if ( m_callback != nullptr )
            m_callback->onDiscoveryCompleted( entryPoint );
    });
271
    t.detach();
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
}

FolderPtr MediaLibrary::onNewFolder( const fs::IDirectory* directory, FolderPtr parent )
{
    //FIXME: Since we insert files/folders with a UNIQUE constraint, maybe we should
    //just let sqlite try to insert, throw an exception in case the contraint gets violated
    //catch it and return nullptr from here.
    //We previously were fetching the folder manually here, but that triggers an eroneous entry
    //in the cache. This might also be something to fix...
    return Folder::create( m_dbConnection, directory,
                        parent == nullptr ? 0 : parent->id() );
}

FilePtr MediaLibrary::onNewFile( const fs::IFile *file, FolderPtr parent )
{
    //FIXME: Same uniqueness comment as onNewFolder above.
    return addFile( file, parent == nullptr ? 0 : parent->id() );
}

291
292
293
294
295
const std::string& MediaLibrary::snapshotPath() const
{
    return m_snapshotPath;
}

296
297
298
299
300
void MediaLibrary::setLogger( ILogger* logger )
{
    Log::SetLogger( logger );
}

301
302
bool MediaLibrary::loadFolders()
{
303
    //FIXME: This should probably be in a sql transaction
304
    //FIXME: This shouldn't be done for "removable"/network files
305
306
307
308
309
310
311
312
313
    static const std::string req = "SELECT * FROM " + policy::FolderTable::Name
            + " WHERE id_parent IS NULL";
    auto rootFolders = sqlite::Tools::fetchAll<Folder, IFolder>( m_dbConnection, req );
    for ( const auto f : rootFolders )
    {
        auto folder = m_fsFactory->createDirectory( f->path() );
        if ( folder->lastModificationDate() == f->lastModificationDate() )
            continue;
        checkSubfolders( folder.get(), f->id() );
314
        f->setLastModificationDate( folder->lastModificationDate() );
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
    }
    return true;
}

bool MediaLibrary::checkSubfolders( fs::IDirectory* folder, unsigned int parentId )
{
    // From here we can have:
    // - New subfolder(s)
    // - Deleted subfolder(s)
    // - New file(s)
    // - Deleted file(s)
    // - Changed file(s)
    // ... in this folder, or in all the sub folders.

    // Load the folders we already know of:
    static const std::string req = "SELECT * FROM " + policy::FolderTable::Name
            + " WHERE id_parent = ?";
    auto subFoldersInDB = sqlite::Tools::fetchAll<Folder, IFolder>( m_dbConnection, req, parentId );
    for ( const auto& subFolderPath : folder->dirs() )
    {
        auto it = std::find_if( begin( subFoldersInDB ), end( subFoldersInDB ), [subFolderPath](const std::shared_ptr<IFolder>& f) {
            return f->path() == subFolderPath;
        });
        // We don't know this folder, it's a new one
        if ( it == end( subFoldersInDB ) )
        {
341
342
343
            //FIXME: In order to add the new folder, we need to use the same discoverer.
            // This probably means we need to store which discoverer was used to add which file
            // and store discoverers as a map instead of a vector
344
345
346
347
348
349
350
351
352
353
354
355
356
            continue;
        }
        auto subFolder = m_fsFactory->createDirectory( subFolderPath );
        if ( subFolder->lastModificationDate() == (*it)->lastModificationDate() )
        {
            // Remove all folders that still exist in FS. That way, the list of folders that
            // will still be in subFoldersInDB when we're done is the list of folders that have
            // been deleted from the FS
            subFoldersInDB.erase( it );
            continue;
        }
        // This folder was modified, let's recurse
        checkSubfolders( subFolder.get(), (*it)->id() );
357
        checkFiles( subFolder.get(), (*it)->id() );
358
        (*it)->setLastModificationDate( subFolder->lastModificationDate() );
359
360
361
362
363
        subFoldersInDB.erase( it );
    }
    // Now all folders we had in DB but haven't seen from the FS must have been deleted.
    for ( auto f : subFoldersInDB )
    {
364
        std::cout << "Folder " << f->path() << " not found in FS, deleting it" << std::endl;
365
366
367
368
369
        deleteFolder( f );
    }
    return true;
}

370
371
372
373
374
375
376
void MediaLibrary::checkFiles( fs::IDirectory* folder, unsigned int parentId )
{
    static const std::string req = "SELECT * FROM " + policy::FileTable::Name
            + " WHERE folder_id = ?";
    auto files = sqlite::Tools::fetchAll<File, IFile>( m_dbConnection, req, parentId );
    for ( const auto& filePath : folder->files() )
    {
377
378
379
        auto file = m_fsFactory->createFile( filePath );
        auto it = std::find_if( begin( files ), end( files ), [filePath](const std::shared_ptr<IFile>& f) {
            return f->mrl() == filePath;
380
381
382
        });
        if ( it == end( files ) )
        {
383
            addFile( file.get(), parentId );
384
385
386
387
388
389
390
391
            continue;
        }
        if ( file->lastModificationDate() == (*it)->lastModificationDate() )
        {
            // Unchanged file
            files.erase( it );
            continue;
        }
392
393
        deleteFile( filePath );
        addFile( file.get(), parentId );
394
395
396
397
398
399
400
401
        files.erase( it );
    }
    for ( auto file : files )
    {
        deleteFile( file );
    }
}

402
FilePtr MediaLibrary::addFile( const fs::IFile* file, unsigned int folderId )
403
{
404
405
406
    auto type = IFile::Type::UnknownType;
    if ( std::find( begin( supportedVideoExtensions ), end( supportedVideoExtensions ),
                    file->extension() ) != end( supportedVideoExtensions ) )
407
    {
408
        type = IFile::Type::VideoType;
409
    }
410
411
412
413
414
415
416
417
    else if ( std::find( begin( supportedAudioExtensions ), end( supportedAudioExtensions ),
                         file->extension() ) != end( supportedAudioExtensions ) )
    {
        type = IFile::Type::AudioType;
    }
    if ( type == IFile::Type::UnknownType )
        return false;

418
    auto fptr = File::create( m_dbConnection, type, file, folderId );
419
    if ( fptr == nullptr )
420
    {
421
        LOG_ERROR( "Failed to add file ", file->fullPath(), " to the media library" );
422
        return nullptr;
423
    }
424
    LOG_INFO( "Adding ", file->name() );
425
    m_callback->onFileAdded( fptr );
426
    m_parser->parse( fptr, m_callback );
427
    return fptr;
428
429
}