MetadataParser.cpp 29.7 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
#if HAVE_CONFIG_H
# include "config.h"
#endif

27
28
29
30
31
#include "MetadataParser.h"
#include "Album.h"
#include "AlbumTrack.h"
#include "Artist.h"
#include "File.h"
32
33
34
#include "filesystem/IDevice.h"
#include "filesystem/IDirectory.h"
#include "Folder.h"
35
#include "Genre.h"
36
#include "Media.h"
37
#include "Playlist.h"
38
#include "Show.h"
39
#include "utils/Directory.h"
40
#include "utils/Filename.h"
41
#include "utils/Url.h"
42
#include "utils/ModificationsNotifier.h"
43
44
45
#include "discoverer/FsDiscoverer.h"
#include "discoverer/probe/PathProbe.h"

46
#include <cstdlib>
47

48
49
50
namespace medialibrary
{

51
52
53
54
55
MetadataParser::MetadataParser()
    : m_previousFolderId( 0 )
{
}

56
57
bool MetadataParser::initialize()
{
58
    m_unknownArtist = Artist::fetch( m_ml, UnknownArtistID );
59
60
61
62
63
    if ( m_unknownArtist == nullptr )
        LOG_ERROR( "Failed to cache unknown artist" );
    return m_unknownArtist != nullptr;
}

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
int MetadataParser::toInt( VLC::Media& vlcMedia, libvlc_meta_t meta, const char* name )
{
    auto str = vlcMedia.meta( meta );
    if ( str.empty() == false )
    {
        try
        {
            return std::stoi( str );
        }
        catch( std::logic_error& ex)
        {
            LOG_WARN( "Invalid ", name, " provided (", str, "): ", ex.what() );
        }
    }
    return 0;
}

81
82
parser::Task::Status MetadataParser::run( parser::Task& task )
{
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
    bool alreadyInParser = false;
    int nbSubitem = task.vlcMedia.subitems()->count();
    // Assume that file containing subitem(s) is a Playlist
    if ( nbSubitem > 0 )
    {
        auto res = addPlaylistMedias( task, nbSubitem );
        if ( res == false ) // playlist addition may fail due to constraint violation
            return parser::Task::Status::Fatal;

        assert( task.file != nullptr );
        task.markStepCompleted( parser::Task::ParserStep::Completed );
        task.file->saveParserStep();
        return parser::Task::Status::Success;
    }

98
    if ( task.file == nullptr )
99
    {
100
        assert( task.media == nullptr );
101
102
        // Try to create Media & File
        try
103
        {
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
            auto t = m_ml->getConn()->newTransaction();
            LOG_INFO( "Adding ", task.mrl );
            task.media = Media::create( m_ml, IMedia::Type::Unknown, utils::file::fileName( task.mrl ) );
            if ( task.media == nullptr )
            {
                LOG_ERROR( "Failed to add media ", task.mrl, " to the media library" );
                return parser::Task::Status::Fatal;
            }
            // For now, assume all media are made of a single file
            task.file = task.media->addFile( *task.fileFs, task.parentFolder->id(),
                                             task.parentFolderFs->device()->isRemovable(),
                                             File::Type::Main );
            if ( task.file == nullptr )
            {
                LOG_ERROR( "Failed to add file ", task.mrl, " to media #", task.media->id() );
                return parser::Task::Status::Fatal;
            }
            t->commit();
            // Synchronize file step tracker with task
            task.markStepCompleted( task.step );
124
        }
125
126
        // Voluntarily trigger an exception for a valid, but less common case, to avoid database overhead
        catch ( sqlite::errors::ConstraintViolation& ex )
127
        {
128
129
130
131
132
133
134
135
136
137
138
139
140
141
            LOG_INFO( "Creation of Media & File failed because ", ex.what(),
                      ". Assuming this task is a duplicate" );
            // Try to retrieve file & Media from database
            auto fileInDB = File::fromMrl( m_ml, task.mrl );
            if ( fileInDB == nullptr ) // The file is no longer present in DB, gracefully delete task
            {
                LOG_ERROR( "File ", task.mrl, " no longer present in DB, aborting");
                return parser::Task::Status::Fatal;
            }
            task.file = fileInDB;
            task.media = fileInDB->media();
            if ( task.media == nullptr ) // Without a media, we cannot go further
                return parser::Task::Status::Fatal;
            alreadyInParser = true;
142
        }
143
    }
144
145
146
147
148
149
150
151
152
    else if ( task.media == nullptr )
    {
        // If we have a file but no media, this is a problem, we can analyze as
        // much as we want, but won't be able to store anything.
        // Keep in mind that if we are in this code path, we are not analyzing
        // a playlist.
        assert( false );
        return parser::Task::Status::Fatal;
    }
153
154
155
156
157
158
159
160

    if ( task.parentPlaylist != nullptr )
        task.parentPlaylist->add( task.media->id(), task.parentPlaylistIndex );

    if ( alreadyInParser == true )
    {
        task.step = parser::Task::ParserStep::Completed; // Let the worker drop this duplicate task
        return parser::Task::Status::Success;
161
162
    }

163
    const auto& tracks = task.vlcMedia.tracks();
164
165
166
167
168

    // If we failed to extract any tracks, don't make any assumption and forward to the
    // thumbnailer. Since it starts an actual playback, it will have more information.
    // Since the metadata steps won't be marked, it will run again once the thumbnailer has completed.
    if ( tracks.empty() == true )
169
170
    {
        // However, if the file is not unknown anymore, it means the thumbnailer has already processed it
171
        if ( task.media->type() == Media::Type::Unknown )
172
        {
173
174
175
176
            // In case the thumbnailer ran before, but the application exited, we would skip the
            // thumbnailer execution, coming back here, and delegating again to the thumbnailer
            // over and over again. We need to ensure the thumbnailer will run, even partially, up to
            // the point the playback started.
177
            task.markStepUncompleted( parser::Task::ParserStep::Thumbnailer );
178
            LOG_INFO( "Skipping metadata parsing for file with unknown type: ", task.file->mrl() );
179
            return parser::Task::Status::Success;
180
        }
181
182
183
        // In that case, stop trying to do something with this file.
        return parser::Task::Status::Fatal;
    }
184

185
    bool isAudio = true;
186
    {
187
188
189
        using TracksT = decltype( tracks );
        sqlite::Tools::withRetries( 3, [this, &isAudio, &task]( TracksT tracks ) {
            auto t = m_ml->getConn()->newTransaction();
190
            for ( const auto& track : tracks )
191
            {
192
                auto codec = track.codec();
193
                std::string fcc( reinterpret_cast<const char*>( &codec ), 4 );
194
                if ( track.type() == VLC::MediaTrack::Type::Video )
195
                {
196
197
198
                    task.media->addVideoTrack( fcc, track.width(), track.height(),
                                          static_cast<float>( track.fpsNum() ) / static_cast<float>( track.fpsDen() ),
                                          track.language(), track.description() );
199
200
                    isAudio = false;
                }
201
                else if ( track.type() == VLC::MediaTrack::Type::Audio )
202
                {
203
204
                    task.media->addAudioTrack( fcc, track.bitrate(), track.rate(), track.channels(),
                                          track.language(), track.description() );
205
                }
206
            }
207
208
209
            task.media->setDuration( task.vlcMedia.duration() );
            t->commit();
        }, std::move( tracks ) );
210
211
212
    }
    if ( isAudio == true )
    {
213
        if ( parseAudioFile( task ) == false )
214
215
216
217
            return parser::Task::Status::Fatal;
    }
    else
    {
218
        if (parseVideoFile( task ) == false )
219
220
            return parser::Task::Status::Fatal;
    }
221

222
223
224
    if ( task.file->isDeleted() == true || task.media->isDeleted() == true )
        return parser::Task::Status::Fatal;

225
    task.markStepCompleted( parser::Task::ParserStep::MetadataAnalysis );
226
227
    // Save ourselves from the useless processing of a thumbnail later if
    // we're analyzing an audio file
228
    if ( isAudio == true && utils::file::schemeIs( "attachment://", task.media->thumbnail() ) == false )
229
        task.markStepCompleted( parser::Task::ParserStep::Thumbnailer );
230
    if ( task.file->saveParserStep() == false )
231
        return parser::Task::Status::Fatal;
232
    m_notifier->notifyMediaCreation( task.media );
233
234
235
    return parser::Task::Status::Success;
}

236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
/* Playlist files */

bool MetadataParser::addPlaylistMedias( parser::Task& task, int nbSubitem ) const
{
    auto t = m_ml->getConn()->newTransaction();
    LOG_INFO( "Try to import ", task.mrl, " as a playlist" );
    auto playlistName = task.vlcMedia.meta( libvlc_meta_Title );
    if ( playlistName.empty() == true )
        playlistName = utils::url::decode( utils::file::fileName( task.mrl ) );
    auto playlistPtr = Playlist::create( m_ml, playlistName );
    if ( playlistPtr == nullptr )
    {
        LOG_ERROR( "Failed to create playlist ", task.mrl, " to the media library" );
        return false;
    }
    task.file = playlistPtr->addFile( *task.fileFs, task.parentFolder->id(), task.parentFolderFs->device()->isRemovable() );
    if ( task.file == nullptr )
    {
        LOG_ERROR( "Failed to add playlist file ", task.mrl );
        return false;
    }
    t->commit();
    auto subitems = task.vlcMedia.subitems();
    for ( int i = 0; i < nbSubitem; ++i ) // FIXME: Interrupt loop if paused
        addPlaylistElement( task, playlistPtr, subitems->itemAtIndex( i ), static_cast<unsigned int>( i ) + 1 );

    return true;
}

void MetadataParser::addPlaylistElement( parser::Task& task, const std::shared_ptr<Playlist>& playlistPtr,
                                         VLC::MediaPtr subitem, unsigned int index ) const
{
    if ( subitem == nullptr )
        return;
    const auto& mrl = subitem->mrl();
    LOG_INFO( "Try to add ", mrl, " to the playlist ", task.mrl );
    auto media = m_ml->media( mrl );
    if ( media != nullptr )
    {
        LOG_INFO( "Media for ", mrl, " already exists, adding it to the playlist ", task.mrl );
        playlistPtr->add( media->id(), index );
        return;
    }
    // Create Media, etc.
    auto fsFactory = m_ml->fsFactoryForMrl( mrl );

    if ( fsFactory == nullptr ) // Media not supported by any FsFactory, registering it as external
    {
        auto t2 = m_ml->getConn()->newTransaction();
        auto externalMedia = Media::create( m_ml, IMedia::Type::Unknown, utils::url::encode(
                subitem->meta( libvlc_meta_Title ) ) );
        if ( externalMedia == nullptr )
        {
            LOG_ERROR( "Failed to create external media for ", mrl, " in the playlist ", task.mrl );
            return;
        }
        // Assuming that external mrl present in playlist file is a main media resource
        auto externalFile = externalMedia->addExternalMrl( mrl, IFile::Type::Main );
        if ( externalFile == nullptr )
            LOG_ERROR( "Failed to create external file for ", mrl, " in the playlist ", task.mrl );
        playlistPtr->add( externalMedia->id(), index );
        t2->commit();
        return;
    }
    bool isDirectory;
    try
    {
        isDirectory = utils::fs::isDirectory( utils::file::toLocalPath( mrl ) );
    }
    catch ( std::system_error& ex )
    {
        LOG_ERROR( ex.what() );
        return;
    }
    LOG_INFO( "Importing ", isDirectory ? "folder " : "file ", mrl, " in the playlist ", task.mrl );
    auto directoryMrl = utils::file::directory( mrl );
    auto parentFolder = Folder::fromMrl( m_ml, directoryMrl );
    bool parentKnown = parentFolder != nullptr;

    auto entryPoint = utils::file::scheme( mrl ) + '/' + utils::file::firstFolder( utils::file::stripScheme( mrl ) );
    if ( parentKnown == false && Folder::fromMrl( m_ml, entryPoint ) != nullptr )
    {
        auto probePtr = std::unique_ptr<prober::PathProbe>( new prober::PathProbe{ utils::file::stripScheme( mrl ),
                                                                                   isDirectory, playlistPtr, parentFolder,
                                                                                   utils::file::stripScheme( directoryMrl ), index, true } );
        auto discoverer = FsDiscoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
        discoverer.reload( entryPoint );
        return;
    }
    auto probePtr = std::unique_ptr<prober::PathProbe>( new prober::PathProbe{ utils::file::stripScheme( mrl ),
                                                                               isDirectory, playlistPtr, parentFolder,
                                                                               utils::file::stripScheme( directoryMrl ), index, false } );
    auto discoverer = FsDiscoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
    if ( parentKnown == false )
    {
        discoverer.discover( entryPoint );
332
333
334
        auto entryFolder = Folder::fromMrl( m_ml, entryPoint );
        if ( entryFolder != nullptr )
            Folder::excludeEntryFolder( m_ml, entryFolder->id() );
335
336
337
338
339
        return;
    }
    discoverer.reload( directoryMrl );
}

340
341
/* Video files */

342
bool MetadataParser::parseVideoFile( parser::Task& task ) const
343
{
344
    auto media = task.media.get();
345
    media->setType( IMedia::Type::Video );
346
347
    const auto& title = task.vlcMedia.meta( libvlc_meta_Title );
    if ( title.length() == 0 )
348
        return true;
349

350
    const auto& showName = task.vlcMedia.meta( libvlc_meta_ShowName );
351

352
353
354
355
356
357
    return sqlite::Tools::withRetries( 3, [this, &showName, &title, &task]() {
        auto t = m_ml->getConn()->newTransaction();
        task.media->setTitleBuffered( title );

        if ( showName.length() != 0 )
        {
358
            auto show = m_ml->show( showName );
359
            if ( show == nullptr )
360
361
362
363
364
365
366
367
368
369
370
            {
                show = m_ml->createShow( showName );
                if ( show == nullptr )
                    return false;
            }
            auto episode = toInt( task.vlcMedia, libvlc_meta_Episode, "episode number" );
            if ( episode != 0 )
            {
                std::shared_ptr<Show> s = std::static_pointer_cast<Show>( show );
                s->addEpisode( *task.media, title, episode );
            }
371
372
373
374
375
376
377
378
379
        }
        else
        {
            // How do we know if it's a movie or a random video?
        }
        task.media->save();
        t->commit();
        return true;
    });
380
381
382
383
384
    return true;
}

/* Audio files */

385
bool MetadataParser::parseAudioFile( parser::Task& task )
386
{
387
    task.media->setType( IMedia::Type::Audio );
388

389
    auto artworkMrl = task.vlcMedia.meta( libvlc_meta_ArtworkURL );
390
    if ( artworkMrl.empty() == false )
391
    {
392
        task.media->setThumbnail( artworkMrl );
393
394
395
396
397
        // Don't use an attachment as default artwork for album/artists
        if ( utils::file::schemeIs( "attachment", artworkMrl ) )
            artworkMrl.clear();
    }

398

399
    auto genre = handleGenre( task );
400
    auto artists = findOrCreateArtist( task );
401
402
    if ( artists.first == nullptr && artists.second == nullptr )
        return false;
403
    auto album = findAlbum( task, artists.first, artists.second );
404
405
406
    return sqlite::Tools::withRetries( 3, [this, &task, &artists]( std::string artworkMrl,
                                                  std::shared_ptr<Album> album, std::shared_ptr<Genre> genre ) {
        auto t = m_ml->getConn()->newTransaction();
407
        if ( album == nullptr )
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
        {
            const auto& albumName = task.vlcMedia.meta( libvlc_meta_Album );
            album = m_ml->createAlbum( albumName, artworkMrl );
            if ( album == nullptr )
                return false;
            m_notifier->notifyAlbumCreation( album );
        }
        // If we know a track artist, specify it, otherwise, fallback to the album/unknown artist
        auto track = handleTrack( album, task, artists.second ? artists.second : artists.first,
                                  genre.get() );

        auto res = link( *task.media, album, artists.first, artists.second );
        task.media->save();
        t->commit();
        return res;
    }, std::move( artworkMrl ), std::move( album ), std::move( genre ) );
424
425
}

426
427
std::shared_ptr<Genre> MetadataParser::handleGenre( parser::Task& task ) const
{
428
429
    const auto& genreStr = task.vlcMedia.meta( libvlc_meta_Genre );
    if ( genreStr.length() == 0 )
430
        return nullptr;
431
    auto genre = Genre::fromName( m_ml, genreStr );
432
433
    if ( genre == nullptr )
    {
434
        genre = Genre::create( m_ml, genreStr );
435
        if ( genre == nullptr )
436
            LOG_ERROR( "Failed to get/create Genre", genreStr );
437
438
439
440
    }
    return genre;
}

441
/* Album handling */
442
443

std::shared_ptr<Album> MetadataParser::findAlbum( parser::Task& task, std::shared_ptr<Artist> albumArtist,
444
                                                    std::shared_ptr<Artist> trackArtist )
445
{
446
447
    const auto& albumName = task.vlcMedia.meta( libvlc_meta_Album );
    if ( albumName.empty() == true )
448
    {
449
450
        if ( albumArtist != nullptr )
            return albumArtist->unknownAlbum();
451
        if ( trackArtist != nullptr )
452
453
            return trackArtist->unknownAlbum();
        return m_unknownArtist->unknownAlbum();
454
455
    }

456
457
458
459
460
461
    if ( m_previousAlbum != nullptr && albumName == m_previousAlbum->title() &&
         m_previousFolderId != 0 && task.file->folderId() == m_previousFolderId )
        return m_previousAlbum;
    m_previousAlbum.reset();
    m_previousFolderId = 0;

462
463
    // Album matching depends on the difference between artist & album artist.
    // Specificaly pass the albumArtist here.
464
465
    static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
            " WHERE title = ?";
466
    auto albums = Album::fetchAll<Album>( m_ml, req, albumName );
467
468
469
470

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

471
    const auto discTotal = toInt( task.vlcMedia, libvlc_meta_DiscTotal, "disc total" );
472
    const auto discNumber = toInt( task.vlcMedia, libvlc_meta_DiscNumber, "disc number" );
473
474
475
476
477
    /*
     * 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
478
     * nullptr, so the link() method can create a new one.
479
480
481
482
     */
    for ( auto it = begin( albums ); it != end( albums ); )
    {
        auto a = (*it).get();
483
        auto candidateAlbumArtist = a->albumArtist();
484
485
486
487
        // When we find an album, we will systematically assign an artist to it.
        // Not having an album artist (even it it's only a temporary one in the
        // case of a compilation album) is not expected at all.
        assert( candidateAlbumArtist != nullptr );
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
        if ( albumArtist != nullptr )
        {
            // 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.
            if ( candidateAlbumArtist != nullptr && candidateAlbumArtist->id() != albumArtist->id() )
            {
                it = albums.erase( it );
                continue;
            }
        }
        // If this is a multidisc album, assume it could be in a multiple amount of folders.
        // Since folders can come in any order, we can't assume the first album will be the
        // 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
        // denotes a multi disc album
        // Check the first case early to avoid fetching tracks if unrequired.
504
        if ( discTotal > 1 || discNumber > 1 )
505
506
507
508
        {
            ++it;
            continue;
        }
509
        const auto tracks = a->cachedTracks();
510
511
512
513
514
515
516
        // If there is no tracks to compare with, we just have to hope this will be the only valid
        // album match
        if ( tracks.size() == 0 )
        {
            ++it;
            continue;
        }
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534

        auto multiDisc = false;
        for ( auto& t : tracks )
        {
            auto at = t->albumTrack();
            assert( at != nullptr );
            if ( at != nullptr && at->discNumber() > 1 )
            {
                multiDisc = true;
                break;
            }
        }
        if ( multiDisc )
        {
            ++it;
            continue;
        }

535
536
537
538
539
540
541
542
        // Attempt to discriminate by date, but only for the same artists.
        // Not taking the artist in consideration would cause compilation to
        // create multiple albums, especially when track are only partially
        // tagged with a year.
        if ( ( albumArtist != nullptr && candidateAlbumArtist != nullptr &&
             albumArtist->id() == candidateAlbumArtist->id() ) ||
             ( trackArtist != nullptr && candidateAlbumArtist != nullptr &&
               trackArtist->id() == candidateAlbumArtist->id() ) )
543
        {
544
545
            auto candidateDate = task.vlcMedia.meta( libvlc_meta_Date );
            if ( candidateDate.empty() == false )
546
            {
547
548
549
550
551
552
553
554
555
556
557
558
559
                try
                {
                    unsigned int year = std::stoi( candidateDate );
                    if ( year != a->releaseYear() )
                        it = albums.erase( it );
                    else
                        ++it;
                    continue;
                }
                catch (...)
                {
                    // Date wasn't helpful, simply ignore the error and continue
                }
560
561
562
            }
        }

563
        // Assume album files will be in the same folder.
564
        auto newFileFolder = utils::file::directory( task.file->mrl() );
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
        auto trackFiles = tracks[0]->files();
        bool excluded = false;
        for ( auto& f : trackFiles )
        {
            auto candidateFolder = utils::file::directory( f->mrl() );
            if ( candidateFolder != newFileFolder )
            {
                excluded = true;
                break;
            }
        }
        if ( excluded == true )
        {
            it = albums.erase( it );
            continue;
        }
        ++it;
    }
    if ( albums.size() == 0 )
        return nullptr;
    if ( albums.size() > 1 )
    {
587
        LOG_WARN( "Multiple candidates for album ", albumName, ". Selecting first one out of luck" );
588
    }
589
590
591
    m_previousFolderId = task.file->folderId();
    m_previousAlbum = albums[0];
    return albums[0];
592
593
594
595
}

///
/// \brief MetadataParser::handleArtists Returns Artist's involved on a track
596
/// \param task The current parser task
597
598
599
600
/// \return A pair containing:
/// The album artist as a first element
/// The track artist as a second element, or nullptr if it is the same as album artist
///
601
std::pair<std::shared_ptr<Artist>, std::shared_ptr<Artist>> MetadataParser::findOrCreateArtist( parser::Task& task ) const
602
603
604
{
    std::shared_ptr<Artist> albumArtist;
    std::shared_ptr<Artist> artist;
605
    static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name + " WHERE name = ?";
606

607
608
609
    const auto& albumArtistStr = task.vlcMedia.meta( libvlc_meta_AlbumArtist );
    const auto& artistStr = task.vlcMedia.meta( libvlc_meta_Artist );
    if ( albumArtistStr.empty() == true && artistStr.empty() == true )
610
    {
611
        return {m_unknownArtist, m_unknownArtist};
612
613
    }

614
    if ( albumArtistStr.empty() == false )
615
    {
616
        albumArtist = Artist::fetch( m_ml, req, albumArtistStr );
617
618
        if ( albumArtist == nullptr )
        {
619
            albumArtist = m_ml->createArtist( albumArtistStr );
620
621
            if ( albumArtist == nullptr )
            {
622
                LOG_ERROR( "Failed to create new artist ", albumArtistStr );
623
624
                return {nullptr, nullptr};
            }
625
            m_notifier->notifyArtistCreation( albumArtist );
626
627
        }
    }
628
    if ( artistStr.empty() == false && artistStr != albumArtistStr )
629
    {
630
        artist = Artist::fetch( m_ml, req, artistStr );
631
632
        if ( artist == nullptr )
        {
633
            artist = m_ml->createArtist( artistStr );
634
635
            if ( artist == nullptr )
            {
636
                LOG_ERROR( "Failed to create new artist ", artistStr );
637
638
                return {nullptr, nullptr};
            }
639
            m_notifier->notifyArtistCreation( albumArtist );
640
641
642
643
644
645
646
        }
    }
    return {albumArtist, artist};
}

/* Tracks handling */

647
648
std::shared_ptr<AlbumTrack> MetadataParser::handleTrack( std::shared_ptr<Album> album, parser::Task& task,
                                                         std::shared_ptr<Artist> artist, Genre* genre ) const
649
{
650
651
652
    auto title = task.vlcMedia.meta( libvlc_meta_Title );
    const auto trackNumber = toInt( task.vlcMedia, libvlc_meta_TrackNumber, "track number" );
    const auto discNumber = toInt( task.vlcMedia, libvlc_meta_DiscNumber, "disc number" );
653
654
655
    if ( title.empty() == true )
    {
        LOG_WARN( "Failed to get track title" );
656
        if ( trackNumber != 0 )
657
658
        {
            title = "Track #";
659
            title += std::to_string( trackNumber );
660
661
662
        }
    }
    if ( title.empty() == false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
663
        task.media->setTitleBuffered( title );
664

665
666
    auto track = std::static_pointer_cast<AlbumTrack>( album->addTrack( task.media, trackNumber,
                                                                        discNumber, artist->id(),
667
                                                                        genre ) );
668
669
670
671
672
    if ( track == nullptr )
    {
        LOG_ERROR( "Failed to create album track" );
        return nullptr;
    }
673

674
675
    const auto& releaseDate = task.vlcMedia.meta( libvlc_meta_Date );
    if ( releaseDate.empty() == false )
676
    {
677
        auto releaseYear = atoi( releaseDate.c_str() );
678
        task.media->setReleaseDate( releaseYear );
679
680
681
682
683
        // 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 );
    }
684
    m_notifier->notifyAlbumTrackCreation( track );
685
686
687
688
689
690
    return track;
}

/* Misc */

bool MetadataParser::link( Media& media, std::shared_ptr<Album> album,
691
                               std::shared_ptr<Artist> albumArtist, std::shared_ptr<Artist> artist )
692
{
693
    if ( albumArtist == nullptr )
694
695
    {
        assert( artist != nullptr );
696
        albumArtist = artist;
697
    }
698
699
700
701
702

    // We might modify albumArtist later, hence handle thumbnails before.
    // 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.
703
704
705
706
707
    // Although we don't want to do this for unknown/various artists, as the
    // thumbnail wouldn't reflect those "special" artists
    if ( albumArtist != nullptr && albumArtist->id() != UnknownArtistID &&
         albumArtist->id() != VariousArtistID &&
         albumArtist->artworkMrl().empty() == true &&
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
         album != nullptr && album->artworkMrl().empty() == false )
        albumArtist->setArtworkMrl( album->artworkMrl() );

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

    auto currentAlbumArtist = album->albumArtist();

    // If we have no main artist yet, that's easy, we need to assign one.
    if ( currentAlbumArtist == nullptr )
    {
        // We don't know if the artist was tagged as artist or albumartist, however, we simply add it
        // as the albumartist until proven we were wrong (ie. until one of the next tracks
        // has a different artist)
724
        album->setAlbumArtist( albumArtist );
725
726
727
728
729
730
731
732
733
734
        // Always add the album artist as an artist
        album->addArtist( albumArtist );
        if ( artist != nullptr )
            album->addArtist( artist );
    }
    else
    {
        if ( albumArtist->id() != currentAlbumArtist->id() )
        {
            // We have more than a single artist on this album, fallback to various artists
735
736
737
            if ( m_variousArtists == nullptr )
                m_variousArtists = Artist::fetch( m_ml, VariousArtistID );
            album->setAlbumArtist( m_variousArtists );
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
            // Add those two artists as "featuring".
            album->addArtist( albumArtist );
        }
        if ( artist != nullptr && artist->id() != albumArtist->id() )
        {
            if ( albumArtist->id() != artist->id() )
               album->addArtist( artist );
        }
    }

    return true;
}

const char* MetadataParser::name() const
{
    return "Metadata";
}
755
756
757
758
759
760
761
762
763
764

uint8_t MetadataParser::nbThreads() const
{
//    auto nbProcs = std::thread::hardware_concurrency();
//    if ( nbProcs == 0 )
//        return 1;
//    return nbProcs;
    // Let's make this code thread-safe first :)
    return 1;
}
765

766
767
768
769
770
771
772
773
774
775
void MetadataParser::flush()
{
    ParserService::flush();
    // Reset locally cached entities
    initialize();
    m_variousArtists = nullptr;
    m_previousAlbum = nullptr;
    m_previousFolderId = 0;
}

776
bool MetadataParser::isCompleted( const parser::Task& task ) const
777
{
778
    // We always need to run this task if the metadata extraction isn't completed
779
780
    return ( static_cast<uint8_t>( task.step ) &
            static_cast<uint8_t>( parser::Task::ParserStep::MetadataAnalysis ) ) != 0;
781
782
}

783
}