VLCMetadataService.cpp 17.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
#include <chrono>

25
#include "VLCMetadataService.h"
26
#include "Media.h"
27
28
29
30
#include "Album.h"
#include "AlbumTrack.h"
#include "Artist.h"
#include "Show.h"
31
#include "utils/Filename.h"
32

33
#include "Media.h"
34

35
VLCMetadataService::VLCMetadataService(const VLC::Instance& vlc, DBConnection dbConnection, std::shared_ptr<factory::IFileSystem> fsFactory )
36
37
38
    : m_instance( vlc )
    , m_cb( nullptr )
    , m_ml( nullptr )
39
    , m_dbConn( dbConnection )
40
    , m_fsFactory( fsFactory )
41
42
43
{
}

44
bool VLCMetadataService::initialize(IMetadataServiceCb* callback, MediaLibrary* ml )
45
46
47
48
49
50
51
52
53
54
55
{
    m_cb = callback;
    m_ml = ml;
    return true;
}

unsigned int VLCMetadataService::priority() const
{
    return 100;
}

56
void VLCMetadataService::run( std::shared_ptr<Media> file, void* data )
57
{
58
59
60
61
62
63
    if ( file->duration() != -1 )
    {
        LOG_INFO( file->mrl(), " was already parsed" );
        m_cb->done( file, Status::Success, data );
        return;
    }
64
    LOG_INFO( "Parsing ", file->mrl() );
65
    auto chrono = std::chrono::steady_clock::now();
66

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
67
    auto ctx = new Context( file );
68
69
    std::unique_ptr<Context> ctxPtr( ctx );

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
70
71
    ctx->media = VLC::Media( m_instance, file->mrl(), VLC::Media::FromPath );

72
    std::unique_lock<std::mutex> lock( m_mutex );
73
    bool done = false;
74

75
    ctx->media.eventManager().onParsedChanged([this, ctx, &done, &chrono](bool parsed) {
76
        if ( parsed == false )
77
            return;
78
79
        auto duration = std::chrono::steady_clock::now() - chrono;
        LOG_DEBUG("VLC parsing done in ", std::chrono::duration_cast<std::chrono::microseconds>( duration ).count(), "µs" );
80
81
        std::lock_guard<std::mutex> lock( m_mutex );
        done = true;
82
        m_cond.notify_all();
83
    });
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
84
    ctx->media.parseAsync();
85
    auto success = m_cond.wait_for( lock, std::chrono::seconds( 5 ), [&done]() { return done == true; } );
86
    if ( success == false )
87
        m_cb->done( ctx->file, Status::Fatal, data );
88
    else
89
90
    {
        auto status = handleMediaMeta( ctx->file, ctx->media );
91
        m_cb->done( ctx->file, status, data );
92
    }
93
    auto duration = std::chrono::steady_clock::now() - chrono;
94
    LOG_DEBUG( "Parsed ", file->mrl(), " in ", std::chrono::duration_cast<std::chrono::milliseconds>( duration ).count(), "ms" );
95
96
}

97
IMetadataService::Status VLCMetadataService::handleMediaMeta( std::shared_ptr<Media> media, VLC::Media& vlcMedia ) const
98
{
99
    const auto tracks = vlcMedia.tracks();
100
    if ( tracks.size() == 0 )
101
    {
102
        LOG_ERROR( "Failed to fetch tracks" );
103
        return Status::Fatal;
104
    }
105

106
    auto t = m_dbConn->newTransaction();
107
    bool isAudio = true;
108
    for ( const auto& track : tracks )
109
    {
110
111
112
        auto codec = track.codec();
        std::string fcc( (const char*)&codec, 4 );
        if ( track.type() == VLC::MediaTrack::Video )
113
114
        {
            isAudio = false;
115
            auto fps = (float)track.fpsNum() / (float)track.fpsDen();
116
            media->addVideoTrack( fcc, track.width(), track.height(), fps );
117
        }
118
        else if ( track.type() == VLC::MediaTrack::Audio )
119
        {
120
121
            media->addAudioTrack( fcc, track.bitrate(), track.rate(), track.channels(),
                                  track.language(), track.description() );
122
123
        }
    }
124
    t->commit();
125
126
    if ( isAudio == true )
    {
127
        if ( parseAudioFile( media, vlcMedia ) == false )
128
            return Status::Fatal;
129
130
131
    }
    else
    {
132
        if (parseVideoFile( media, vlcMedia ) == false )
133
            return Status::Fatal;
134
    }
135
    auto duration = vlcMedia.duration();
136
137
    media->setDuration( duration );
    if ( media->save() == false )
138
        return Status::Error;
139
    return Status::Success;
140
141
}

142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/* Video files */

bool VLCMetadataService::parseVideoFile( std::shared_ptr<Media> file, VLC::Media& media ) const
{
    file->setType( IMedia::Type::VideoType );
    auto title = media.meta( libvlc_meta_Title );
    if ( title.length() == 0 )
        return true;
    auto showName = media.meta( libvlc_meta_ShowName );
    if ( showName.length() == 0 )
    {
        auto show = m_ml->show( showName );
        if ( show == nullptr )
        {
            show = m_ml->createShow( showName );
            if ( show == nullptr )
                return false;
        }

        auto episodeIdStr = media.meta( libvlc_meta_Episode );
        if ( episodeIdStr.length() > 0 )
        {
            size_t endpos;
            int episodeId = std::stoi( episodeIdStr, &endpos );
            if ( endpos != episodeIdStr.length() )
            {
                LOG_ERROR( "Invalid episode id provided" );
                return true;
            }
            std::shared_ptr<Show> s = std::static_pointer_cast<Show>( show );
            s->addEpisode( title, episodeId );
        }
    }
    else
    {
        // How do we know if it's a movie or a random video?
    }
    return true;
}

/* Audio files */

bool VLCMetadataService::parseAudioFile( std::shared_ptr<Media> media, VLC::Media& vlcMedia ) const
{
    media->setType( IMedia::Type::AudioType );

188
189
    auto cover = vlcMedia.meta( libvlc_meta_ArtworkURL );
    if ( cover.empty() == false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
190
        media->setThumbnail( cover );
191

192
    auto artists = handleArtists( media, vlcMedia );
193
    auto album = handleAlbum( media, vlcMedia, artists.first, artists.second );
194
    if ( album == nullptr )
195
    {
196
        LOG_WARN( "Failed to get/create associated album" );
197
198
        return false;
    }
199
200
201
202
    auto t = m_dbConn->newTransaction();
    auto res = link( media, album, artists.first, artists.second );
    t->commit();
    return res;
203
204
205
}

/* Album handling */
206
std::shared_ptr<Album> VLCMetadataService::findAlbum( Media* media, VLC::Media& vlcMedia, const std::string& title, Artist* albumArtist ) const
207
208
209
{
    static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
            " WHERE title = ?";
210
    auto albums = Album::fetchAll<Album>( m_dbConn, req, title );
211
212
213
214

    if ( albums.size() == 0 )
        return nullptr;

215
216
217
218
    auto discTotalStr = vlcMedia.meta( libvlc_meta_DiscTotal );
    auto discTotal = 0u;
    if ( discTotalStr.empty() == false )
        discTotal = atoi( discTotalStr.c_str() );
219

220
221
222
223
224
    auto discNumberStr = vlcMedia.meta( libvlc_meta_DiscNumber );
    auto discNumber = 0u;
    if ( discNumberStr.empty() == false )
        discNumber = atoi( discNumberStr.c_str() );

225
226
227
228
229
230
231
232
233
    /*
     * Even if we get only 1 album, we need to filter out invalid matches.
     * For instance, if we have already inserted an album "A" by an artist "john"
     * but we are now trying to handle an album "A" by an artist "doe", not filtering
     * candidates would yield the only "A" album we know, while we should return
     * nullptr, so handleAlbum can create a new one.
     */
    for ( auto it = begin( albums ); it != end( albums ); )
    {
234
        auto a = (*it).get();
235
        if ( albumArtist != nullptr )
236
237
238
        {
            // We assume that an album without album artist is a positive match.
            // At the end of the day, without proper tags, there's only so much we can do.
239
240
            auto candidateAlbumArtist = a->albumArtist();
            if ( candidateAlbumArtist != nullptr && candidateAlbumArtist->id() != albumArtist->id() )
241
242
243
244
245
            {
                it = albums.erase( it );
                continue;
            }
        }
246
        // If this is a multidisc album, assume it could be in a multiple amount of folders.
247
        // Since folders can come in any order, we can't assume the first album will be the
248
249
        // first media we see. If the discTotal or discNumber meta are provided, that's easy. If not,
        // we assume that another CD with the same name & artists, and a disc number > 1
250
251
        // denotes a multi disc album
        // Check the first case early to avoid fetching tracks if unrequired.
252
        if ( discTotal > 1 || discNumber > 1 )
253
        {
254
255
256
257
258
259
260
261
262
263
264
265
            ++it;
            continue;
        }
        const auto tracks = a->tracks();
        assert( tracks.size() > 0 );

        auto multiDisc = false;
        for ( auto& t : tracks )
        {
            auto at = t->albumTrack();
            assert( at != nullptr );
            if ( at != nullptr && at->discNumber() > 1 )
266
            {
267
268
                multiDisc = true;
                break;
269
270
            }
        }
271
272
273
274
275
276
277
        if ( multiDisc )
        {
            ++it;
            continue;
        }

        // Assume album files will be in the same folder.
278
279
280
        auto candidateFolder = utils::file::directory( tracks[0]->mrl() );
        auto newFileFolder = utils::file::directory( media->mrl() );
        if ( candidateFolder != newFileFolder )
281
282
283
284
        {
            it = albums.erase( it );
            continue;
        }
285
286
287
288
289
290
291
292
        ++it;
    }
    if ( albums.size() == 0 )
        return nullptr;
    if ( albums.size() > 1 )
    {
        LOG_WARN( "Multiple candidates for album ", title, ". Selecting first one out of luck" );
    }
293
294
295
    return std::static_pointer_cast<Album>( albums[0] );
}

296
std::shared_ptr<Album> VLCMetadataService::handleAlbum( std::shared_ptr<Media> media, VLC::Media& vlcMedia, std::shared_ptr<Artist> albumArtist, std::shared_ptr<Artist> trackArtist ) const
297
{
298
    auto albumTitle = vlcMedia.meta( libvlc_meta_Album );
299
    std::shared_ptr<Album> album;
300
301
302
303
304
305
306
307
308
309
310
311
    std::shared_ptr<Artist> artist = albumArtist;
    if ( artist == nullptr )
    {
        if ( trackArtist != nullptr )
            artist = trackArtist;
        else
        {
            //FIXME: We might fetch the unknown artist twice while parsing the same file, that sucks.
            artist = Artist::fetch( m_dbConn, medialibrary::UnknownArtistID );
        }
    }

312
    if ( albumTitle.length() > 0 )
313
    {
314
        album = findAlbum( media.get(), vlcMedia, albumTitle, albumArtist.get() );
315

316
        if ( album == nullptr )
317
        {
318
319
320
            album = m_ml->createAlbum( albumTitle );
            if ( album != nullptr )
            {
321
                auto artwork = vlcMedia.meta( libvlc_meta_ArtworkURL );
322
                if ( artwork.length() != 0 )
323
                    album->setArtworkMrl( artwork );
324
            }
325
        }
326
    }
327
    else
328
329
        album = artist->unknownAlbum();

330
331
    if ( album == nullptr )
        return nullptr;
332
333
    // If we know a track artist, specify it, otherwise, fallback to the album/unknown artist
    auto track = handleTrack( album, media, vlcMedia, trackArtist ? trackArtist : artist );
334
335
336
    if ( track != nullptr )
        media->setAlbumTrack( track );
    return album;
337
338
}

339
340
341
342
343
344
345
346
///
/// \brief VLCMetadataService::handleArtists Returns Artist's involved on a track
/// \param media The track to analyze
/// \param vlcMedia VLC's media
/// \return A pair containing:
/// The album artist as a first element
/// The track artist as a second element, if it differs from the album artist.
///
347
std::pair<std::shared_ptr<Artist>, std::shared_ptr<Artist>> VLCMetadataService::handleArtists( std::shared_ptr<Media> media, VLC::Media& vlcMedia ) const
348
{
349
    std::shared_ptr<Artist> albumArtist;
350
    std::shared_ptr<Artist> artist;
351
    auto albumArtistName = vlcMedia.meta( libvlc_meta_AlbumArtist );
352
353
    auto artistName = vlcMedia.meta( libvlc_meta_Artist );

354
    if ( albumArtistName.empty() == true && artistName.empty() == true )
355
    {
356
        return {nullptr, nullptr};
357
358
359
360
361
362
    }

    if ( albumArtistName.empty() == false )
    {
        albumArtist = std::static_pointer_cast<Artist>( m_ml->artist( albumArtistName ) );
        if ( albumArtist == nullptr )
363
        {
364
365
            albumArtist = m_ml->createArtist( albumArtistName );
            if ( albumArtist == nullptr )
366
            {
367
                LOG_ERROR( "Failed to create new artist ", albumArtistName );
368
                return {nullptr, nullptr};
369
370
371
            }
        }
    }
372
    if ( artistName.empty() == false && artistName != albumArtistName )
373
374
375
376
377
378
379
380
    {
        artist = std::static_pointer_cast<Artist>( m_ml->artist( artistName ) );
        if ( artist == nullptr )
        {
            artist = m_ml->createArtist( artistName );
            if ( artist == nullptr )
            {
                LOG_ERROR( "Failed to create new artist ", artistName );
381
                return {nullptr, nullptr};
382
383
384
            }
        }
    }
385
386
387
388

    if ( artistName.length() > 0 )
        media->setArtist( artistName );
    else if ( albumArtistName.length() > 0 )
389
    {
390
391
392
        // Always provide an artist, to avoid the user from having to fallback
        // to the album artist by himself
        media->setArtist( albumArtistName );
393
    }
394
    return {albumArtist, artist};
395
}
396

397
398
/* Tracks handling */

399
std::shared_ptr<AlbumTrack> VLCMetadataService::handleTrack( std::shared_ptr<Album> album, std::shared_ptr<Media> media, VLC::Media& vlcMedia, std::shared_ptr<Artist> artist ) const
400
{
401
    auto trackNbStr = vlcMedia.meta( libvlc_meta_TrackNumber );
402

403
    auto title = vlcMedia.meta( libvlc_meta_Title );
404
    if ( title.empty() == true )
405
406
    {
        LOG_WARN( "Failed to get track title" );
407
408
409
410
411
        if ( trackNbStr.empty() == false )
        {
            title = "Track #";
            title += trackNbStr;
        }
412
    }
413
414
415
416
    if ( title.empty() == false )
        media->setTitle( title );
    unsigned int trackNb;
    if ( trackNbStr.empty() == false )
417
        trackNb = atoi( trackNbStr.c_str() );
418
419
    else
        trackNb = 0;
420
421
422
423
424
425
426

    auto discNumberStr = vlcMedia.meta( libvlc_meta_DiscNumber );
    auto discNumber = 0;
    if ( discNumberStr.empty() == false )
        discNumber = atoi( discNumberStr.c_str() );

    auto track = std::static_pointer_cast<AlbumTrack>( album->addTrack( media, trackNb, discNumber ) );
427
428
429
430
431
    if ( track == nullptr )
    {
        LOG_ERROR( "Failed to create album track" );
        return nullptr;
    }
432
433
    if ( artist != nullptr )
        track->setArtist( artist );
434
    auto genre = vlcMedia.meta( libvlc_meta_Genre );
435
    if ( genre.length() != 0 )
436
    {
437
        track->setGenre( genre );
438
    }
439
440
441
442
443
    auto releaseYearStr = vlcMedia.meta( libvlc_meta_Date );
    if ( releaseYearStr.empty() == false )
    {
        auto releaseYear = atoi( releaseYearStr.c_str() );
        track->setReleaseYear( releaseYear );
444
445
446
447
        // Let the album handle multiple dates. In order to do this properly, we need
        // to know if the date has been changed before, which can be known only by
        // using Album class internals.
        album->setReleaseYear( releaseYear, false );
448
    }
449
450
    return track;
}
451

452
453
/* Misc */

454
bool VLCMetadataService::link( std::shared_ptr<Media> media, std::shared_ptr<Album> album,
455
                               std::shared_ptr<Artist> albumArtist, std::shared_ptr<Artist> artist ) const
456
{
457
458
    if ( albumArtist == nullptr && artist == nullptr )
    {
459
        albumArtist = Artist::fetch( m_dbConn, medialibrary::UnknownArtistID );
460
461
    }

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
462
    // We might modify albumArtist later, hence handle thumbnails before.
463
464
465
    // If we have an albumArtist (meaning the track was properly tagged, we
    // can assume this artist is a correct match. We can use the thumbnail from
    // the current album for the albumArtist, if none has been set before.
466
467
    if ( albumArtist != nullptr && albumArtist->artworkMrl().empty() == true && album != nullptr )
        albumArtist->setArtworkMrl( album->artworkMrl() );
468
469
470
471
472
473
474
475
476
477

    if ( albumArtist != nullptr )
        albumArtist->addMedia( media.get() );
    if ( artist != nullptr && ( albumArtist == nullptr || albumArtist->id() != artist->id() ) )
        artist->addMedia( media.get() );

    auto currentAlbumArtist = album->albumArtist();

    // If we have no main artist yet, that's easy, we need to assign one.
    if ( currentAlbumArtist == nullptr )
478
    {
479
480
        // If the track was properly tagged, that's even better.
        if ( albumArtist != nullptr )
481
        {
482
483
484
485
            album->setAlbumArtist( albumArtist.get() );
            // Always add the album artist as an artist
            album->addArtist( albumArtist );
            if ( artist != nullptr )
486
487
                album->addArtist( artist );
        }
488
489
        // If however we only have an artist, as opposed to an album artist, we
        // add it, until proven we were wrong (ie. until one of the next tracks has a different artist)
490
491
        else
        {
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
            // Guaranteed to be true since we need at least an artist to enter here.
            assert( artist != nullptr );
            album->setAlbumArtist( artist.get() );
        }
    }
    else
    {
        if ( albumArtist == nullptr )
        {
            // Fallback to artist, since the same logic would apply anyway
            albumArtist = artist;
        }
        if ( albumArtist->id() != currentAlbumArtist->id() )
        {
            // We have more than a single artist on this album, fallback to various artists
            auto variousArtists = Artist::fetch( m_dbConn, medialibrary::VariousArtistID );
            album->setAlbumArtist( variousArtists.get() );
            // Add those two artists as "featuring".
            album->addArtist( albumArtist );
        }
        if ( artist != nullptr && artist->id() != albumArtist->id() )
        {
            if ( albumArtist->id() != artist->id() )
               album->addArtist( artist );
516
        }
517
    }
518

519
520
    return true;
}