MetadataParser.cpp 32.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/*****************************************************************************
 * Media Library
 *****************************************************************************
 * Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
 *
 * Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

23 24 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
#include "medialibrary/filesystem/IDevice.h"
#include "medialibrary/filesystem/IDirectory.h"
34
#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
namespace medialibrary
{
50 51
namespace parser
{
52

53
MetadataAnalyzer::MetadataAnalyzer()
54 55
    : m_ml( nullptr )
    , m_previousFolderId( 0 )
56 57 58
{
}

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

67
bool MetadataAnalyzer::initialize( IMediaLibrary* ml )
68
{
69 70
    m_ml = static_cast<MediaLibrary*>( ml );
    m_notifier = m_ml->getNotifier();
71
    return cacheUnknownArtist();
72 73
}

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

93
Status MetadataAnalyzer::run( IItem& item )
94
{
95
    bool alreadyInParser = false;
96
    int nbSubitem = item.nbSubItems();
97 98 99
    // Assume that file containing subitem(s) is a Playlist
    if ( nbSubitem > 0 )
    {
100
        auto res = addPlaylistMedias( item );
101
        if ( res == false ) // playlist addition may fail due to constraint violation
102
            return Status::Fatal;
103

104
        assert( item.file() != nullptr );
105
        return Status::Completed;
106 107
    }

108
    if ( item.file() == nullptr )
109
    {
110
        assert( item.media() == nullptr );
111
        // Try to create Media & File
112
        auto mrl = item.mrl();
113
        try
114
        {
115
            auto t = m_ml->getConn()->newTransaction();
116
            LOG_INFO( "Adding ", mrl );
117 118
            auto m = Media::create( m_ml, IMedia::Type::Unknown, utils::file::fileName( mrl ) );
            if ( m == nullptr )
119
            {
120
                LOG_ERROR( "Failed to add media ", mrl, " to the media library" );
121
                return Status::Fatal;
122 123
            }
            // For now, assume all media are made of a single file
124 125 126
            auto file = m->addFile( *item.fileFs(),
                                    item.parentFolder()->id(),
                                    item.parentFolderFs()->device()->isRemovable(),
127 128
                                    File::Type::Main );
            if ( file == nullptr )
129
            {
130
                LOG_ERROR( "Failed to add file ", mrl, " to media #", m->id() );
131
                return Status::Fatal;
132
            }
133
            item.setMedia( std::move( m ) );
134 135
            // Will invoke ITaskCb::updateFileId to upadte m_fileId & its
            // representation in DB
136
            item.setFile( std::move( file ) );
137
            t->commit();
138
        }
139 140
        // Voluntarily trigger an exception for a valid, but less common case, to avoid database overhead
        catch ( sqlite::errors::ConstraintViolation& ex )
141
        {
142 143 144
            LOG_INFO( "Creation of Media & File failed because ", ex.what(),
                      ". Assuming this task is a duplicate" );
            // Try to retrieve file & Media from database
145
            auto fileInDB = File::fromMrl( m_ml, mrl );
146 147
            if ( fileInDB == nullptr ) // The file is no longer present in DB, gracefully delete task
            {
148
                LOG_ERROR( "File ", mrl, " no longer present in DB, aborting");
149
                return Status::Fatal;
150
            }
151 152
            auto media = fileInDB->media();
            if ( media == nullptr ) // Without a media, we cannot go further
153
                return Status::Fatal;
154 155
            item.setFile( std::move( fileInDB ) );
            item.setMedia( std::move( media ) );
156

157
            alreadyInParser = true;
158
        }
159
    }
160
    else if ( item.media() == nullptr )
161 162 163 164 165 166
    {
        // 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 );
167
        return Status::Fatal;
168
    }
169
    auto media = std::static_pointer_cast<Media>( item.media() );
170

171
    if ( item.parentPlaylist() != nullptr )
172
        item.parentPlaylist()->add( *media, item.parentPlaylistIndex() );
173 174

    if ( alreadyInParser == true )
175
        return Status::Discarded;
176

177
    const auto& tracks = item.tracks();
178 179

    if ( tracks.empty() == true )
180
        return Status::Fatal;
181

182
    bool isAudio = true;
183
    {
184
        using TracksT = decltype( tracks );
185
        sqlite::Tools::withRetries( 3, [this, &isAudio, &item, &media]( TracksT tracks ) {
186
            auto t = m_ml->getConn()->newTransaction();
187
            for ( const auto& track : tracks )
188
            {
189
                if ( track.type == IItem::Track::Type::Video )
190
                {
191
                    media->addVideoTrack( track.codec, track.v.width, track.v.height,
192
                                          track.v.fpsNum, track.v.fpsDen, track.bitrate,
193
                                          track.v.sarNum, track.v.sarDen,
194
                                          track.language, track.description );
195 196
                    isAudio = false;
                }
197
                else
198
                {
199
                    assert( track.type == IItem::Track::Type::Audio );
200
                    media->addAudioTrack( track.codec, track.bitrate,
201 202
                                               track.a.rate, track.a.nbChannels,
                                               track.language, track.description );
203
                }
204
            }
205
            media->setDuration( item.duration() );
206 207
            t->commit();
        }, std::move( tracks ) );
208 209 210
    }
    if ( isAudio == true )
    {
211
        if ( parseAudioFile( item ) == false )
212
            return Status::Fatal;
213 214 215
    }
    else
    {
216
        if (parseVideoFile( item ) == false )
217
            return Status::Fatal;
218
    }
219

220 221
    if ( std::static_pointer_cast<File>( item.file() )->isDeleted() == true ||
         std::static_pointer_cast<Media>( media )->isDeleted() == true )
222
        return Status::Fatal;
223

224
    m_notifier->notifyMediaCreation( media );
225
    return Status::Success;
226 227
}

228 229
/* Playlist files */

230
bool MetadataAnalyzer::addPlaylistMedias( IItem& item ) const
231
{
232
    const auto& mrl = item.mrl();
233
    LOG_INFO( "Try to import ", mrl, " as a playlist" );
234
    std::shared_ptr<Playlist> playlistPtr;
235
    if ( item.file() != nullptr )
236
    {
237 238 239 240 241
        // We are most likely re-scanning a file representing a playlist.
        // If a task has a file, it means the playlist & the associated file have
        // been created.
        std::string req = "SELECT * FROM " + policy::PlaylistTable::Name +
                " WHERE file_id = ?";
242
        playlistPtr = Playlist::fetch( m_ml, req, item.file()->id() );
243 244 245 246 247 248 249
        if ( playlistPtr == nullptr )
        {
            // The playlist had to be created, something is very wrong, give up
            // FIXME: Check that the task will be deleted.
            assert( false );
            return false;
        }
250
    }
251
    else
252
    {
253
        auto playlistName = item.meta( IItem::Metadata::Title );
254 255 256 257 258 259 260 261 262 263
        if ( playlistName.empty() == true )
            playlistName = utils::url::decode( utils::file::fileName( mrl ) );
        auto t = m_ml->getConn()->newTransaction();
        playlistPtr = Playlist::create( m_ml, playlistName );
        if ( playlistPtr == nullptr )
        {
            LOG_ERROR( "Failed to create playlist ", mrl, " to the media library" );
            return false;
        }

264 265 266
        auto file = playlistPtr->addFile( *item.fileFs(),
                                          item.parentFolder()->id(),
                                          item.parentFolderFs()->device()->isRemovable() );
267 268 269 270 271
        if ( file == nullptr )
        {
            LOG_ERROR( "Failed to add playlist file ", mrl );
            return false;
        }
272 273
        // Will invoke ITaskCb::updateFileId to upadte m_fileId & its
        // representation in DB
274
        item.setFile( std::move( file ) );
275
        t->commit();
276
    }
277 278 279 280 281
    // Now regardless of if the playlist is re-scanned or discovered from the
    // first time, just schedule all members for insertion. media & files will
    // be recreated if need be, and appropriate entries in PlaylistMediaRelation
    // table will be recreated to link things together.

282 283
    for ( auto i = 0u; i < item.nbSubItems(); ++i ) // FIXME: Interrupt loop if paused
        addPlaylistElement( item, playlistPtr, item.subItem( i ) );
284 285 286 287

    return true;
}

288
void MetadataAnalyzer::addPlaylistElement( IItem& item,
289
                                         std::shared_ptr<Playlist> playlistPtr,
290
                                         const IItem& subitem ) const
291
{
292
    const auto& mrl = subitem.mrl();
293
    LOG_INFO( "Try to add ", mrl, " to the playlist ", mrl );
294 295 296
    auto media = m_ml->media( mrl );
    if ( media != nullptr )
    {
297
        LOG_INFO( "Media for ", mrl, " already exists, adding it to the playlist ", mrl );
298
        playlistPtr->add( *media, subitem.parentPlaylistIndex() );
299 300 301 302 303 304 305 306
        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();
307
        auto externalMedia = Media::create( m_ml, IMedia::Type::External, utils::url::encode(
308
                subitem.meta( IItem::Metadata::Title ) ) );
309 310
        if ( externalMedia == nullptr )
        {
311
            LOG_ERROR( "Failed to create external media for ", mrl, " in the playlist ", item.mrl() );
312 313 314 315 316
            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 )
317
            LOG_ERROR( "Failed to create external file for ", mrl, " in the playlist ", item.mrl() );
318
        playlistPtr->add( *externalMedia, subitem.parentPlaylistIndex() );
319 320 321 322 323 324 325 326 327 328 329 330 331
        t2->commit();
        return;
    }
    bool isDirectory;
    try
    {
        isDirectory = utils::fs::isDirectory( utils::file::toLocalPath( mrl ) );
    }
    catch ( std::system_error& ex )
    {
        LOG_ERROR( ex.what() );
        return;
    }
332
    LOG_INFO( "Importing ", isDirectory ? "folder " : "file ", mrl, " in the playlist ", item.mrl() );
333 334 335 336
    auto directoryMrl = utils::file::directory( mrl );
    auto parentFolder = Folder::fromMrl( m_ml, directoryMrl );
    bool parentKnown = parentFolder != nullptr;

337 338 339 340 341 342 343 344
    // 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();
345 346
    if ( parentKnown == false && Folder::fromMrl( m_ml, entryPoint ) != nullptr )
    {
347 348 349 350
        auto probePtr = std::unique_ptr<prober::PathProbe>(
                    new prober::PathProbe{ utils::file::stripScheme( mrl ),
                       isDirectory, std::move( playlistPtr ), parentFolder,
                       utils::file::stripScheme( directoryMrl ), subitem.parentPlaylistIndex(), true } );
351
        FsDiscoverer discoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
352 353 354
        discoverer.reload( entryPoint );
        return;
    }
355 356 357 358
    auto probePtr = std::unique_ptr<prober::PathProbe>(
                new prober::PathProbe{ utils::file::stripScheme( mrl ),
                   isDirectory, std::move( playlistPtr ), parentFolder,
                   utils::file::stripScheme( directoryMrl ), subitem.parentPlaylistIndex(), false } );
359
    FsDiscoverer discoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
360 361 362
    if ( parentKnown == false )
    {
        discoverer.discover( entryPoint );
363 364 365
        auto entryFolder = Folder::fromMrl( m_ml, entryPoint );
        if ( entryFolder != nullptr )
            Folder::excludeEntryFolder( m_ml, entryFolder->id() );
366 367 368 369 370
        return;
    }
    discoverer.reload( directoryMrl );
}

371 372
/* Video files */

373
bool MetadataAnalyzer::parseVideoFile( IItem& item ) const
374
{
375
    auto media = static_cast<Media*>( item.media().get() );
376
    media->setType( IMedia::Type::Video );
377
    const auto& title = item.meta( IItem::Metadata::Title );
378
    if ( title.length() == 0 )
379
        return true;
380

381 382
    const auto& showName = item.meta( IItem::Metadata::ShowName );
    const auto& artworkMrl = item.meta( IItem::Metadata::ArtworkUrl );
383

384
    return sqlite::Tools::withRetries( 3, [this, &showName, &title, media, &item, &artworkMrl]() {
385
        auto t = m_ml->getConn()->newTransaction();
386
        media->setTitleBuffered( title );
387

388
        if ( artworkMrl.empty() == false )
389
            media->setThumbnail( artworkMrl, Thumbnail::Origin::Media );
390

391 392
        if ( showName.length() != 0 )
        {
393 394 395 396 397 398
            const std::string req = "SELECT * FROM " + policy::ShowTable::Name +
                    " WHERE name = ?";

            auto shows = Show::fetchAll<Show>( m_ml, req, showName );
            std::shared_ptr<Show> show;
            if ( shows.empty() == true )
399 400 401 402 403
            {
                show = m_ml->createShow( showName );
                if ( show == nullptr )
                    return false;
            }
404 405 406 407 408 409
            else
            {
                //FIXME: Discriminate amongst shows
                LOG_WARN( "Defaulting to first matching show" );
                show = shows[0];
            }
410
            auto episode = toInt( item, IItem::Metadata::Episode );
411 412 413
            if ( episode != 0 )
            {
                std::shared_ptr<Show> s = std::static_pointer_cast<Show>( show );
414
                s->addEpisode( *media, episode );
415
            }
416 417 418 419 420
        }
        else
        {
            // How do we know if it's a movie or a random video?
        }
421
        media->save();
422 423 424
        t->commit();
        return true;
    });
425 426 427 428 429
    return true;
}

/* Audio files */

430
bool MetadataAnalyzer::parseAudioFile( IItem& item )
431
{
432 433
    auto media = static_cast<Media*>( item.media().get() );
    media->setType( IMedia::Type::Audio );
434

435
    auto artworkMrl = item.meta( IItem::Metadata::ArtworkUrl );
436
    if ( artworkMrl.empty() == false )
437
    {
438
        media->setThumbnail( artworkMrl, Thumbnail::Origin::Media );
439 440 441 442 443
        // Don't use an attachment as default artwork for album/artists
        if ( utils::file::schemeIs( "attachment", artworkMrl ) )
            artworkMrl.clear();
    }

444

445 446
    auto genre = handleGenre( item );
    auto artists = findOrCreateArtist( item );
447 448
    if ( artists.first == nullptr && artists.second == nullptr )
        return false;
449
    auto album = findAlbum( item, artists.first, artists.second );
450
    return sqlite::Tools::withRetries( 3, [this, &item, &artists, media]( std::string artworkMrl,
451 452
                                                  std::shared_ptr<Album> album, std::shared_ptr<Genre> genre ) {
        auto t = m_ml->getConn()->newTransaction();
453
        if ( album == nullptr )
454
        {
455
            const auto& albumName = item.meta( IItem::Metadata::Album );
456 457 458 459 460 461 462 463 464
            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 );
465 466 467 468 469
            if ( album == nullptr )
                return false;
            m_notifier->notifyAlbumCreation( album );
        }
        // If we know a track artist, specify it, otherwise, fallback to the album/unknown artist
470
        auto track = handleTrack( album, item, artists.second ? artists.second : artists.first,
471 472
                                  genre.get() );

473 474
        auto res = link( *media, album, artists.first, artists.second );
        media->save();
475 476 477
        t->commit();
        return res;
    }, std::move( artworkMrl ), std::move( album ), std::move( genre ) );
478 479
}

480
std::shared_ptr<Genre> MetadataAnalyzer::handleGenre( IItem& item ) const
481
{
482
    const auto& genreStr = item.meta( IItem::Metadata::Genre );
483
    if ( genreStr.length() == 0 )
484
        return nullptr;
485
    auto genre = Genre::fromName( m_ml, genreStr );
486 487
    if ( genre == nullptr )
    {
488
        genre = Genre::create( m_ml, genreStr );
489
        if ( genre == nullptr )
490
            LOG_ERROR( "Failed to get/create Genre", genreStr );
491 492 493 494
    }
    return genre;
}

495
/* Album handling */
496

497
std::shared_ptr<Album> MetadataAnalyzer::findAlbum( IItem& item, std::shared_ptr<Artist> albumArtist,
498
                                                    std::shared_ptr<Artist> trackArtist )
499
{
500
    const auto& albumName = item.meta( IItem::Metadata::Album );
501
    if ( albumName.empty() == true )
502
    {
503 504
        if ( albumArtist != nullptr )
            return albumArtist->unknownAlbum();
505
        if ( trackArtist != nullptr )
506 507
            return trackArtist->unknownAlbum();
        return m_unknownArtist->unknownAlbum();
508 509
    }

510
    auto file = static_cast<File*>( item.file().get() );
511
    if ( m_previousAlbum != nullptr && albumName == m_previousAlbum->title() &&
512
         m_previousFolderId != 0 && file->folderId() == m_previousFolderId )
513 514 515 516
        return m_previousAlbum;
    m_previousAlbum.reset();
    m_previousFolderId = 0;

517 518
    // Album matching depends on the difference between artist & album artist.
    // Specificaly pass the albumArtist here.
519 520
    static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
            " WHERE title = ?";
521
    auto albums = Album::fetchAll<Album>( m_ml, req, albumName );
522 523 524 525

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

526 527
    const auto discTotal = toInt( item, IItem::Metadata::DiscTotal );
    const auto discNumber = toInt( item, IItem::Metadata::DiscNumber );
528 529 530 531 532
    /*
     * 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
533
     * nullptr, so the link() method can create a new one.
534 535 536 537
     */
    for ( auto it = begin( albums ); it != end( albums ); )
    {
        auto a = (*it).get();
538
        auto candidateAlbumArtist = a->albumArtist();
539 540 541 542
        // 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 );
543 544 545 546
        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.
547
            if ( candidateAlbumArtist->id() != albumArtist->id() )
548 549 550 551 552 553 554 555 556 557 558
            {
                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.
559
        if ( discTotal > 1 || discNumber > 1 )
560 561 562 563
        {
            ++it;
            continue;
        }
564
        const auto tracks = a->cachedTracks();
565 566 567 568 569 570 571
        // 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;
        }
572 573

        auto multiDisc = false;
574 575
        auto multipleArtists = false;
        int64_t previousArtistId = trackArtist != nullptr ? trackArtist->id() : 0;
576 577 578 579
        for ( auto& t : tracks )
        {
            auto at = t->albumTrack();
            assert( at != nullptr );
580 581 582
            if ( at == nullptr )
                continue;
            if ( at->discNumber() > 1 )
583
                multiDisc = true;
584 585 586 587 588
            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 )
589 590 591 592 593 594 595 596
                break;
        }
        if ( multiDisc )
        {
            ++it;
            continue;
        }

597
        // Assume album files will be in the same folder.
598
        auto newFileFolder = utils::file::directory( file->mrl() );
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
        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;
        }

618 619 620 621
        // 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.
622
        if ( multipleArtists == false )
623
        {
624
            auto candidateDate = item.meta( IItem::Metadata::Date );
625
            if ( candidateDate.empty() == false )
626
            {
627 628 629 630 631 632 633 634 635 636 637 638 639
                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
                }
640 641
            }
        }
642 643 644 645 646 647 648 649 650
        // 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 );
651 652 653 654 655
    }
    if ( albums.size() == 0 )
        return nullptr;
    if ( albums.size() > 1 )
    {
656
        LOG_WARN( "Multiple candidates for album ", albumName, ". Selecting first one out of luck" );
657
    }
658
    m_previousFolderId = file->folderId();
659 660
    m_previousAlbum = albums[0];
    return albums[0];
661 662 663 664
}

///
/// \brief MetadataParser::handleArtists Returns Artist's involved on a track
665
/// \param task The current parser task
666 667 668 669
/// \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
///
670
std::pair<std::shared_ptr<Artist>, std::shared_ptr<Artist>> MetadataAnalyzer::findOrCreateArtist( IItem& item ) const
671 672 673
{
    std::shared_ptr<Artist> albumArtist;
    std::shared_ptr<Artist> artist;
674
    static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name + " WHERE name = ?";
675

676 677
    const auto& albumArtistStr = item.meta( IItem::Metadata::AlbumArtist );
    const auto& artistStr = item.meta( IItem::Metadata::Artist );
678
    if ( albumArtistStr.empty() == true && artistStr.empty() == true )
679
    {
680
        return {m_unknownArtist, m_unknownArtist};
681 682
    }

683
    if ( albumArtistStr.empty() == false )
684
    {
685
        albumArtist = Artist::fetch( m_ml, req, albumArtistStr );
686 687
        if ( albumArtist == nullptr )
        {
688
            albumArtist = m_ml->createArtist( albumArtistStr );
689 690
            if ( albumArtist == nullptr )
            {
691
                LOG_ERROR( "Failed to create new artist ", albumArtistStr );
692 693
                return {nullptr, nullptr};
            }
694
            m_notifier->notifyArtistCreation( albumArtist );
695 696
        }
    }
697
    if ( artistStr.empty() == false && artistStr != albumArtistStr )
698
    {
699
        artist = Artist::fetch( m_ml, req, artistStr );
700 701
        if ( artist == nullptr )
        {
702
            artist = m_ml->createArtist( artistStr );
703 704
            if ( artist == nullptr )
            {
705
                LOG_ERROR( "Failed to create new artist ", artistStr );
706 707
                return {nullptr, nullptr};
            }
708
            m_notifier->notifyArtistCreation( artist );
709 710 711 712 713 714 715
        }
    }
    return {albumArtist, artist};
}

/* Tracks handling */

716
std::shared_ptr<AlbumTrack> MetadataAnalyzer::handleTrack( std::shared_ptr<Album> album, IItem& item,
717
                                                         std::shared_ptr<Artist> artist, Genre* genre ) const
718
{
719 720
    assert( sqlite::Transaction::transactionInProgress() == true );

721 722 723
    auto title = item.meta( IItem::Metadata::Title );
    const auto trackNumber = toInt( item, IItem::Metadata::TrackNumber );
    const auto discNumber = toInt( item, IItem::Metadata::DiscNumber );
724
    auto media = std::static_pointer_cast<Media>( item.media() );
725 726 727
    if ( title.empty() == true )
    {
        LOG_WARN( "Failed to get track title" );
728
        if ( trackNumber != 0 )
729 730
        {
            title = "Track #";
731
            title += std::to_string( trackNumber );
732 733 734
        }
    }
    if ( title.empty() == false )
735
        media->setTitleBuffered( title );
736

737
    auto track = std::static_pointer_cast<AlbumTrack>( album->addTrack( media, trackNumber,
738
                                                                        discNumber, artist->id(),
739
                                                                        genre ) );
740 741 742 743 744
    if ( track == nullptr )
    {
        LOG_ERROR( "Failed to create album track" );
        return nullptr;
    }
745

746
    const auto& releaseDate = item.meta( IItem::Metadata::Date );
747
    if ( releaseDate.empty() == false )
748
    {
749
        auto releaseYear = atoi( releaseDate.c_str() );
750
        media->setReleaseDate( releaseYear );
751 752 753 754 755
        // 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 );
    }
756
    m_notifier->notifyAlbumTrackCreation( track );
757 758 759 760 761
    return track;
}

/* Misc */

762
bool MetadataAnalyzer::link( Media& media, std::shared_ptr<Album> album,
763
                               std::shared_ptr<Artist> albumArtist, std::shared_ptr<Artist> artist )
764
{
765
    if ( albumArtist == nullptr )
766 767
    {
        assert( artist != nullptr );
768
        albumArtist = artist;
769
    }
770
    assert( album != nullptr );
771