MetadataParser.cpp 31 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
    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 );
94
        task.saveParserStep();
95 96 97
        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
            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;
            }
121
            task.updateFileId();
122
            t->commit();
123
        }
124 125
        // Voluntarily trigger an exception for a valid, but less common case, to avoid database overhead
        catch ( sqlite::errors::ConstraintViolation& ex )
126
        {
127 128 129 130 131 132 133 134 135 136 137 138 139 140
            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;
141
        }
142
    }
143 144 145 146 147 148 149 150 151
    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;
    }
152 153 154 155 156 157

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

    if ( alreadyInParser == true )
    {
158 159
        // Let the worker drop this duplicate task
        task.markStepCompleted( parser::Task::ParserStep::Completed );
160 161
        // And remove it from DB
        task.destroy( m_ml, task.id() );
162
        return parser::Task::Status::Success;
163 164
    }

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

    if ( tracks.empty() == true )
168
        return parser::Task::Status::Fatal;
169

170
    bool isAudio = true;
171
    {
172 173 174
        using TracksT = decltype( tracks );
        sqlite::Tools::withRetries( 3, [this, &isAudio, &task]( TracksT tracks ) {
            auto t = m_ml->getConn()->newTransaction();
175
            for ( const auto& track : tracks )
176
            {
177
                auto codec = track.codec();
178
                std::string fcc( reinterpret_cast<const char*>( &codec ), 4 );
179
                if ( track.type() == VLC::MediaTrack::Type::Video )
180
                {
181 182 183
                    task.media->addVideoTrack( fcc, track.width(), track.height(),
                                          static_cast<float>( track.fpsNum() ) / static_cast<float>( track.fpsDen() ),
                                          track.language(), track.description() );
184 185
                    isAudio = false;
                }
186
                else if ( track.type() == VLC::MediaTrack::Type::Audio )
187
                {
188 189
                    task.media->addAudioTrack( fcc, track.bitrate(), track.rate(), track.channels(),
                                          track.language(), track.description() );
190
                }
191
            }
192 193 194
            task.media->setDuration( task.vlcMedia.duration() );
            t->commit();
        }, std::move( tracks ) );
195 196 197
    }
    if ( isAudio == true )
    {
198
        if ( parseAudioFile( task ) == false )
199 200 201 202
            return parser::Task::Status::Fatal;
    }
    else
    {
203
        if (parseVideoFile( task ) == false )
204 205
            return parser::Task::Status::Fatal;
    }
206

207 208 209
    if ( task.file->isDeleted() == true || task.media->isDeleted() == true )
        return parser::Task::Status::Fatal;

210
    task.markStepCompleted( parser::Task::ParserStep::MetadataAnalysis );
211
    if ( task.saveParserStep() == false )
212
        return parser::Task::Status::Fatal;
213
    m_notifier->notifyMediaCreation( task.media );
214 215 216
    return parser::Task::Status::Success;
}

217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 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
/* 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;

296 297 298 299 300 301 302 303
    // The minimal entrypoint must be a device mountpoint
    auto device = fsFactory->createDeviceFromMrl( mrl );
    if ( device == nullptr )
    {
        LOG_ERROR( "Can't add a local folder with unknown storage device. ");
        return;
    }
    auto entryPoint = device->mountpoint();
304 305 306 307 308
    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 } );
309
        FsDiscoverer discoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
310 311 312 313 314 315
        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 } );
316
    FsDiscoverer discoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
317 318 319
    if ( parentKnown == false )
    {
        discoverer.discover( entryPoint );
320 321 322
        auto entryFolder = Folder::fromMrl( m_ml, entryPoint );
        if ( entryFolder != nullptr )
            Folder::excludeEntryFolder( m_ml, entryFolder->id() );
323 324 325 326 327
        return;
    }
    discoverer.reload( directoryMrl );
}

328 329
/* Video files */

330
bool MetadataParser::parseVideoFile( parser::Task& task ) const
331
{
332
    auto media = task.media.get();
333
    media->setType( IMedia::Type::Video );
334 335
    const auto& title = task.vlcMedia.meta( libvlc_meta_Title );
    if ( title.length() == 0 )
336
        return true;
337

338
    const auto& showName = task.vlcMedia.meta( libvlc_meta_ShowName );
339

340 341 342 343 344 345
    return sqlite::Tools::withRetries( 3, [this, &showName, &title, &task]() {
        auto t = m_ml->getConn()->newTransaction();
        task.media->setTitleBuffered( title );

        if ( showName.length() != 0 )
        {
346
            auto show = m_ml->show( showName );
347
            if ( show == nullptr )
348 349 350 351 352 353 354 355 356 357 358
            {
                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 );
            }
359 360 361 362 363 364 365 366 367
        }
        else
        {
            // How do we know if it's a movie or a random video?
        }
        task.media->save();
        t->commit();
        return true;
    });
368 369 370 371 372
    return true;
}

/* Audio files */

373
bool MetadataParser::parseAudioFile( parser::Task& task )
374
{
375
    task.media->setType( IMedia::Type::Audio );
376

377
    auto artworkMrl = task.vlcMedia.meta( libvlc_meta_ArtworkURL );
378
    if ( artworkMrl.empty() == false )
379
    {
380
        task.media->setThumbnail( artworkMrl, Thumbnail::Origin::Media );
381 382 383 384 385
        // Don't use an attachment as default artwork for album/artists
        if ( utils::file::schemeIs( "attachment", artworkMrl ) )
            artworkMrl.clear();
    }

386

387
    auto genre = handleGenre( task );
388
    auto artists = findOrCreateArtist( task );
389 390
    if ( artists.first == nullptr && artists.second == nullptr )
        return false;
391
    auto album = findAlbum( task, artists.first, artists.second );
392 393 394
    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();
395
        if ( album == nullptr )
396 397
        {
            const auto& albumName = task.vlcMedia.meta( libvlc_meta_Album );
398 399 400 401 402 403 404 405 406
            int64_t thumbnailId = 0;
            if ( artworkMrl.empty() == false )
            {
                auto thumbnail = Thumbnail::create( m_ml, artworkMrl,
                                                    Thumbnail::Origin::Album );
                if ( thumbnail != nullptr )
                    thumbnailId = thumbnail->id();
            }
            album = m_ml->createAlbum( albumName, thumbnailId );
407 408 409 410 411 412 413 414 415 416 417 418 419
            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 ) );
420 421
}

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

437
/* Album handling */
438 439

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

452 453 454 455 456 457
    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;

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

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

467
    const auto discTotal = toInt( task.vlcMedia, libvlc_meta_DiscTotal, "disc total" );
468
    const auto discNumber = toInt( task.vlcMedia, libvlc_meta_DiscNumber, "disc number" );
469 470 471 472 473
    /*
     * 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
474
     * nullptr, so the link() method can create a new one.
475 476 477 478
     */
    for ( auto it = begin( albums ); it != end( albums ); )
    {
        auto a = (*it).get();
479
        auto candidateAlbumArtist = a->albumArtist();
480 481 482 483
        // 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 );
484 485 486 487
        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.
488
            if ( candidateAlbumArtist->id() != albumArtist->id() )
489 490 491 492 493 494 495 496 497 498 499
            {
                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.
500
        if ( discTotal > 1 || discNumber > 1 )
501 502 503 504
        {
            ++it;
            continue;
        }
505
        const auto tracks = a->cachedTracks();
506 507 508 509 510 511 512
        // 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;
        }
513 514

        auto multiDisc = false;
515 516
        auto multipleArtists = false;
        int64_t previousArtistId = trackArtist != nullptr ? trackArtist->id() : 0;
517 518 519 520
        for ( auto& t : tracks )
        {
            auto at = t->albumTrack();
            assert( at != nullptr );
521 522 523
            if ( at == nullptr )
                continue;
            if ( at->discNumber() > 1 )
524
                multiDisc = true;
525 526 527 528 529
            if ( previousArtistId != 0 && previousArtistId != at->artist()->id() )
                multipleArtists = true;
            previousArtistId = at->artist()->id();
            // We now know enough about the album, we can stop looking at its tracks
            if ( multiDisc == true && multipleArtists == true )
530 531 532 533 534 535 536 537
                break;
        }
        if ( multiDisc )
        {
            ++it;
            continue;
        }

538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558
        // Assume album files will be in the same folder.
        auto newFileFolder = utils::file::directory( task.file->mrl() );
        auto trackFiles = tracks[0]->files();
        bool differentFolder = false;
        for ( auto& f : trackFiles )
        {
            auto candidateFolder = utils::file::directory( f->mrl() );
            if ( candidateFolder != newFileFolder )
            {
                differentFolder = true;
                break;
            }
        }
        // We now have a candidate by the same artist in the same folder, assume it to be
        // a positive match.
        if ( differentFolder == false )
        {
            ++it;
            continue;
        }

559 560 561 562
        // 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.
563
        if ( multipleArtists == false )
564
        {
565 566
            auto candidateDate = task.vlcMedia.meta( libvlc_meta_Date );
            if ( candidateDate.empty() == false )
567
            {
568 569 570 571 572 573 574 575 576 577 578 579 580
                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
                }
581 582
            }
        }
583 584 585 586 587 588 589 590 591
        // The candidate is :
        // - in a different folder
        // - not a multidisc album
        // - Either:
        //      - from the same artist & without a date to discriminate
        //      - from the same artist & with a different date
        //      - from different artists
        // Assume it's a negative match.
        it = albums.erase( it );
592 593 594 595 596
    }
    if ( albums.size() == 0 )
        return nullptr;
    if ( albums.size() > 1 )
    {
597
        LOG_WARN( "Multiple candidates for album ", albumName, ". Selecting first one out of luck" );
598
    }
599 600 601
    m_previousFolderId = task.file->folderId();
    m_previousAlbum = albums[0];
    return albums[0];
602 603 604 605
}

///
/// \brief MetadataParser::handleArtists Returns Artist's involved on a track
606
/// \param task The current parser task
607 608 609 610
/// \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
///
611
std::pair<std::shared_ptr<Artist>, std::shared_ptr<Artist>> MetadataParser::findOrCreateArtist( parser::Task& task ) const
612 613 614
{
    std::shared_ptr<Artist> albumArtist;
    std::shared_ptr<Artist> artist;
615
    static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name + " WHERE name = ?";
616

617 618 619
    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 )
620
    {
621
        return {m_unknownArtist, m_unknownArtist};
622 623
    }

624
    if ( albumArtistStr.empty() == false )
625
    {
626
        albumArtist = Artist::fetch( m_ml, req, albumArtistStr );
627 628
        if ( albumArtist == nullptr )
        {
629
            albumArtist = m_ml->createArtist( albumArtistStr );
630 631
            if ( albumArtist == nullptr )
            {
632
                LOG_ERROR( "Failed to create new artist ", albumArtistStr );
633 634
                return {nullptr, nullptr};
            }
635
            m_notifier->notifyArtistCreation( albumArtist );
636 637
        }
    }
638
    if ( artistStr.empty() == false && artistStr != albumArtistStr )
639
    {
640
        artist = Artist::fetch( m_ml, req, artistStr );
641 642
        if ( artist == nullptr )
        {
643
            artist = m_ml->createArtist( artistStr );
644 645
            if ( artist == nullptr )
            {
646
                LOG_ERROR( "Failed to create new artist ", artistStr );
647 648
                return {nullptr, nullptr};
            }
649
            m_notifier->notifyArtistCreation( artist );
650 651 652 653 654 655 656
        }
    }
    return {albumArtist, artist};
}

/* Tracks handling */

657 658
std::shared_ptr<AlbumTrack> MetadataParser::handleTrack( std::shared_ptr<Album> album, parser::Task& task,
                                                         std::shared_ptr<Artist> artist, Genre* genre ) const
659
{
660 661
    assert( sqlite::Transaction::transactionInProgress() == true );

662 663 664
    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" );
665 666 667
    if ( title.empty() == true )
    {
        LOG_WARN( "Failed to get track title" );
668
        if ( trackNumber != 0 )
669 670
        {
            title = "Track #";
671
            title += std::to_string( trackNumber );
672 673 674
        }
    }
    if ( title.empty() == false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
675
        task.media->setTitleBuffered( title );
676

677 678
    auto track = std::static_pointer_cast<AlbumTrack>( album->addTrack( task.media, trackNumber,
                                                                        discNumber, artist->id(),
679
                                                                        genre ) );
680 681 682 683 684
    if ( track == nullptr )
    {
        LOG_ERROR( "Failed to create album track" );
        return nullptr;
    }
685

686 687
    const auto& releaseDate = task.vlcMedia.meta( libvlc_meta_Date );
    if ( releaseDate.empty() == false )
688
    {
689
        auto releaseYear = atoi( releaseDate.c_str() );
690
        task.media->setReleaseDate( releaseYear );
691 692 693 694 695
        // 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 );
    }
696
    m_notifier->notifyAlbumTrackCreation( track );
697 698 699 700 701 702
    return track;
}

/* Misc */

bool MetadataParser::link( Media& media, std::shared_ptr<Album> album,
703
                               std::shared_ptr<Artist> albumArtist, std::shared_ptr<Artist> artist )
704
{
705
    if ( albumArtist == nullptr )
706 707
    {
        assert( artist != nullptr );
708
        albumArtist = artist;
709
    }
710 711 712 713 714

    // 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.
715 716 717 718 719
    // 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 &&
720
         album != nullptr && album->artworkMrl().empty() == false )
721
        albumArtist->setArtworkMrl( album->artworkMrl(), Thumbnail::Origin::Album );
722

723 724 725 726 727 728
    // Until we have a better artwork extraction/assignation, simply do the same
    // for artists
    if ( artist != nullptr && artist->id() != UnknownArtistID &&
         artist->id() != VariousArtistID &&
         artist->artworkMrl().empty() == true &&
         album != nullptr && album->artworkMrl().empty() == false )
729
        artist->setArtworkMrl( album->artworkMrl(), Thumbnail::Origin::Album );
730

731 732 733 734 735 736 737 738 739 740 741 742 743
    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)
744
        album->setAlbumArtist( albumArtist );
745 746
        // Always add the album artist as an artist
        album->addArtist( albumArtist );
747 748 749
        // Always update the album artist number of tracks.
        // The artist might be different, and will be handled a few lines below
        albumArtist->updateNbTrack( 1 );
750
        if ( artist != nullptr )
751 752 753 754 755
        {
            // If the album artist is not the artist, we need to update the
            // album artist track count as well.
            if ( albumArtist->id() != artist->id() )
                artist->updateNbTrack( 1 );
756
            album->addArtist( artist );
757
        }
758 759 760
    }
    else
    {
761
        // We have more than a single artist on this album, fallback to various artists
762 763
        if ( albumArtist->id() != currentAlbumArtist->id() )
        {
764 765
            if ( m_variousArtists == nullptr )
                m_variousArtists = Artist::fetch( m_ml, VariousArtistID );
766 767 768 769 770 771 772 773 774 775 776 777 778 779
            // If we already switched to various artist, no need to do it again
            if ( m_variousArtists->id() != currentAlbumArtist->id() )
            {
                // All tracks from this album must now also be reflected in various
                // artist number of tracks
                m_variousArtists->updateNbTrack( album->nbTracks() );
                album->setAlbumArtist( m_variousArtists );
            }
            // However we always need to bump the various artist number of tracks
            else
            {
                m_variousArtists->updateNbTrack( 1 );
            }
            // Add this artist as "featuring".
780 781 782 783
            album->addArtist( albumArtist );
        }
        if ( artist != nullptr && artist->id() != albumArtist->id() )
        {
784 785
           album->addArtist( artist );
           artist->updateNbTrack( 1 );
786
        }
787
        albumArtist->updateNbTrack( 1 );
788 789 790 791 792 793 794 795 796
    }

    return true;
}

const char* MetadataParser::name() const
{
    return "Metadata";
}
797 798 799 800 801 802 803 804 805 806

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;
}
807

808 809 810 811 812 813 814 815
void MetadataParser::flush()
{
    ParserService::flush();
    m_variousArtists = nullptr;
    m_previousAlbum = nullptr;
    m_previousFolderId = 0;
}

816 817 818 819 820 821
void MetadataParser::restart()
{
    // Reset locally cached entities
    initialize();
}

822
bool MetadataParser::isCompleted( const parser::Task& task ) const
823
{
824
    // We always need to run this task if the metadata extraction isn't completed
825
    return task.isStepCompleted( parser::Task::ParserStep::MetadataAnalysis );
826 827
}

828
}