MetadataParser.cpp 15 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
#include "Genre.h"
33 34 35
#include "Media.h"
#include "Show.h"
#include "utils/Filename.h"
36
#include "utils/ModificationsNotifier.h"
37
#include <cstdlib>
38

39 40 41
namespace medialibrary
{

42 43
bool MetadataParser::initialize()
{
44
    m_unknownArtist = Artist::fetch( m_ml, UnknownArtistID );
45 46 47 48 49 50 51 52 53
    if ( m_unknownArtist == nullptr )
        LOG_ERROR( "Failed to cache unknown artist" );
    return m_unknownArtist != nullptr;
}

parser::Task::Status MetadataParser::run( parser::Task& task )
{
    auto& media = task.media;

54
    bool isAudio = task.videoTracks.empty() && task.audioTracks.empty() == false;
55
    {
56 57 58 59 60 61 62 63 64 65 66 67
        auto t = m_ml->getConn()->newTransaction();
        // Some media (ogg/ts, most likely) won't have visible tracks, but shouldn't be considered audio files.
        for ( const auto& t : task.videoTracks )
        {
            media->addVideoTrack( t.fcc, t.width, t.height, t.fps, t.language, t.description );
        }
        for ( const auto& t : task.audioTracks )
        {
            media->addAudioTrack( t.fcc, t.bitrate, t.samplerate, t.nbChannels,
                                  t.language, t.description );
        }
        t->commit();
68 69 70
    }
    if ( isAudio == true )
    {
71
        if ( parseAudioFile( task ) == false )
72 73 74 75
            return parser::Task::Status::Fatal;
    }
    else
    {
76
        if (parseVideoFile( task ) == false )
77 78
            return parser::Task::Status::Fatal;
    }
79
    auto duration = task.duration;
80
    media->setDuration( duration );
81 82

    auto t = m_ml->getConn()->newTransaction();
83 84
    if ( media->save() == false )
        return parser::Task::Status::Error;
85 86 87 88
    task.file->markStepCompleted( File::ParserStep::MetadataAnalysis );
    if ( task.file->saveParserStep() == false )
        return parser::Task::Status::Error;
    t->commit();
89
    m_notifier->notifyMediaCreation( media );
90 91 92 93 94
    return parser::Task::Status::Success;
}

/* Video files */

95
bool MetadataParser::parseVideoFile( parser::Task& task ) const
96
{
97
    auto media = task.media.get();
98
    media->setType( IMedia::Type::VideoType );
99
    if ( task.title.length() == 0 )
100
        return true;
101 102

    if ( task.showName.length() == 0 )
103
    {
104
        auto show = m_ml->show( task.showName );
105 106
        if ( show == nullptr )
        {
107
            show = m_ml->createShow( task.showName );
108 109 110 111
            if ( show == nullptr )
                return false;
        }

112
        if ( task.episode != 0 )
113 114
        {
            std::shared_ptr<Show> s = std::static_pointer_cast<Show>( show );
115
            s->addEpisode( *media, task.title, task.episode );
116 117 118 119 120 121 122 123 124 125 126
        }
    }
    else
    {
        // How do we know if it's a movie or a random video?
    }
    return true;
}

/* Audio files */

127
bool MetadataParser::parseAudioFile( parser::Task& task ) const
128
{
129
    task.media->setType( IMedia::Type::AudioType );
130

131 132
    if ( task.artworkMrl.empty() == false )
        task.media->setThumbnail( task.artworkMrl );
133

134
    auto genre = handleGenre( task );
135
    auto artists = findOrCreateArtist( task );
136 137
    auto album = findAlbum( task, artists.first, artists.second );
    auto t = m_ml->getConn()->newTransaction();
138 139
    if ( album == nullptr )
    {
140
        album = m_ml->createAlbum( task.albumName, task.artworkMrl );
141 142 143
        if ( album == nullptr )
            return false;
        m_notifier->notifyAlbumCreation( album );
144
    }
145 146 147 148
    // 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 ? artists.first : m_unknownArtist), genre.get() );

149
    auto res = link( *task.media, album, artists.first, artists.second );
150 151 152 153
    t->commit();
    return res;
}

154 155 156 157 158 159 160 161 162 163 164 165 166 167
std::shared_ptr<Genre> MetadataParser::handleGenre( parser::Task& task ) const
{
    if ( task.genre.length() == 0 )
        return nullptr;
    auto genre = Genre::fromName( m_ml, task.genre );
    if ( genre == nullptr )
    {
        genre = Genre::create( m_ml, task.genre );
        if ( genre == nullptr )
            LOG_ERROR( "Failed to get/create Genre", task.genre );
    }
    return genre;
}

168
/* Album handling */
169 170 171

std::shared_ptr<Album> MetadataParser::findAlbum( parser::Task& task, std::shared_ptr<Artist> albumArtist,
                                                    std::shared_ptr<Artist> trackArtist ) const
172
{
173
    if ( task.albumName.empty() == true )
174
    {
175 176 177 178 179 180
        std::shared_ptr<Artist> artist = albumArtist;
        if ( albumArtist != nullptr )
            return albumArtist->unknownAlbum();
        else if ( trackArtist != nullptr )
            return trackArtist->unknownAlbum();
        return m_unknownArtist->unknownAlbum();
181 182 183 184
    }

    // Album matching depends on the difference between artist & album artist.
    // Specificaly pass the albumArtist here.
185 186
    static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
            " WHERE title = ?";
187
    auto albums = Album::fetchAll<Album>( m_ml, req, task.albumName );
188 189 190 191 192 193 194 195 196

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

    /*
     * 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
197
     * nullptr, so the link() method can create a new one.
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
     */
    for ( auto it = begin( albums ); it != end( albums ); )
    {
        auto a = (*it).get();
        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.
            auto candidateAlbumArtist = a->albumArtist();
            if ( candidateAlbumArtist != nullptr && candidateAlbumArtist->id() != albumArtist->id() )
            {
                it = albums.erase( it );
                continue;
            }
        }
        // If this is a multidisc album, assume it could be in a multiple amount of folders.
        // Since folders can come in any order, we can't assume the first album will be the
        // first media we see. If the discTotal or discNumber meta are provided, that's easy. If not,
        // we assume that another CD with the same name & artists, and a disc number > 1
        // denotes a multi disc album
        // Check the first case early to avoid fetching tracks if unrequired.
219
        if ( task.discTotal > 1 || task.discNumber > 1 )
220 221 222 223
        {
            ++it;
            continue;
        }
224
        const auto tracks = a->cachedTracks();
225 226 227 228 229 230 231
        // 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;
        }
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250

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

        // Assume album files will be in the same folder.
251
        auto newFileFolder = utils::file::directory( task.file->mrl() );
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
        auto trackFiles = tracks[0]->files();
        bool excluded = false;
        for ( auto& f : trackFiles )
        {
            auto candidateFolder = utils::file::directory( f->mrl() );
            if ( candidateFolder != newFileFolder )
            {
                excluded = true;
                break;
            }
        }
        if ( excluded == true )
        {
            it = albums.erase( it );
            continue;
        }
        ++it;
    }
    if ( albums.size() == 0 )
        return nullptr;
    if ( albums.size() > 1 )
    {
274
        LOG_WARN( "Multiple candidates for album ", task.albumName, ". Selecting first one out of luck" );
275 276 277 278 279 280
    }
    return std::static_pointer_cast<Album>( albums[0] );
}

///
/// \brief MetadataParser::handleArtists Returns Artist's involved on a track
281
/// \param task The current parser task
282 283 284 285
/// \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
///
286
std::pair<std::shared_ptr<Artist>, std::shared_ptr<Artist>> MetadataParser::findOrCreateArtist( parser::Task& task ) const
287 288 289
{
    std::shared_ptr<Artist> albumArtist;
    std::shared_ptr<Artist> artist;
290
    static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name + " WHERE name = ?";
291

292
    if ( task.albumArtist.empty() == true && task.artist.empty() == true )
293
    {
294
        return {m_unknownArtist, m_unknownArtist};
295 296
    }

297
    if ( task.albumArtist.empty() == false )
298
    {
299
        albumArtist = Artist::fetch( m_ml, req, task.albumArtist);
300 301
        if ( albumArtist == nullptr )
        {
302
            albumArtist = m_ml->createArtist( task.albumArtist );
303 304
            if ( albumArtist == nullptr )
            {
305
                LOG_ERROR( "Failed to create new artist ", task.albumArtist );
306 307
                return {nullptr, nullptr};
            }
308
            m_notifier->notifyArtistCreation( albumArtist );
309 310
        }
    }
311
    if ( task.artist.empty() == false && task.artist != task.albumArtist )
312
    {
313
        artist = Artist::fetch( m_ml, req, task.artist );
314 315
        if ( artist == nullptr )
        {
316
            artist = m_ml->createArtist( task.artist );
317 318
            if ( artist == nullptr )
            {
319
                LOG_ERROR( "Failed to create new artist ", task.artist );
320 321
                return {nullptr, nullptr};
            }
322
            m_notifier->notifyArtistCreation( albumArtist );
323 324 325 326 327 328 329
        }
    }
    return {albumArtist, artist};
}

/* Tracks handling */

330 331
std::shared_ptr<AlbumTrack> MetadataParser::handleTrack( std::shared_ptr<Album> album, parser::Task& task,
                                                         std::shared_ptr<Artist> artist, Genre* genre ) const
332
{
333
    auto title = task.title;
334 335 336
    if ( title.empty() == true )
    {
        LOG_WARN( "Failed to get track title" );
337
        if ( task.trackNumber != 0 )
338 339
        {
            title = "Track #";
340
            title += std::to_string( task.trackNumber );
341 342 343
        }
    }
    if ( title.empty() == false )
344
        task.media->setTitle( title );
345

346 347 348
    auto track = std::static_pointer_cast<AlbumTrack>( album->addTrack( task.media, task.trackNumber,
                                                                        task.discNumber, artist->id(),
                                                                        genre != nullptr ? genre->id() : 0 ) );
349 350 351 352 353
    if ( track == nullptr )
    {
        LOG_ERROR( "Failed to create album track" );
        return nullptr;
    }
354 355

    if ( task.releaseDate.empty() == false )
356
    {
357
        auto releaseYear = atoi( task.releaseDate.c_str() );
358
        task.media->setReleaseDate( releaseYear );
359 360 361 362 363
        // 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 );
    }
364
    m_notifier->notifyAlbumTrackCreation( track );
365 366 367 368 369 370 371 372
    return track;
}

/* Misc */

bool MetadataParser::link( Media& media, std::shared_ptr<Album> album,
                               std::shared_ptr<Artist> albumArtist, std::shared_ptr<Artist> artist ) const
{
373 374
    if ( albumArtist == nullptr )
        albumArtist = artist;
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396

    // 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.
    if ( albumArtist != nullptr && albumArtist->artworkMrl().empty() == true &&
         album != nullptr && album->artworkMrl().empty() == false )
        albumArtist->setArtworkMrl( album->artworkMrl() );

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

    auto currentAlbumArtist = album->albumArtist();

    // If we have no main artist yet, that's easy, we need to assign one.
    if ( currentAlbumArtist == nullptr )
    {
        // We don't know if the artist was tagged as artist or albumartist, however, we simply add it
        // as the albumartist until proven we were wrong (ie. until one of the next tracks
        // has a different artist)
397
        album->setAlbumArtist( albumArtist );
398 399 400 401 402 403 404 405 406 407
        // Always add the album artist as an artist
        album->addArtist( albumArtist );
        if ( artist != nullptr )
            album->addArtist( artist );
    }
    else
    {
        if ( albumArtist->id() != currentAlbumArtist->id() )
        {
            // We have more than a single artist on this album, fallback to various artists
408
            auto variousArtists = Artist::fetch( m_ml, VariousArtistID );
409
            album->setAlbumArtist( variousArtists );
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
            // Add those two artists as "featuring".
            album->addArtist( albumArtist );
        }
        if ( artist != nullptr && artist->id() != albumArtist->id() )
        {
            if ( albumArtist->id() != artist->id() )
               album->addArtist( artist );
        }
    }

    return true;
}


const char* MetadataParser::name() const
{
    return "Metadata";
}
428 429 430 431 432 433 434 435 436 437

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

439 440 441 442 443
File::ParserStep MetadataParser::step() const
{
    return File::ParserStep::MetadataAnalysis;
}

444
}