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