MetadataParser.cpp 32 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
168
169
170

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

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

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

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

238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/* 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;

317
318
319
320
321
322
323
324
    // 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();
325
326
327
328
329
    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 } );
330
        FsDiscoverer discoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
331
332
333
334
335
336
        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 } );
337
    FsDiscoverer discoverer( fsFactory, m_ml, nullptr, std::move( probePtr ) );
338
339
340
    if ( parentKnown == false )
    {
        discoverer.discover( entryPoint );
341
342
343
        auto entryFolder = Folder::fromMrl( m_ml, entryPoint );
        if ( entryFolder != nullptr )
            Folder::excludeEntryFolder( m_ml, entryFolder->id() );
344
345
346
347
348
        return;
    }
    discoverer.reload( directoryMrl );
}

349
350
/* Video files */

351
bool MetadataParser::parseVideoFile( parser::Task& task ) const
352
{
353
    auto media = task.media.get();
354
    media->setType( IMedia::Type::Video );
355
356
    const auto& title = task.vlcMedia.meta( libvlc_meta_Title );
    if ( title.length() == 0 )
357
        return true;
358

359
    const auto& showName = task.vlcMedia.meta( libvlc_meta_ShowName );
360

361
362
363
364
365
366
    return sqlite::Tools::withRetries( 3, [this, &showName, &title, &task]() {
        auto t = m_ml->getConn()->newTransaction();
        task.media->setTitleBuffered( title );

        if ( showName.length() != 0 )
        {
367
            auto show = m_ml->show( showName );
368
            if ( show == nullptr )
369
370
371
372
373
374
375
376
377
378
379
            {
                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 );
            }
380
381
382
383
384
385
386
387
388
        }
        else
        {
            // How do we know if it's a movie or a random video?
        }
        task.media->save();
        t->commit();
        return true;
    });
389
390
391
392
393
    return true;
}

/* Audio files */

394
bool MetadataParser::parseAudioFile( parser::Task& task )
395
{
396
    task.media->setType( IMedia::Type::Audio );
397

398
    auto artworkMrl = task.vlcMedia.meta( libvlc_meta_ArtworkURL );
399
    if ( artworkMrl.empty() == false )
400
    {
401
        task.media->setThumbnail( artworkMrl );
402
403
404
405
406
        // Don't use an attachment as default artwork for album/artists
        if ( utils::file::schemeIs( "attachment", artworkMrl ) )
            artworkMrl.clear();
    }

407

408
    auto genre = handleGenre( task );
409
    auto artists = findOrCreateArtist( task );
410
411
    if ( artists.first == nullptr && artists.second == nullptr )
        return false;
412
    auto album = findAlbum( task, artists.first, artists.second );
413
414
415
    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();
416
        if ( album == nullptr )
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
        {
            const auto& albumName = task.vlcMedia.meta( libvlc_meta_Album );
            album = m_ml->createAlbum( albumName, artworkMrl );
            if ( album == nullptr )
                return false;
            m_notifier->notifyAlbumCreation( album );
        }
        // If we know a track artist, specify it, otherwise, fallback to the album/unknown artist
        auto track = handleTrack( album, task, artists.second ? artists.second : artists.first,
                                  genre.get() );

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

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

450
/* Album handling */
451
452

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

465
466
467
468
469
470
    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;

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

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

480
    const auto discTotal = toInt( task.vlcMedia, libvlc_meta_DiscTotal, "disc total" );
481
    const auto discNumber = toInt( task.vlcMedia, libvlc_meta_DiscNumber, "disc number" );
482
483
484
485
486
    /*
     * 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
487
     * nullptr, so the link() method can create a new one.
488
489
490
491
     */
    for ( auto it = begin( albums ); it != end( albums ); )
    {
        auto a = (*it).get();
492
        auto candidateAlbumArtist = a->albumArtist();
493
494
495
496
        // 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 );
497
498
499
500
        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.
501
            if ( candidateAlbumArtist->id() != albumArtist->id() )
502
503
504
505
506
507
508
509
510
511
512
            {
                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.
513
        if ( discTotal > 1 || discNumber > 1 )
514
515
516
517
        {
            ++it;
            continue;
        }
518
        const auto tracks = a->cachedTracks();
519
520
521
522
523
524
525
        // 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;
        }
526
527

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

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

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

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

630
631
632
    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 )
633
    {
634
        return {m_unknownArtist, m_unknownArtist};
635
636
    }

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

/* Tracks handling */

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

675
676
677
    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" );
678
679
680
    if ( title.empty() == true )
    {
        LOG_WARN( "Failed to get track title" );
681
        if ( trackNumber != 0 )
682
683
        {
            title = "Track #";
684
            title += std::to_string( trackNumber );
685
686
687
        }
    }
    if ( title.empty() == false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
688
        task.media->setTitleBuffered( title );
689

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

699
700
    const auto& releaseDate = task.vlcMedia.meta( libvlc_meta_Date );
    if ( releaseDate.empty() == false )
701
    {
702
        auto releaseYear = atoi( releaseDate.c_str() );
703
        task.media->setReleaseDate( releaseYear );
704
705
706
707
708
        // 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 );
    }
709
    m_notifier->notifyAlbumTrackCreation( track );
710
711
712
713
714
715
    return track;
}

/* Misc */

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

    // 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.
728
729
730
731
732
    // 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 &&
733
734
735
         album != nullptr && album->artworkMrl().empty() == false )
        albumArtist->setArtworkMrl( album->artworkMrl() );

736
737
738
739
740
741
742
743
    // 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 )
        artist->setArtworkMrl( album->artworkMrl() );

744
745
746
747
748
749
750
751
752
753
754
755
756
    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)
757
        album->setAlbumArtist( albumArtist );
758
759
        // Always add the album artist as an artist
        album->addArtist( albumArtist );
760
761
762
        // Always update the album artist number of tracks.
        // The artist might be different, and will be handled a few lines below
        albumArtist->updateNbTrack( 1 );
763
        if ( artist != nullptr )
764
765
766
767
768
        {
            // 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 );
769
            album->addArtist( artist );
770
        }
771
772
773
    }
    else
    {
774
        // We have more than a single artist on this album, fallback to various artists
775
776
        if ( albumArtist->id() != currentAlbumArtist->id() )
        {
777
778
            if ( m_variousArtists == nullptr )
                m_variousArtists = Artist::fetch( m_ml, VariousArtistID );
779
780
781
782
783
784
785
786
787
788
789
790
791
792
            // 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".
793
794
795
796
            album->addArtist( albumArtist );
        }
        if ( artist != nullptr && artist->id() != albumArtist->id() )
        {
797
798
           album->addArtist( artist );
           artist->updateNbTrack( 1 );
799
        }
800
        albumArtist->updateNbTrack( 1 );
801
802
803
804
805
806
807
808
809
    }

    return true;
}

const char* MetadataParser::name() const
{
    return "Metadata";
}
810
811
812
813
814
815
816
817
818
819

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

821
822
823
824
825
826
827
828
void MetadataParser::flush()
{
    ParserService::flush();
    m_variousArtists = nullptr;
    m_previousAlbum = nullptr;
    m_previousFolderId = 0;
}

829
830
831
832
833
834
void MetadataParser::restart()
{
    // Reset locally cached entities
    initialize();
}

835
bool MetadataParser::isCompleted( const parser::Task& task ) const
836
{
837
    // We always need to run this task if the metadata extraction isn't completed
838
    return task.isStepCompleted( parser::Task::ParserStep::MetadataAnalysis );
839
840
}

841
}