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 
37 constexpr auto user_agent = "mythtv";
38 
39 std::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 
59 static 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 
90 static 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 
101 static 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 
108 std::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 
255 void 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 
264 static 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 
271 QString 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 
346 bool 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 
384 bool MusicBrainz::hasMetadata(int track) const
385 {
386  return m_tracks.find(track) != m_tracks.end();
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  {
400  metadata->getAlbumArtImages()->addImage(&m_albumArt);
401  }
402  return metadata;
403 }
404 
406 {
407  LOG(VB_MEDIA, LOG_DEBUG, "musicbrainz: Reset metadata");
408  m_tracks.clear();
410 }
411 
musicbrainz.h
AlbumArtImage::m_imageType
ImageType m_imageType
Definition: musicmetadata.h:51
hardwareprofile.smolt.user_agent
string user_agent
Definition: smolt.py:100
MusicMetadata::getAlbumArtImages
AlbumArtImages * getAlbumArtImages(void)
Definition: musicmetadata.cpp:1383
MusicMetadata::CompilationArtist
QString CompilationArtist() const
Definition: musicmetadata.h:137
MusicMetadata::setTrack
void setTrack(int ltrack)
Definition: musicmetadata.h:199
xbmcvfs.exists
bool exists(str path)
Definition: xbmcvfs.py:51
MusicMetadata::setAlbum
void setAlbum(const QString &lalbum, const QString &lalbum_sort=nullptr)
Definition: musicmetadata.h:151
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MusicBrainz::queryForDevice
bool queryForDevice(const QString &deviceName)
Query music metadata using disc id of specified device.
Definition: musicbrainz.cpp:346
MusicMetadata
Definition: musicmetadata.h:80
IT_FRONTCOVER
@ IT_FRONTCOVER
Definition: musicmetadata.h:31
MusicMetadata::Artist
QString Artist() const
Definition: musicmetadata.h:125
MusicBrainz::getMetadata
MusicMetadata * getMetadata(int track) const
Creates and return metadata for specified track.
Definition: musicbrainz.cpp:389
MusicBrainz::hasMetadata
bool hasMetadata(int track) const
Checks if metadata for given track exists.
Definition: musicbrainz.cpp:384
mythlogging.h
MusicBrainz::setCompilationFlag
void setCompilationFlag(bool isCompilation)
Sets compilation flag for all metadata.
MusicMetadata::setLength
void setLength(T llength)
Definition: musicmetadata.h:206
hardwareprofile.i18n.t
t
Definition: i18n.py:36
MusicBrainz::reset
void reset()
Reset last queried metadata.
Definition: musicbrainz.cpp:405
MusicMetadata::setTitle
void setTitle(const QString &ltitle, const QString &ltitle_sort=nullptr)
Definition: musicmetadata.h:163
MusicBrainz::m_albumArt
AlbumArtImage m_albumArt
Definition: musicbrainz.h:67
hardwareprofile.smolt.error
def error(message)
Definition: smolt.py:409
MythDate::fromString
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
MusicMetadata::setCompilationArtist
void setCompilationArtist(const QString &lcompilation_artist, const QString &lcompilation_artist_sort=nullptr)
Definition: musicmetadata.h:139
MusicMetadata::setArtist
void setArtist(const QString &lartist, const QString &lartist_sort=nullptr)
Definition: musicmetadata.h:127
mythmiscutil.h
MythDate::ISODate
@ ISODate
Default UTC.
Definition: mythdate.h:17
AlbumArtImage
Definition: musicmetadata.h:39
MusicMetadata::setYear
void setYear(int lyear)
Definition: musicmetadata.h:196
MusicMetadata::setCompilation
void setCompilation(bool state)
Definition: musicmetadata.h:252
MusicBrainz::m_tracks
QMap< int, MusicMetadata > m_tracks
Definition: musicbrainz.h:66
AlbumArtImages::addImage
void addImage(const AlbumArtImage *newImage)
Definition: musicmetadata.cpp:2252
AlbumArtImage::m_filename
QString m_filename
Definition: musicmetadata.h:49