MetadataParser.cpp 32.1 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
MetadataParser::MetadataParser()
52 53
    : m_ml( nullptr )
    , m_previousFolderId( 0 )
54 55 56
{
}

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

65 66 67 68
bool MetadataParser::initialize( MediaLibrary* ml)
{
    m_ml = ml;
    m_notifier = ml->getNotifier();
69
    return cacheUnknownArtist();
70 71
}

72
int MetadataParser::toInt( parser::Task& task, parser::Task::Item::Metadata meta )
73
{
74
    auto str = task.item().meta( meta );
75 76 77 78 79 80 81 82
    if ( str.empty() == false )
    {
        try
        {
            return std::stoi( str );
        }
        catch( std::logic_error& ex)
        {
83
            LOG_WARN( "Invalid meta #",
84
                      static_cast<typename std::underlying_type<parser::Task::Item::Metadata>::type>( meta ),
85
                      " provided (", str, "): ", ex.what() );
86 87 88 89 90
        }
    }
    return 0;
}

91 92
parser::Task::Status MetadataParser::run( parser::Task& task )
{
93 94 95 96 97 98 99 100 101 102 103
    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 );
104
        task.saveParserStep();
105 106 107
        return parser::Task::Status::Success;
    }

108
    if ( task.file == nullptr )
109
    {
110
        assert( task.media == nullptr );
111
        // Try to create Media & File
112
        auto mrl = task.item().mrl();
113
        try
114
        {
115
            auto t = m_ml->getConn()->newTransaction();
116 117
            LOG_INFO( "Adding ", mrl );
            task.media = Media::create( m_ml, IMedia::Type::Unknown, utils::file::fileName( mrl ) );
118 119
            if ( task.media == nullptr )
            {
120
                LOG_ERROR( "Failed to add media ", mrl, " to the media library" );
121 122 123 124 125 126 127 128
                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 )
            {
129
                LOG_ERROR( "Failed to add file ", mrl, " to media #", task.media->id() );
130 131
                return parser::Task::Status::Fatal;
            }
132
            task.updateFileId();
133
            t->commit();
134
        }
135 136
        // Voluntarily trigger an exception for a valid, but less common case, to avoid database overhead
        catch ( sqlite::errors::ConstraintViolation& ex )
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
141
            auto fileInDB = File::fromMrl( m_ml, mrl );
142 143
            if ( fileInDB == nullptr ) // The file is no longer present in DB, gracefully delete task
            {
144
                LOG_ERROR( "File ", mrl, " no longer present in DB, aborting");
145 146 147 148 149 150 151
                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;
152
        }
153
    }
154 155 156 157 158 159 160 161 162
    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;
    }
163 164 165 166 167 168

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

    if ( alreadyInParser == true )
    {
169 170
        // Let the worker drop this duplicate task
        task.markStepCompleted( parser::Task::ParserStep::Completed );
171 172
        // And remove it from DB
        task.destroy( m_ml, task.id() );
173
        return parser::Task::Status::Success;
174 175
    }

176
    const auto& tracks = task.vlcMedia.tracks();
177 178

    if ( tracks.empty() == true )
179
        return parser::Task::Status::Fatal;
180

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

218 219 220
    if ( task.file->isDeleted() == true || task.media->isDeleted() == true )
        return parser::Task::Status::Fatal;

221
    task.markStepCompleted( parser::Task::ParserStep::MetadataAnalysis );
222
    if ( task.saveParserStep() == false )
223
        return parser::Task::Status::Fatal;
224
    m_notifier->notifyMediaCreation( task.media );
225 226 227
    return parser::Task::Status::Success;
}

228 229 230 231 232
/* Playlist files */

bool MetadataParser::addPlaylistMedias( parser::Task& task, int nbSubitem ) const
{
    auto t = m_ml->getConn()->newTransaction();
233 234
    const auto& mrl = task.item().mrl();
    LOG_INFO( "Try to import ", mrl, " as a playlist" );
235
    auto playlistName = task.item().meta( parser::Task::Item::Metadata::Title );
236
    if ( playlistName.empty() == true )
237
        playlistName = utils::url::decode( utils::file::fileName( mrl ) );
238 239 240
    auto playlistPtr = Playlist::create( m_ml, playlistName );
    if ( playlistPtr == nullptr )
    {
241
        LOG_ERROR( "Failed to create playlist ", mrl, " to the media library" );
242 243 244 245 246
        return false;
    }
    task.file = playlistPtr->addFile( *task.fileFs, task.parentFolder->id(), task.parentFolderFs->device()->isRemovable() );
    if ( task.file == nullptr )
    {
247
        LOG_ERROR( "Failed to add playlist file ", mrl );
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
        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();
264
    LOG_INFO( "Try to add ", mrl, " to the playlist ", mrl );
265 266 267
    auto media = m_ml->media( mrl );
    if ( media != nullptr )
    {
268
        LOG_INFO( "Media for ", mrl, " already exists, adding it to the playlist ", mrl );
269 270 271 272 273 274 275 276 277 278 279 280 281
        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 )
        {
282
            LOG_ERROR( "Failed to create external media for ", subitem->mrl(), " in the playlist ", mrl );
283 284 285 286 287
            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 )
288
            LOG_ERROR( "Failed to create external file for ", subitem->mrl(), " in the playlist ", mrl );
289 290 291 292 293 294 295 296 297 298 299 300 301 302
        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;
    }
303
    LOG_INFO( "Importing ", isDirectory ? "folder " : "file ", subitem->mrl(), " in the playlist ", mrl );
304 305 306 307
    auto directoryMrl = utils::file::directory( mrl );
    auto parentFolder = Folder::fromMrl( m_ml, directoryMrl );
    bool parentKnown = parentFolder != nullptr;

308 309 310 311 312 313 314 315
    // 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();
316 317 318 319 320
    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 } );
321
        FsDiscoverer discoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
322 323 324 325 326 327
        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 } );
328
    FsDiscoverer discoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
329 330 331
    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
    const auto& title = task.item().meta( parser::Task::Item::Metadata::Title );
347
    if ( title.length() == 0 )
348
        return true;
349

350 351
    const auto& showName = task.item().meta( parser::Task::Item::Metadata::ShowName );
    const auto& artworkMrl = task.item().meta( parser::Task::Item::Metadata::ArtworkUrl );
352

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

357 358 359
        if ( artworkMrl.empty() == false )
            task.media->setThumbnail( artworkMrl, Thumbnail::Origin::Media );

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

/* Audio files */

389
bool MetadataParser::parseAudioFile( parser::Task& task )
390
{
391
    task.media->setType( IMedia::Type::Audio );
392

393
    auto artworkMrl = task.item().meta( parser::Task::Item::Metadata::ArtworkUrl );
394
    if ( artworkMrl.empty() == false )
395
    {
396
        task.media->setThumbnail( artworkMrl, Thumbnail::Origin::Media );
397 398 399 400 401
        // Don't use an attachment as default artwork for album/artists
        if ( utils::file::schemeIs( "attachment", artworkMrl ) )
            artworkMrl.clear();
    }

402

403
    auto genre = handleGenre( task );
404
    auto artists = findOrCreateArtist( task );
405 406
    if ( artists.first == nullptr && artists.second == nullptr )
        return false;
407
    auto album = findAlbum( task, artists.first, artists.second );
408 409 410
    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();
411
        if ( album == nullptr )
412
        {
413
            const auto& albumName = task.item().meta( parser::Task::Item::Metadata::Album );
414 415 416 417 418 419 420 421 422
            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 );
423 424 425 426 427 428 429 430 431 432 433 434 435
            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 ) );
436 437
}

438 439
std::shared_ptr<Genre> MetadataParser::handleGenre( parser::Task& task ) const
{
440
    const auto& genreStr = task.item().meta( parser::Task::Item::Metadata::Genre );
441
    if ( genreStr.length() == 0 )
442
        return nullptr;
443
    auto genre = Genre::fromName( m_ml, genreStr );
444 445
    if ( genre == nullptr )
    {
446
        genre = Genre::create( m_ml, genreStr );
447
        if ( genre == nullptr )
448
            LOG_ERROR( "Failed to get/create Genre", genreStr );
449 450 451 452
    }
    return genre;
}

453
/* Album handling */
454 455

std::shared_ptr<Album> MetadataParser::findAlbum( parser::Task& task, std::shared_ptr<Artist> albumArtist,
456
                                                    std::shared_ptr<Artist> trackArtist )
457
{
458
    const auto& albumName = task.item().meta( parser::Task::Item::Metadata::Album );
459
    if ( albumName.empty() == true )
460
    {
461 462
        if ( albumArtist != nullptr )
            return albumArtist->unknownAlbum();
463
        if ( trackArtist != nullptr )
464 465
            return trackArtist->unknownAlbum();
        return m_unknownArtist->unknownAlbum();
466 467
    }

468 469 470 471 472 473
    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;

474 475
    // Album matching depends on the difference between artist & album artist.
    // Specificaly pass the albumArtist here.
476 477
    static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
            " WHERE title = ?";
478
    auto albums = Album::fetchAll<Album>( m_ml, req, albumName );
479 480 481 482

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

483 484
    const auto discTotal = toInt( task, parser::Task::Item::Metadata::DiscTotal );
    const auto discNumber = toInt( task, parser::Task::Item::Metadata::DiscNumber );
485 486 487 488 489
    /*
     * 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
490
     * nullptr, so the link() method can create a new one.
491 492 493 494
     */
    for ( auto it = begin( albums ); it != end( albums ); )
    {
        auto a = (*it).get();
495
        auto candidateAlbumArtist = a->albumArtist();
496 497 498 499
        // 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 );
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.
504
            if ( candidateAlbumArtist->id() != albumArtist->id() )
505 506 507 508 509 510 511 512 513 514 515
            {
                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.
516
        if ( discTotal > 1 || discNumber > 1 )
517 518 519 520
        {
            ++it;
            continue;
        }
521
        const auto tracks = a->cachedTracks();
522 523 524 525 526 527 528
        // 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;
        }
529 530

        auto multiDisc = false;
531 532
        auto multipleArtists = false;
        int64_t previousArtistId = trackArtist != nullptr ? trackArtist->id() : 0;
533 534 535 536
        for ( auto& t : tracks )
        {
            auto at = t->albumTrack();
            assert( at != nullptr );
537 538 539
            if ( at == nullptr )
                continue;
            if ( at->discNumber() > 1 )
540
                multiDisc = true;
541 542 543 544 545
            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 )
546 547 548 549 550 551 552 553
                break;
        }
        if ( multiDisc )
        {
            ++it;
            continue;
        }

554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
        // 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;
        }

575 576 577 578
        // 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.
579
        if ( multipleArtists == false )
580
        {
581
            auto candidateDate = task.item().meta( parser::Task::Item::Metadata::Date );
582
            if ( candidateDate.empty() == false )
583
            {
584 585 586 587 588 589 590 591 592 593 594 595 596
                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
                }
597 598
            }
        }
599 600 601 602 603 604 605 606 607
        // 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 );
608 609 610 611 612
    }
    if ( albums.size() == 0 )
        return nullptr;
    if ( albums.size() > 1 )
    {
613
        LOG_WARN( "Multiple candidates for album ", albumName, ". Selecting first one out of luck" );
614
    }
615 616 617
    m_previousFolderId = task.file->folderId();
    m_previousAlbum = albums[0];
    return albums[0];
618 619 620 621
}

///
/// \brief MetadataParser::handleArtists Returns Artist's involved on a track
622
/// \param task The current parser task
623 624 625 626
/// \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
///
627
std::pair<std::shared_ptr<Artist>, std::shared_ptr<Artist>> MetadataParser::findOrCreateArtist( parser::Task& task ) const
628 629 630
{
    std::shared_ptr<Artist> albumArtist;
    std::shared_ptr<Artist> artist;
631
    static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name + " WHERE name = ?";
632

633 634
    const auto& albumArtistStr = task.item().meta( parser::Task::Item::Metadata::AlbumArtist );
    const auto& artistStr = task.item().meta( parser::Task::Item::Metadata::Artist );
635
    if ( albumArtistStr.empty() == true && artistStr.empty() == true )
636
    {
637
        return {m_unknownArtist, m_unknownArtist};
638 639
    }

640
    if ( albumArtistStr.empty() == false )
641
    {
642
        albumArtist = Artist::fetch( m_ml, req, albumArtistStr );
643 644
        if ( albumArtist == nullptr )
        {
645
            albumArtist = m_ml->createArtist( albumArtistStr );
646 647
            if ( albumArtist == nullptr )
            {
648
                LOG_ERROR( "Failed to create new artist ", albumArtistStr );
649 650
                return {nullptr, nullptr};
            }
651
            m_notifier->notifyArtistCreation( albumArtist );
652 653
        }
    }
654
    if ( artistStr.empty() == false && artistStr != albumArtistStr )
655
    {
656
        artist = Artist::fetch( m_ml, req, artistStr );
657 658
        if ( artist == nullptr )
        {
659
            artist = m_ml->createArtist( artistStr );
660 661
            if ( artist == nullptr )
            {
662
                LOG_ERROR( "Failed to create new artist ", artistStr );
663 664
                return {nullptr, nullptr};
            }
665
            m_notifier->notifyArtistCreation( artist );
666 667 668 669 670 671 672
        }
    }
    return {albumArtist, artist};
}

/* Tracks handling */

673 674
std::shared_ptr<AlbumTrack> MetadataParser::handleTrack( std::shared_ptr<Album> album, parser::Task& task,
                                                         std::shared_ptr<Artist> artist, Genre* genre ) const
675
{
676 677
    assert( sqlite::Transaction::transactionInProgress() == true );

678 679 680
    auto title = task.item().meta( parser::Task::Item::Metadata::Title );
    const auto trackNumber = toInt( task, parser::Task::Item::Metadata::TrackNumber );
    const auto discNumber = toInt( task, parser::Task::Item::Metadata::DiscNumber );
681 682 683
    if ( title.empty() == true )
    {
        LOG_WARN( "Failed to get track title" );
684
        if ( trackNumber != 0 )
685 686
        {
            title = "Track #";
687
            title += std::to_string( trackNumber );
688 689 690
        }
    }
    if ( title.empty() == false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
691
        task.media->setTitleBuffered( title );
692

693 694
    auto track = std::static_pointer_cast<AlbumTrack>( album->addTrack( task.media, trackNumber,
                                                                        discNumber, artist->id(),
695
                                                                        genre ) );
696 697 698 699 700
    if ( track == nullptr )
    {
        LOG_ERROR( "Failed to create album track" );
        return nullptr;
    }
701

702
    const auto& releaseDate = task.item().meta( parser::Task::Item::Metadata::Date );
703
    if ( releaseDate.empty() == false )
704
    {
705
        auto releaseYear = atoi( releaseDate.c_str() );
706
        task.media->setReleaseDate( releaseYear );
707 708 709 710 711
        // 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 );
    }
712
    m_notifier->notifyAlbumTrackCreation( track );
713 714 715 716 717 718
    return track;
}

/* Misc */

bool MetadataParser::link( Media& media, std::shared_ptr<Album> album,
719
                               std::shared_ptr<Artist> albumArtist, std::shared_ptr<Artist> artist )
720
{
721
    if ( albumArtist == nullptr )
722 723
    {
        assert( artist != nullptr );
724
        albumArtist = artist;
725
    }
726
    assert( album != nullptr );
727

728 729
    auto albumThumbnail = album->thumbnail();

730 731 732 733
    // 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.
734 735 736 737
    // 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 &&
738 739 740 741 742 743 744 745 746 747 748 749 750 751
         albumThumbnail != nullptr )
    {
        auto albumArtistThumbnail = albumArtist->thumbnail();
        // If the album artist has no thumbnail, let's assign it
        if ( albumArtistThumbnail == nullptr )
        {
            albumArtist->setArtworkMrl( albumThumbnail->mrl(), Thumbnail::Origin::AlbumArtist );
        }
        else if ( albumArtistThumbnail->origin() == Thumbnail::Origin::Artist )
        {
            // We only want to change the thumbnail if it was assigned from an
            // album this artist was only featuring on
        }
    }
752

753 754 755 756
    // Until we have a better artwork extraction/assignation, simply do the same
    // for artists
    if ( artist != nullptr && artist->id() != UnknownArtistID &&
         artist->id() != VariousArtistID &&
757 758 759 760
         albumThumbnail != nullptr && artist->thumbnail() == nullptr )
    {
        artist->setArtworkMrl( album->artworkMrl(), Thumbnail::Origin::Artist );
    }
761

762 763 764 765 766 767 768 769 770 771 772 773 774
    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)
775
        album->setAlbumArtist( albumArtist );
776 777
        // Always add the album artist as an artist
        album->addArtist( albumArtist );
778 779 780
        // Always update the album artist number of tracks.
        // The artist might be different, and will be handled a few lines below
        albumArtist->updateNbTrack( 1 );
781
        if ( artist != nullptr )
782 783 784 785 786
        {
            // 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 );
787
            album->addArtist( artist );
788
        }
789 790 791
    }
    else
    {
792
        // We have more than a single artist on this album, fallback to various artists
793 794
        if ( albumArtist->id() != currentAlbumArtist->id() )
        {
795 796
            if ( m_variousArtists == nullptr )
                m_variousArtists = Artist::fetch( m_ml, VariousArtistID );
797 798 799 800 801 802 803 804 805 806 807 808 809 810
            // 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".
811 812 813 814
            album->addArtist( albumArtist );
        }
        if ( artist != nullptr && artist->id() != albumArtist->id() )
        {
815 816
           album->addArtist( artist );
           artist->updateNbTrack( 1 );
817
        }
818
        albumArtist->updateNbTrack( 1 );
819 820 821 822 823 824 825 826 827
    }

    return true;
}

const char* MetadataParser::name() const
{
    return "Metadata";
}
828 829 830 831 832 833 834 835 836 837

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

839
void MetadataParser::onFlushing()
840 841 842 843 844 845
{
    m_variousArtists = nullptr;
    m_previousAlbum = nullptr;
    m_previousFolderId = 0;
}

846
void MetadataParser::onRestarted()
847 848
{
    // Reset locally cached entities
849
    cacheUnknownArtist();
850 851
}

852
bool MetadataParser::isCompleted( const parser::Task& task ) const
853
{
854
    // We always need to run this task if the metadata extraction isn't completed
855
    return task.isStepCompleted( parser::Task::ParserStep::MetadataAnalysis );
856 857
}

858
}