354 changes: 354 additions & 0 deletions mythplugins/mythmusic/mythmusic/musicbrainz.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
#include "musicbrainz.h"
#include "config.h"

// Qt
#include <QObject>
#include <QFile>

// MythTV
#include "libmythbase/mythmiscutil.h"

#ifdef HAVE_MUSICBRAINZ

#include <string>
#include <fstream>

// libdiscid
#include <discid/discid.h>

// libmusicbrainz5
#include "musicbrainz5/Artist.h"
#include "musicbrainz5/ArtistCredit.h"
#include "musicbrainz5/NameCredit.h"
#include "musicbrainz5/Query.h"
#include "musicbrainz5/Disc.h"
#include "musicbrainz5/Medium.h"
#include "musicbrainz5/Release.h"
#include "musicbrainz5/Track.h"
#include "musicbrainz5/TrackList.h"
#include "musicbrainz5/Recording.h"
#include "musicbrainz5/HTTPFetch.h"

// libcoverart
#include "coverart/CoverArt.h"
#include "coverart/HTTPFetch.h"

constexpr auto user_agent = "mythtv";

std::string MusicBrainz::queryDiscId(const std::string &device)
{
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Query disc id for device %1").arg(QString::fromStdString(device)));
DiscId *disc = discid_new();
std::string disc_id;
if ( discid_read_sparse(disc, device.c_str(), 0) == 0 )
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: %1").arg(discid_get_error_msg(disc)));
}
else
{
disc_id = discid_get_id(disc);
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Got disc id %1").arg(QString::fromStdString(disc_id)));
}
discid_free(disc);

return disc_id;
}

/// Compile artist names from artist credits
static std::vector<std::string> queryArtists(const MusicBrainz5::CArtistCredit *artist_credit)
{
std::vector<std::string> artist_names;
if (!artist_credit)
{
return artist_names;
}

for (int a = 0; a < artist_credit->NameCreditList()->NumItems(); ++a)
{
auto *artist = artist_credit->NameCreditList()->Item(a)->Artist();
artist_names.emplace_back(artist->Name());
}
return artist_names;
}

/// Creates single artist string from artist list
static QString artistsToString(const std::vector<std::string> &artists)
{
QString res;
for (const auto &artist : artists)
{
res += QString(res.isEmpty() ? "%1" : "; %1").arg(artist.c_str());
}
return res;
}

/// Log MusicBrainz query errors
static void logError(MusicBrainz5::CQuery &query)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastResult: %1").arg(query.LastResult()));
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastHTTPCode: %1").arg(query.LastHTTPCode()));
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastErrorMessage: '%1'").arg(QString::fromStdString(query.LastErrorMessage())));
}

std::string MusicBrainz::queryRelease(const std::string &discId)
{
// clear old metadata
m_tracks.clear();

LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Query metadata for disc id '%1'").arg(QString::fromStdString(discId)));
MusicBrainz5::CQuery query(user_agent);
try
{
auto discMetadata = query.Query("discid", discId);
if (discMetadata.Disc() && discMetadata.Disc()->ReleaseList())
{
auto *releases = discMetadata.Disc()->ReleaseList();
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Found %1 release(s)").arg(releases->NumItems()));
for (int count = 0; count < releases->NumItems(); ++count)
{
auto *basicRelease = releases->Item(count);
// The releases returned from LookupDiscID don't contain full information
MusicBrainz5::CQuery::tParamMap params;
params["inc"]="artists recordings artist-credits discids";
auto releaseMetadata = query.Query("release", basicRelease->ID(), "", params);
if (releaseMetadata.Release())
{
auto *fullRelease = releaseMetadata.Release();
if (!fullRelease)
{
continue;
}
auto media = fullRelease->MediaMatchingDiscID(discId);
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Found %1 matching media").arg(media.NumItems()));
for (int m = 0; m < media.NumItems(); ++m)
{
auto *medium = media.Item(m);
if (!medium || !medium->ContainsDiscID(discId))
{
continue;
}
std::string albumTitle;
if (!medium->Title().empty())
{
albumTitle = medium->Title();
}
else if(!fullRelease->Title().empty())
{
albumTitle = fullRelease->Title();
}
const auto albumArtists = queryArtists(fullRelease->ArtistCredit());
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Release: %1").arg(QString::fromStdString(fullRelease->ID())));
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Title: %1").arg(QString::fromStdString(albumTitle)));
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Artist: %1").arg(artistsToString(albumArtists)));
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Date: %1").arg(QString::fromStdString(fullRelease->Date())));
auto *tracks = medium->TrackList();
if (tracks)
{
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Found %1 track(s)").arg(tracks->NumItems()));
for (int t = 0; t < tracks->NumItems(); ++t)
{
auto *track = tracks->Item(t);
if (track && track->Recording())
{
auto *recording = track->Recording();
const auto length = std::div(recording->Length() / 1000, 60);
const int minutes = length.quot;
const int seconds = length.rem;
const auto artists = queryArtists(recording->ArtistCredit());
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: %1: %2:%3 - %4 (%5)")
.arg(track->Position())
.arg(minutes, 2).arg(seconds, 2, 10, QChar('0'))
.arg(QString::fromStdString(recording->Title()))
.arg(artistsToString(artists)));

// fill metadata
MusicMetadata &metadata = m_tracks[track->Position()];
metadata.setAlbum(QString::fromStdString(albumTitle));
metadata.setTitle(QString::fromStdString(recording->Title()));
metadata.setTrack(track->Position());
metadata.setLength(std::chrono::milliseconds(recording->Length()));
if (albumArtists.size() == 1)
{
metadata.setArtist(QString::fromStdString(albumArtists[0]));
}
else if(albumArtists.size() > 1)
{
metadata.setArtist(QObject::tr("Various Artists"));
}
metadata.setYear(QDate::fromString(QString::fromStdString(fullRelease->Date()), Qt::ISODate).year());
}
}
}
}
return fullRelease->ID();
}
}
}
}
catch (MusicBrainz5::CConnectionError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Connection Exception: '%1'").arg(error.what()));
logError(query);
}
catch (MusicBrainz5::CTimeoutError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Timeout Exception: '%1'").arg(error.what()));
logError(query);
}
catch (MusicBrainz5::CAuthenticationError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Authentication Exception: '%1'").arg(error.what()));
logError(query);
}
catch (MusicBrainz5::CFetchError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Fetch Exception: '%1'").arg(error.what()));
logError(query);
}
catch (MusicBrainz5::CRequestError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Request Exception: '%1'").arg(error.what()));
logError(query);
}
catch (MusicBrainz5::CResourceNotFoundError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: ResourceNotFound Exception: '%1'").arg(error.what()));
logError(query);
}

return {};
}

static void logError(CoverArtArchive::CCoverArt &coverArt)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastResult: %1").arg(coverArt.LastResult()));
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastHTTPCode: %1").arg(coverArt.LastHTTPCode()));
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastErrorMessage: '%1'").arg(QString::fromStdString(coverArt.LastErrorMessage())));
}

QString MusicBrainz::queryCoverart(const std::string &releaseId)
{
const QString fileName = QString("musicbrainz-%1-front.jpg").arg(releaseId.c_str());
const QString filePath = QDir::temp().absoluteFilePath(fileName);
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Check if coverart file exists for release '%1'").arg(QString::fromStdString(releaseId)));
if (QDir::temp().exists(fileName))
{
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Cover art file '%1' exist already").arg(filePath));
return filePath;
}

LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Query cover art for release '%1'").arg(QString::fromStdString(releaseId)));
CoverArtArchive::CCoverArt coverArt(user_agent);
try
{
std::vector<unsigned char> imageData = coverArt.FetchFront(releaseId);
if (imageData.size())
{
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Saving front coverart to '%1'").arg(filePath));

QFile coverArtFile(filePath);
if (!coverArtFile.open(QIODevice::WriteOnly))
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Unable to open temporary file '%1'").arg(filePath));
return {};
}

const auto coverArtBytes = static_cast<qint64>(imageData.size());
const auto writtenBytes = coverArtFile.write(reinterpret_cast<const char*>(imageData.data()), coverArtBytes);
coverArtFile.close();
if (writtenBytes != coverArtBytes)
{
LOG(VB_MEDIA, LOG_ERR, QString("ERROR musicbrainz: Could not write coverart data to file '%1'").arg(filePath));
return {};
}

return filePath;
}
}
catch (CoverArtArchive::CConnectionError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Connection Exception: '%1'").arg(error.what()));
logError(coverArt);
}
catch (CoverArtArchive::CTimeoutError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Timeout Exception: '%1'").arg(error.what()));
logError(coverArt);
}
catch (CoverArtArchive::CAuthenticationError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Authentication Exception: '%1'").arg(error.what()));
logError(coverArt);
}
catch (CoverArtArchive::CFetchError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Fetch Exception: '%1'").arg(error.what()));
logError(coverArt);
}
catch (CoverArtArchive::CRequestError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Request Exception: '%1'").arg(error.what()));
logError(coverArt);
}
catch (CoverArtArchive::CResourceNotFoundError& error)
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: ResourceNotFound Exception: '%1'").arg(error.what()));
logError(coverArt);
}

return {};
}

#endif // HAVE_MUSICBRAINZ

bool MusicBrainz::queryForDevice(const QString &deviceName)
{
#ifdef HAVE_MUSICBRAINZ
const auto discId = queryDiscId(deviceName.toStdString());
if (discId.empty())
{
return false;
}
if (discId == m_discId)
{
// already queried
LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Metadata for disc %1 already present").arg(QString::fromStdString(m_discId)));
return true;
}
const auto releaseId = queryRelease(discId);
if (releaseId.empty())
{
return false;
}
const auto covertArtFileName = queryCoverart(releaseId);
if (!covertArtFileName.isEmpty())
{
m_albumArt.m_filename = covertArtFileName;
m_albumArt.m_imageType = IT_FRONTCOVER;
}
m_discId = discId;

return true;
#else
return false;
#endif
}

bool MusicBrainz::hasMetadata(int track) const
{
return m_tracks.find(track) != m_tracks.end();
}

MusicMetadata *MusicBrainz::getMetadata(int track) const
{
auto it = m_tracks.find(track);
if (it == m_tracks.end())
{
LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: No metadata for track %1").arg(track));
return nullptr;
}
auto *metadata = new MusicMetadata(it.value());
metadata->getAlbumArtImages()->addImage(&m_albumArt);
return metadata;
}

61 changes: 61 additions & 0 deletions mythplugins/mythmusic/mythmusic/musicbrainz.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#ifndef MUSICBRAINZ_H
#define MUSICBRAINZ_H

#include "config.h"

// Qt
#include <QString>
#include <QMap>

// MythTV
#include <libmythmetadata/musicmetadata.h>

class MusicBrainz
{
public:
/**
* Query music metadata using disc id of specified device
*
* @param [in] deviceName name of the CD device to query metadata for
* @return true if query was successful, false otherwise
*/
bool queryForDevice(const QString &deviceName);

/**
* Checks if metadata for given track exists
*
* @param track [in] track number to check metadata for
* @return true if metadata was found, false otherwise
*/
bool hasMetadata(int track) const;

/**
* Creates and return metadata for specified track
*
* @param [in] track the track number for which to return the metadata
* @return pointer to newly created metadata object, nullptr if no metadata for this track exists
*/
MusicMetadata *getMetadata(int track) const;

private:

#ifdef HAVE_MUSICBRAINZ

/// Query disc id for specified device
std::string queryDiscId(const std::string &device);

/// Query release id and release metadata
std::string queryRelease(const std::string &disc_id);

/// Query coverart for given release id
QString queryCoverart(const std::string &releaseId);

std::string m_discId; ///< disc id corresponding to current metadata

#endif // HAVE_MUSICBRAINZ

QMap<int, MusicMetadata> m_tracks;
AlbumArtImage m_albumArt;
};

#endif // MUSICBRAINZ_H
5 changes: 5 additions & 0 deletions mythplugins/mythmusic/mythmusic/mythmusic.pro
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ cdio {
LIBS += -lcdio -lcdio_cdda -lcdio_paranoia
}

musicbrainz {
HEADERS += musicbrainz.h
SOURCES += musicbrainz.cpp
}

mingw {
LIBS += -logg

Expand Down