MythTV master
musicbrainz.cpp
Go to the documentation of this file.
1#include "musicbrainz.h"
2#include "config.h"
3
4// Qt
5#include <QObject>
6#include <QFile>
7
8// MythTV
11
12#ifdef HAVE_MUSICBRAINZ
13
14#include <string>
15#include <fstream>
16
17// libdiscid
18#include <discid/discid.h>
19
20// libmusicbrainz5
21#include "musicbrainz5/Artist.h"
22#include "musicbrainz5/ArtistCredit.h"
23#include "musicbrainz5/NameCredit.h"
24#include "musicbrainz5/Query.h"
25#include "musicbrainz5/Disc.h"
26#include "musicbrainz5/Medium.h"
27#include "musicbrainz5/Release.h"
28#include "musicbrainz5/Track.h"
29#include "musicbrainz5/TrackList.h"
30#include "musicbrainz5/Recording.h"
31#include "musicbrainz5/HTTPFetch.h"
32
33// libcoverart
34#include "coverart/CoverArt.h"
35#include "coverart/HTTPFetch.h"
36
37constexpr auto user_agent = "mythtv";
38
39std::string MusicBrainz::queryDiscId(const std::string &device)
40{
41 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Query disc id for device %1").arg(QString::fromStdString(device)));
42 DiscId *disc = discid_new();
43 std::string disc_id;
44 if ( discid_read_sparse(disc, device.c_str(), 0) == 0 )
45 {
46 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: %1").arg(discid_get_error_msg(disc)));
47 }
48 else
49 {
50 disc_id = discid_get_id(disc);
51 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Got disc id %1").arg(QString::fromStdString(disc_id)));
52 }
53 discid_free(disc);
54
55 return disc_id;
56}
57
59static std::vector<std::string> queryArtists(const MusicBrainz5::CArtistCredit *artist_credit)
60{
61 std::vector<std::string> artist_names;
62 if (!artist_credit)
63 {
64 return artist_names;
65 }
66
67 std::string joinPhrase;
68 for (int a = 0; a < artist_credit->NameCreditList()->NumItems(); ++a)
69 {
70 auto *nameCredit = artist_credit->NameCreditList()->Item(a);
71 auto *artist = nameCredit->Artist();
72 if (a == 0)
73 {
74 joinPhrase = nameCredit->JoinPhrase();
75 artist_names.emplace_back(artist->Name());
76 }
77 else if (!joinPhrase.empty())
78 {
79 artist_names.back() += joinPhrase + artist->Name();
80 }
81 else
82 {
83 artist_names.emplace_back(artist->Name());
84 }
85 }
86 return artist_names;
87}
88
90static QString artistsToString(const std::vector<std::string> &artists)
91{
92 QString res;
93 for (const auto &artist : artists)
94 {
95 res += QString(res.isEmpty() ? "%1" : "; %1").arg(artist.c_str());
96 }
97 return res;
98}
99
101static void logError(MusicBrainz5::CQuery &query)
102{
103 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastResult: %1").arg(query.LastResult()));
104 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastHTTPCode: %1").arg(query.LastHTTPCode()));
105 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastErrorMessage: '%1'").arg(QString::fromStdString(query.LastErrorMessage())));
106}
107
108std::string MusicBrainz::queryRelease(const std::string &discId)
109{
110 // clear old metadata
111 m_tracks.clear();
112
113 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Query metadata for disc id '%1'").arg(QString::fromStdString(discId)));
114 MusicBrainz5::CQuery query(user_agent);
115 try
116 {
117 auto discMetadata = query.Query("discid", discId);
118 if (discMetadata.Disc() && discMetadata.Disc()->ReleaseList())
119 {
120 auto *releases = discMetadata.Disc()->ReleaseList();
121 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Found %1 release(s)").arg(releases->NumItems()));
122 for (int count = 0; count < releases->NumItems(); ++count)
123 {
124 auto *basicRelease = releases->Item(count);
125 // The releases returned from LookupDiscID don't contain full information
126 MusicBrainz5::CQuery::tParamMap params;
127 params["inc"]="artists recordings artist-credits discids";
128 auto releaseMetadata = query.Query("release", basicRelease->ID(), "", params);
129 if (releaseMetadata.Release())
130 {
131 auto *fullRelease = releaseMetadata.Release();
132 if (!fullRelease)
133 {
134 continue;
135 }
136 auto media = fullRelease->MediaMatchingDiscID(discId);
137 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Found %1 matching media").arg(media.NumItems()));
138 int artistDiff = 0;
139 for (int m = 0; m < media.NumItems(); ++m)
140 {
141 auto *medium = media.Item(m);
142 if (!medium || !medium->ContainsDiscID(discId))
143 {
144 continue;
145 }
146 std::string albumTitle;
147 if (!medium->Title().empty())
148 {
149 albumTitle = medium->Title();
150 }
151 else if(!fullRelease->Title().empty())
152 {
153 albumTitle = fullRelease->Title();
154 }
155 const auto albumArtists = queryArtists(fullRelease->ArtistCredit());
156 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Release: %1").arg(QString::fromStdString(fullRelease->ID())));
157 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Title: %1").arg(QString::fromStdString(albumTitle)));
158 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Artist: %1").arg(artistsToString(albumArtists)));
159 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Date: %1").arg(QString::fromStdString(fullRelease->Date())));
160 auto *tracks = medium->TrackList();
161 if (tracks)
162 {
163 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Found %1 track(s)").arg(tracks->NumItems()));
164 for (int t = 0; t < tracks->NumItems(); ++t)
165 {
166 auto *track = tracks->Item(t);
167 if (track && track->Recording())
168 {
169 auto *recording = track->Recording();
170 const auto length = std::div(recording->Length() / 1000, 60);
171 const int minutes = length.quot;
172 const int seconds = length.rem;
173 const auto artists = queryArtists(recording->ArtistCredit());
174 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: %1: %2:%3 - %4 (%5)")
175 .arg(track->Position())
176 .arg(minutes, 2).arg(seconds, 2, 10, QChar('0'))
177 .arg(QString::fromStdString(recording->Title()),
178 artistsToString(artists)));
179
180 // fill metadata
181 MusicMetadata &metadata = m_tracks[track->Position()];
182 metadata.setAlbum(QString::fromStdString(albumTitle));
183 metadata.setTitle(QString::fromStdString(recording->Title()));
184 metadata.setTrack(track->Position());
185 metadata.setLength(std::chrono::milliseconds(recording->Length()));
186 if (albumArtists.size() == 1)
187 {
188 metadata.setCompilationArtist(QString::fromStdString(albumArtists[0]));
189 }
190 else if(albumArtists.size() > 1)
191 {
192 metadata.setCompilationArtist(QObject::tr("Various Artists"));
193 }
194 if (artists.size() == 1)
195 {
196 metadata.setArtist(QString::fromStdString(artists[0]));
197 }
198 else if(artists.size() > 1)
199 {
200 metadata.setArtist(QObject::tr("Various Artists"));
201 }
202 if (metadata.CompilationArtist() != metadata.Artist())
203 {
204 artistDiff++;
205 }
206 metadata.setYear(QDate::fromString(QString::fromStdString(fullRelease->Date()), Qt::ISODate).year());
207 }
208 }
209 }
210 }
211 // Set compilation flag if album artist differs from track artists
212 // as there might be some tracks featuring guest artists we only set
213 // the compilation flag if at least half of the track artists differ
214 setCompilationFlag(artistDiff > m_tracks.count() / 2);
215
216 return fullRelease->ID();
217 }
218 }
219 }
220 }
221 catch (MusicBrainz5::CConnectionError& error)
222 {
223 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Connection Exception: '%1'").arg(error.what()));
224 logError(query);
225 }
226 catch (MusicBrainz5::CTimeoutError& error)
227 {
228 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Timeout Exception: '%1'").arg(error.what()));
229 logError(query);
230 }
231 catch (MusicBrainz5::CAuthenticationError& error)
232 {
233 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Authentication Exception: '%1'").arg(error.what()));
234 logError(query);
235 }
236 catch (MusicBrainz5::CFetchError& error)
237 {
238 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Fetch Exception: '%1'").arg(error.what()));
239 logError(query);
240 }
241 catch (MusicBrainz5::CRequestError& error)
242 {
243 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Request Exception: '%1'").arg(error.what()));
244 logError(query);
245 }
246 catch (MusicBrainz5::CResourceNotFoundError& error)
247 {
248 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: ResourceNotFound Exception: '%1'").arg(error.what()));
249 logError(query);
250 }
251
252 return {};
253}
254
255void MusicBrainz::setCompilationFlag(bool isCompilation)
256{
257 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Setting compilation flag: %1").arg(isCompilation));
258 for (auto &metadata : m_tracks)
259 {
260 metadata.setCompilation(isCompilation);
261 }
262}
263
264static void logError(CoverArtArchive::CCoverArt &coverArt)
265{
266 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastResult: %1").arg(coverArt.LastResult()));
267 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastHTTPCode: %1").arg(coverArt.LastHTTPCode()));
268 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: LastErrorMessage: '%1'").arg(QString::fromStdString(coverArt.LastErrorMessage())));
269}
270
271QString MusicBrainz::queryCoverart(const std::string &releaseId)
272{
273 const QString fileName = QString("musicbrainz-%1-front.jpg").arg(releaseId.c_str());
274 QString filePath = QDir::temp().absoluteFilePath(fileName);
275 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Check if coverart file exists for release '%1'").arg(QString::fromStdString(releaseId)));
276 if (QDir::temp().exists(fileName))
277 {
278 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Cover art file '%1' exist already").arg(filePath));
279 return filePath;
280 }
281
282 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Query cover art for release '%1'").arg(QString::fromStdString(releaseId)));
283 CoverArtArchive::CCoverArt coverArt(user_agent);
284 try
285 {
286 std::vector<unsigned char> imageData = coverArt.FetchFront(releaseId);
287 if (!imageData.empty())
288 {
289 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Saving front coverart to '%1'").arg(filePath));
290
291 QFile coverArtFile(filePath);
292 if (!coverArtFile.open(QIODevice::WriteOnly))
293 {
294 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Unable to open temporary file '%1'").arg(filePath));
295 return {};
296 }
297
298 const auto coverArtBytes = static_cast<qint64>(imageData.size());
299 const auto writtenBytes = coverArtFile.write(reinterpret_cast<const char*>(imageData.data()), coverArtBytes);
300 coverArtFile.close();
301 if (writtenBytes != coverArtBytes)
302 {
303 LOG(VB_MEDIA, LOG_ERR, QString("ERROR musicbrainz: Could not write coverart data to file '%1'").arg(filePath));
304 return {};
305 }
306
307 return filePath;
308 }
309 }
310 catch (CoverArtArchive::CConnectionError& error)
311 {
312 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Connection Exception: '%1'").arg(error.what()));
313 logError(coverArt);
314 }
315 catch (CoverArtArchive::CTimeoutError& error)
316 {
317 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Timeout Exception: '%1'").arg(error.what()));
318 logError(coverArt);
319 }
320 catch (CoverArtArchive::CAuthenticationError& error)
321 {
322 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Authentication Exception: '%1'").arg(error.what()));
323 logError(coverArt);
324 }
325 catch (CoverArtArchive::CFetchError& error)
326 {
327 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Fetch Exception: '%1'").arg(error.what()));
328 logError(coverArt);
329 }
330 catch (CoverArtArchive::CRequestError& error)
331 {
332 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: Request Exception: '%1'").arg(error.what()));
333 logError(coverArt);
334 }
335 catch (CoverArtArchive::CResourceNotFoundError& error)
336 {
337 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: ResourceNotFound Exception: '%1'").arg(error.what()));
338 logError(coverArt);
339 }
340
341 return {};
342}
343
344#endif // HAVE_MUSICBRAINZ
345
346bool MusicBrainz::queryForDevice(const QString &deviceName)
347{
348#ifdef HAVE_MUSICBRAINZ
349 const auto discId = queryDiscId(deviceName.toStdString());
350 if (discId.empty())
351 {
352 reset();
353 return false;
354 }
355 if (discId == m_discId)
356 {
357 // already queried
358 LOG(VB_MEDIA, LOG_DEBUG, QString("musicbrainz: Metadata for disc %1 already present").arg(QString::fromStdString(m_discId)));
359 return true;
360 }
361
362 // new disc id, reset existing data
363 reset();
364
365 const auto releaseId = queryRelease(discId);
366 if (releaseId.empty())
367 {
368 return false;
369 }
370 const auto covertArtFileName = queryCoverart(releaseId);
371 if (!covertArtFileName.isEmpty())
372 {
373 m_albumArt.m_filename = covertArtFileName;
375 }
376 m_discId = discId;
377
378 return true;
379#else
380 return false;
381#endif
382}
383
384bool MusicBrainz::hasMetadata(int track) const
385{
386 return m_tracks.contains(track);
387}
388
390{
391 auto it = m_tracks.find(track);
392 if (it == m_tracks.end())
393 {
394 LOG(VB_MEDIA, LOG_ERR, QString("musicbrainz: No metadata for track %1").arg(track));
395 return nullptr;
396 }
397 auto *metadata = new MusicMetadata(it.value());
398 if (!m_albumArt.m_filename.isEmpty())
399 {
401 }
402 return metadata;
403}
404
406{
407 LOG(VB_MEDIA, LOG_DEBUG, "musicbrainz: Reset metadata");
408 m_tracks.clear();
410}
411
QString m_filename
Definition: musicmetadata.h:55
ImageType m_imageType
Definition: musicmetadata.h:57
void addImage(const AlbumArtImage *newImage)
MusicMetadata * getMetadata(int track) const
Creates and return metadata for specified track.
QMap< int, MusicMetadata > m_tracks
Definition: musicbrainz.h:66
AlbumArtImage m_albumArt
Definition: musicbrainz.h:67
bool hasMetadata(int track) const
Checks if metadata for given track exists.
bool queryForDevice(const QString &deviceName)
Query music metadata using disc id of specified device.
void reset()
Reset last queried metadata.
void setCompilationFlag(bool isCompilation)
Sets compilation flag for all metadata.
void setYear(int lyear)
void setCompilationArtist(const QString &lcompilation_artist, const QString &lcompilation_artist_sort=nullptr)
QString CompilationArtist() const
void setCompilation(bool state)
void setLength(T llength)
void setTitle(const QString &ltitle, const QString &ltitle_sort=nullptr)
QString Artist() const
void setAlbum(const QString &lalbum, const QString &lalbum_sort=nullptr)
void setTrack(int ltrack)
AlbumArtImages * getAlbumArtImages(void)
void setArtist(const QString &lartist, const QString &lartist_sort=nullptr)
@ IT_FRONTCOVER
Definition: musicmetadata.h:37
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
@ ISODate
Default UTC.
Definition: mythdate.h:17
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
def error(message)
Definition: smolt.py:409
bool exists(str path)
Definition: xbmcvfs.py:51