1 /**
2  * \file musicbrainzimporter.cpp
3  * MusicBrainz release database importer.
4  *
5  * \b Project: Kid3
6  * \author Urs Fleisch
7  * \date 13 Oct 2006
8  *
9  * Copyright (C) 2006-2018  Urs Fleisch
10  *
11  * This file is part of Kid3.
12  *
13  * Kid3 is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * Kid3 is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "musicbrainzimporter.h"
28 #include <QDomDocument>
29 #include <QUrl>
30 #include <QRegularExpression>
31 #include "serverimporterconfig.h"
32 #include "trackdatamodel.h"
33 #include "musicbrainzconfig.h"
34 #include "genres.h"
35 
36 /**
37  * Constructor.
38  *
39  * @param netMgr network access manager
40  * @param trackDataModel track data to be filled with imported values
41  */
MusicBrainzImporter(QNetworkAccessManager * netMgr,TrackDataModel * trackDataModel)42 MusicBrainzImporter::MusicBrainzImporter(
43   QNetworkAccessManager* netMgr, TrackDataModel *trackDataModel)
44   : ServerImporter(netMgr, trackDataModel)
45 {
46   setObjectName(QLatin1String("MusicBrainzImporter"));
47   m_headers["User-Agent"] = "curl/7.52.1";
48 }
49 
50 /**
51  * Name of import source.
52  * @return name.
53  */
name() const54 const char* MusicBrainzImporter::name() const {
55   return QT_TRANSLATE_NOOP("@default", "MusicBrainz Release");
56 }
57 
58 /** NULL-terminated array of server strings, 0 if not used */
serverList() const59 const char** MusicBrainzImporter::serverList() const
60 {
61   return nullptr;
62 }
63 
64 /** default server, 0 to disable */
defaultServer() const65 const char* MusicBrainzImporter::defaultServer() const {
66   return nullptr;
67 }
68 
69 /** anchor to online help, 0 to disable */
helpAnchor() const70 const char* MusicBrainzImporter::helpAnchor() const {
71   return "import-musicbrainzrelease";
72 }
73 
74 /** configuration, 0 if not used */
config() const75 ServerImporterConfig* MusicBrainzImporter::config() const {
76   return &MusicBrainzConfig::instance();
77 }
78 
79 /** additional tags option, false if not used */
additionalTags() const80 bool MusicBrainzImporter::additionalTags() const { return true; }
81 
82 /**
83  * Process finished findCddbAlbum request.
84  *
85  * @param searchStr search data received
86  */
parseFindResults(const QByteArray & searchStr)87 void MusicBrainzImporter::parseFindResults(const QByteArray& searchStr)
88 {
89   /* simplified XML result:
90 <metadata>
91   <release-list offset="0" count="3">
92     <release ext:score="100" id="978c7ed1-a854-4ef2-bd4e-e7c1317be854">
93       <title>Odin</title>
94       <artist-credit>
95         <name-credit>
96           <artist id="d1075cad-33e3-496b-91b0-d4670aabf4f8">
97             <name>Wizard</name>
98             <sort-name>Wizard</sort-name>
99           </artist>
100         </name-credit>
101       </artist-credit>
102     </release>
103   */
104   int start = searchStr.indexOf("<?xml");
105   int end = searchStr.indexOf("</metadata>");
106   QByteArray xmlStr = searchStr;
107   if (start >= 0 && end > start) {
108     xmlStr = xmlStr.mid(start, end + 11 - start);
109   }
110   QDomDocument doc;
111   if (doc.setContent(xmlStr, false)) {
112     m_albumListModel->clear();
113     QDomElement releaseList =
114       doc.namedItem(QLatin1String("metadata")).toElement()
115          .namedItem(QLatin1String("release-list")).toElement();
116     for (QDomNode releaseNode = releaseList.namedItem(QLatin1String("release"));
117          !releaseNode.isNull();
118          releaseNode = releaseNode.nextSibling()) {
119       QDomElement release = releaseNode.toElement();
120       QString id = release.attribute(QLatin1String("id"));
121       QString title = release.namedItem(QLatin1String("title")).toElement()
122           .text();
123       QDomElement artist = release.namedItem(QLatin1String("artist-credit"))
124           .toElement().namedItem(QLatin1String("name-credit")).toElement()
125           .namedItem(QLatin1String("artist")).toElement();
126       QString name = artist.namedItem(QLatin1String("name")).toElement().text();
127       m_albumListModel->appendItem(
128         name + QLatin1String(" - ") + title,
129         QLatin1String("release"),
130         id);
131     }
132   }
133 }
134 
135 namespace {
136 
137 /**
138  * Uppercase the first characters of each word in a string.
139  *
140  * @param str string with words to uppercase
141  *
142  * @return string with first letters in uppercase.
143  */
upperCaseFirstLetters(const QString & str)144 QString upperCaseFirstLetters(const QString& str)
145 {
146   QString result(str);
147   int len = result.length();
148   int pos = 0;
149   while (pos < len) {
150     result[pos] = result.at(pos).toUpper();
151     pos = result.indexOf(QLatin1Char(' '), pos);
152     if (pos++ == -1) {
153       break;
154     }
155   }
156   return result;
157 }
158 
159 /**
160  * Add involved people to a frame.
161  * The format used is (should be converted according to tag specifications):
162  * involvee 1 (involvement 1)\n
163  * involvee 2 (involvement 2)\n
164  * ...
165  * involvee n (involvement n)
166  *
167  * @param frames      frame collection
168  * @param type        type of frame
169  * @param involvement involvement (e.g. instrument)
170  * @param involvee    name of involvee (e.g. musician)
171  */
addInvolvedPeople(FrameCollection & frames,Frame::Type type,const QString & involvement,const QString & involvee)172 void addInvolvedPeople(
173   FrameCollection& frames, Frame::Type type,
174   const QString& involvement, const QString& involvee)
175 {
176   QString value = frames.getValue(type);
177   if (!value.isEmpty()) value += Frame::stringListSeparator();
178   value += upperCaseFirstLetters(involvement);
179   value += Frame::stringListSeparator();
180   value += involvee;
181   frames.setValue(type, value);
182 }
183 
184 /**
185  * Set tags from an XML node with a relation list.
186  *
187  * @param relationList relation-list with target-type Artist
188  * @param frames       tags will be added to these frames
189  *
190  * @return true if credits found.
191  */
parseCredits(const QDomElement & relationList,FrameCollection & frames)192 bool parseCredits(const QDomElement& relationList, FrameCollection& frames)
193 {
194   bool result = false;
195   QDomNode relation(relationList.firstChild());
196   while (!relation.isNull()) {
197     QString artist(relation.toElement().namedItem(QLatin1String("artist"))
198                            .toElement().namedItem(QLatin1String("name"))
199                            .toElement().text());
200     if (!artist.isEmpty()) {
201       QString type(relation.toElement().attribute(QLatin1String("type")));
202       if (type == QLatin1String("instrument")) {
203         QDomNode attributeList(relation.toElement()
204                                .namedItem(QLatin1String("attribute-list")));
205         if (!attributeList.isNull()) {
206           addInvolvedPeople(frames, Frame::FT_Performer,
207             attributeList.firstChild().toElement().text(), artist);
208         }
209       } else if (type == QLatin1String("vocal")) {
210         addInvolvedPeople(frames, Frame::FT_Performer, type, artist);
211       } else {
212         static const struct {
213           const char* credit;
214           Frame::Type type;
215         } creditToType[] = {
216           { "composer", Frame::FT_Composer },
217           { "conductor", Frame::FT_Conductor },
218           { "performing orchestra", Frame::FT_AlbumArtist },
219           { "lyricist", Frame::FT_Lyricist },
220           { "publisher", Frame::FT_Publisher },
221           { "remixer", Frame::FT_Remixer }
222         };
223         bool found = false;
224         for (const auto& c2t : creditToType) {
225           if (type == QString::fromLatin1(c2t.credit)) {
226             frames.setValue(c2t.type, artist);
227             found = true;
228             break;
229           }
230         }
231         if (!found && type != QLatin1String("tribute")) {
232           addInvolvedPeople(frames, Frame::FT_Arranger, type, artist);
233         }
234       }
235     }
236     result = true;
237     relation = relation.nextSibling();
238   }
239   return result;
240 }
241 
242 /**
243  * Transform the lower case genres returned by MusicBrainz to match the
244  * standard genre names.
245  * @param genre lower case genre
246  * @return capitalized canonical genre.
247  */
fixUpGenre(QString genre)248 QString fixUpGenre(QString genre)
249 {
250   if (genre.isEmpty()) {
251     return genre;
252   }
253   for (int i = 0; i < genre.length(); ++i) {
254     if (i == 0 || genre.at(i - 1) == QLatin1Char('-') ||
255         genre.at(i - 1) == QLatin1Char(' ') ||
256         genre.at(i - 1) == QLatin1Char('&')) {
257       genre[i] = genre[i].toUpper();
258     }
259   }
260   genre.replace(QLatin1String(" And "), QLatin1String(" & "))
261        .replace(QLatin1String("Ebm"), QLatin1String("EBM"))
262        .replace(QLatin1String("Edm"), QLatin1String("EDM"))
263        .replace(QLatin1String("Idm"), QLatin1String("IDM"))
264        .replace(QLatin1String("Uk"), QLatin1String("UK"));
265   return genre;
266 }
267 
268 /**
269  * Get genres from an XML node with a genre-list.
270  * @param element XML node which could have a genre-list
271  * @return genres separated by frame string list separator, null if not found.
272  */
parseGenres(const QDomElement & element)273 QString parseGenres(const QDomElement& element)
274 {
275   QDomNode genreList =
276       element.namedItem(QLatin1String("genre-list"));
277   if (!genreList.isNull()) {
278     QStringList genres, customGenres;
279     for (QDomNode genreNode = genreList.namedItem(QLatin1String("genre"));
280          !genreNode.isNull();
281          genreNode = genreNode.nextSibling()) {
282       if (!genreNode.isNull()) {
283         QString genre = fixUpGenre(genreNode.toElement()
284             .namedItem(QLatin1String("name")).toElement().text());
285         if (!genre.isEmpty()) {
286           int genreNum = Genres::getNumber(genre);
287           if (genreNum != 255) {
288             genres.append(QString::fromLatin1(Genres::getName(genreNum)));
289           } else {
290             customGenres.append(genre);
291           }
292         }
293       }
294     }
295     genres.append(customGenres);
296     return genres.join(Frame::stringListSeparator());
297   }
298   return QString();
299 }
300 
301 }
302 
303 /**
304  * Parse result of album request and populate m_trackDataModel with results.
305  *
306  * @param albumStr album data received
307  */
parseAlbumResults(const QByteArray & albumStr)308 void MusicBrainzImporter::parseAlbumResults(const QByteArray& albumStr)
309 {
310   /*
311 <metadata>
312   <release id="978c7ed1-a854-4ef2-bd4e-e7c1317be854">
313     <title>Odin</title>
314     <artist-credit>
315       <name-credit>
316         <artist id="d1075cad-33e3-496b-91b0-d4670aabf4f8">
317           <name>Wizard</name>
318           <sort-name>Wizard</sort-name>
319         </artist>
320       </name-credit>
321     </artist-credit>
322     <date>2003-08-19</date>
323     <asin>B00008OUEN</asin>
324     <medium-list count="1">
325       <medium>
326         <position>1</position>
327         <track-list count="11" offset="0">
328           <track>
329             <position>1</position>
330             <recording id="dac7c002-432f-4dcb-ad57-5ebde8e258b0">
331               <title>The Prophecy</title>
332               <length>319173</length>
333             </recording>
334   */
335   int start = albumStr.indexOf("<?xml");
336   int end = albumStr.indexOf("</metadata>");
337   QByteArray xmlStr = start >= 0 && end > start ?
338     albumStr.mid(start, end + 11 - start) : albumStr;
339   QDomDocument doc;
340   if (doc.setContent(xmlStr, false)) {
341     QDomElement release =
342       doc.namedItem(QLatin1String("metadata")).toElement()
343          .namedItem(QLatin1String("release")).toElement();
344     FrameCollection framesHdr;
345     const bool standardTags = getStandardTags();
346     if (standardTags) {
347       framesHdr.setAlbum(release.namedItem(QLatin1String("title")).toElement()
348                          .text());
349       QDomElement artist = release.namedItem(QLatin1String("artist-credit"))
350           .toElement().namedItem(QLatin1String("name-credit"))
351           .toElement().namedItem(QLatin1String("artist"))
352           .toElement();
353       framesHdr.setArtist(artist.namedItem(QLatin1String("name"))
354                           .toElement().text());
355       QString genre = parseGenres(artist);
356       if (!genre.isEmpty()) {
357         framesHdr.setGenre(genre);
358       }
359       QString date(release.namedItem(QLatin1String("date")).toElement().text());
360       if (!date.isEmpty()) {
361         QRegularExpression dateRe(QLatin1String(R"(^(\d{4})(?:-\d{2})?(?:-\d{2})?$)"));
362         int year = 0;
363         auto match = dateRe.match(date);
364         if (match.hasMatch()) {
365           year = match.captured(1).toInt();
366         } else {
367           year = date.toInt();
368         }
369         if (year != 0) {
370           framesHdr.setYear(year);
371         }
372       }
373     }
374 
375     ImportTrackDataVector trackDataVector(m_trackDataModel->getTrackData());
376     trackDataVector.setCoverArtUrl(QUrl());
377     const bool coverArt = getCoverArt();
378     if (coverArt) {
379       QString asin(release.namedItem(QLatin1String("asin")).toElement().text());
380       if (!asin.isEmpty()) {
381         trackDataVector.setCoverArtUrl(
382           QUrl(QLatin1String("http://www.amazon.com/dp/") + asin));
383       }
384     }
385 
386     const bool additionalTags = getAdditionalTags();
387     if (additionalTags) {
388       // label can be found in the label-info-list
389       QDomElement labelInfoList(
390             release.namedItem(QLatin1String("label-info-list")).toElement());
391       if (!labelInfoList.isNull()) {
392         QDomElement labelInfo(
393               labelInfoList.namedItem(QLatin1String("label-info")).toElement());
394         if (!labelInfo.isNull()) {
395           QString label(labelInfo.namedItem(QLatin1String("label"))
396                         .namedItem(QLatin1String("name")).toElement().text());
397           if (!label.isEmpty()) {
398             framesHdr.setValue(Frame::FT_Publisher, label);
399           }
400           QString catNo(labelInfo.namedItem(QLatin1String("catalog-number"))
401                         .toElement().text());
402           if (!catNo.isEmpty()) {
403             framesHdr.setValue(Frame::FT_CatalogNumber, catNo);
404           }
405         }
406       }
407       // Release country can be found in "country"
408       QString country(release.namedItem(QLatin1String("country")).toElement()
409                       .text());
410       if (!country.isEmpty()) {
411         framesHdr.setValue(Frame::FT_ReleaseCountry, country);
412       }
413     }
414 
415     if (additionalTags || coverArt) {
416       QDomNode relationListNode(release.firstChild());
417       while (!relationListNode.isNull()) {
418         if (relationListNode.nodeName() == QLatin1String("relation-list")) {
419           QDomElement relationList(relationListNode.toElement());
420           if (!relationList.isNull()) {
421             QString targetType(relationList.attribute(QLatin1String("target-type")));
422             if (targetType == QLatin1String("artist")) {
423               if (additionalTags) {
424                 parseCredits(relationList, framesHdr);
425               }
426             } else if (targetType == QLatin1String("url")) {
427               if (coverArt) {
428                 QDomNode relationNode(relationList.firstChild());
429                 while (!relationNode.isNull()) {
430                   if (relationNode.nodeName() == QLatin1String("relation")) {
431                     QDomElement relation(relationNode.toElement());
432                     if (!relation.isNull()) {
433                       QString type(relation.attribute(QLatin1String("type")));
434                       if (type == QLatin1String("cover art link") ||
435                           type == QLatin1String("amazon asin")) {
436                         QString coverArtUrl =
437                             relation.namedItem(QLatin1String("target"))
438                             .toElement().text();
439                         // https://www.amazon.de/gp/product/ does not work,
440                         // fix such links.
441                         coverArtUrl.replace(
442                             QRegularExpression(QLatin1String(
443                                   "https://www\\.amazon\\.[^/]+/gp/product/")),
444                             QLatin1String("http://images.amazon.com/images/P/"));
445                         if (!coverArtUrl.endsWith(QLatin1String(".jpg"))) {
446                           coverArtUrl += QLatin1String(".jpg");
447                         }
448                         trackDataVector.setCoverArtUrl(
449                           QUrl(coverArtUrl));
450                       }
451                     }
452                   }
453                   relationNode = relationNode.nextSibling();
454                 }
455               }
456             }
457           }
458         }
459         relationListNode = relationListNode.nextSibling();
460       }
461     }
462 
463     auto it = trackDataVector.begin();
464     bool atTrackDataListEnd = (it == trackDataVector.end());
465     int discNr = 1, trackNr = 1;
466     bool ok;
467     FrameCollection frames(framesHdr);
468     QDomElement mediumList = release.namedItem(QLatin1String("medium-list"))
469         .toElement();
470     int mediumCount = mediumList.attribute(QLatin1String("count")).toInt();
471     for (QDomNode mediumNode = mediumList.namedItem(QLatin1String("medium"));
472          !mediumNode.isNull();
473          mediumNode = mediumNode.nextSibling()) {
474       int position = mediumNode.namedItem(QLatin1String("position"))
475           .toElement().text().toInt(&ok);
476       if (ok) {
477         discNr = position;
478       }
479       QDomElement trackList = mediumNode.namedItem(QLatin1String("track-list"))
480           .toElement();
481       for (QDomNode trackNode = trackList.namedItem(QLatin1String("track"));
482            !trackNode.isNull();
483            trackNode = trackNode.nextSibling()) {
484         if (mediumCount > 1 && additionalTags) {
485           frames.setValue(Frame::FT_Disc, QString::number(discNr));
486         }
487         QDomElement track = trackNode.toElement();
488         position = track.namedItem(QLatin1String("position")).toElement()
489             .text().toInt(&ok);
490         if (ok) {
491           trackNr = position;
492         }
493         if (standardTags) {
494           frames.setTrack(trackNr);
495         }
496         int duration = track.namedItem(QLatin1String("length")).toElement()
497             .text().toInt();
498         QDomElement recording = track.namedItem(QLatin1String("recording"))
499             .toElement();
500         if (!recording.isNull()) {
501           if (standardTags) {
502             frames.setTitle(recording.namedItem(QLatin1String("title"))
503                             .toElement().text());
504           }
505           int length = recording.namedItem(QLatin1String("length"))
506               .toElement().text().toInt(&ok);
507           if (ok) {
508             duration = length;
509           }
510           QDomNode artistNode =
511               recording.namedItem(QLatin1String("artist-credit"));
512           if (!artistNode.isNull()) {
513             QDomElement artistElement = artistNode.toElement()
514                 .namedItem(QLatin1String("name-credit")).toElement()
515                 .namedItem(QLatin1String("artist")).toElement();
516             QString artist = artistElement
517                 .namedItem(QLatin1String("name")).toElement().text();
518             if (!artist.isEmpty()) {
519               // use the artist in the header as the album artist
520               // and the artist in the track as the artist
521               if (standardTags) {
522                 frames.setArtist(artist);
523               }
524               if (additionalTags) {
525                 frames.setValue(Frame::FT_AlbumArtist, framesHdr.getArtist());
526               }
527             }
528             QString genre = parseGenres(artistElement);
529             if (!genre.isEmpty()) {
530               frames.setGenre(genre);
531             }
532           }
533           QString genre = parseGenres(recording);
534           if (!genre.isEmpty()) {
535             frames.setGenre(genre);
536           }
537           if (additionalTags) {
538             QDomNode relationListNode(recording.firstChild());
539             while (!relationListNode.isNull()) {
540               if (relationListNode.nodeName() == QLatin1String("relation-list")) {
541                 QDomElement relationList(relationListNode.toElement());
542                 if (!relationList.isNull()) {
543                   QString targetType(
544                         relationList.attribute(QLatin1String("target-type")));
545                   if (targetType == QLatin1String("artist")) {
546                     parseCredits(relationList, frames);
547                   } else if (targetType == QLatin1String("work")) {
548                     QDomNode workRelationListNode(relationList
549                           .namedItem(QLatin1String("relation"))
550                           .namedItem(QLatin1String("work"))
551                           .namedItem(QLatin1String("relation-list")));
552                     if (!workRelationListNode.isNull()) {
553                       parseCredits(workRelationListNode.toElement(), frames);
554                     }
555                   }
556                 }
557               }
558               relationListNode = relationListNode.nextSibling();
559             }
560           }
561         }
562         duration /= 1000;
563         if (atTrackDataListEnd) {
564           ImportTrackData trackData;
565           trackData.setFrameCollection(frames);
566           trackData.setImportDuration(duration);
567           trackDataVector.push_back(trackData);
568         } else {
569           while (!atTrackDataListEnd && !it->isEnabled()) {
570             ++it;
571             atTrackDataListEnd = (it == trackDataVector.end());
572           }
573           if (!atTrackDataListEnd) {
574             (*it).setFrameCollection(frames);
575             (*it).setImportDuration(duration);
576             ++it;
577             atTrackDataListEnd = (it == trackDataVector.end());
578           }
579         }
580         ++trackNr;
581         frames = framesHdr;
582       }
583       ++discNr;
584     }
585     // handle redundant tracks
586     frames.clear();
587     while (!atTrackDataListEnd) {
588       if (it->isEnabled()) {
589         if ((*it).getFileDuration() == 0) {
590           it = trackDataVector.erase(it);
591         } else {
592           (*it).setFrameCollection(frames);
593           (*it).setImportDuration(0);
594           ++it;
595         }
596       } else {
597         ++it;
598       }
599       atTrackDataListEnd = (it == trackDataVector.end());
600     }
601     m_trackDataModel->setTrackData(trackDataVector);
602   }
603 }
604 
605 /**
606  * Send a query command to search on the server.
607  *
608  * @param cfg      import source configuration
609  * @param artist   artist to search
610  * @param album    album to search
611  */
sendFindQuery(const ServerImporterConfig * cfg,const QString & artist,const QString & album)612 void MusicBrainzImporter::sendFindQuery(
613   const ServerImporterConfig* cfg,
614   const QString& artist, const QString& album)
615 {
616   Q_UNUSED(cfg)
617   /*
618    * Query looks like this:
619    * http://musicbrainz.org/ws/2/release?query=artist:wizard%20AND%20release:odin
620    */
621   QString path(QLatin1String("/ws/2/release?query="));
622   if (!artist.isEmpty()) {
623     QString artistQuery(artist.contains(QLatin1Char(' '))
624                         ? QLatin1Char('"') + artist + QLatin1Char('"')
625                         : artist);
626     if (!album.isEmpty()) {
627       artistQuery += QLatin1String(" AND ");
628     }
629     path += QLatin1String("artist:");
630     path += QString::fromLatin1(QUrl::toPercentEncoding(artistQuery));
631   }
632   if (!album.isEmpty()) {
633     QString albumQuery(album.contains(QLatin1Char(' '))
634                         ? QLatin1Char('"') + album + QLatin1Char('"')
635                         : album);
636     path += QLatin1String("release:");
637     path += QString::fromLatin1(QUrl::toPercentEncoding(albumQuery));
638   }
639   sendRequest(QLatin1String("musicbrainz.org"), path, QLatin1String("https"),
640               m_headers);
641 }
642 
643 /**
644  * Send a query command to fetch the track list
645  * from the server.
646  *
647  * @param cfg      import source configuration
648  * @param cat      category
649  * @param id       ID
650  */
sendTrackListQuery(const ServerImporterConfig * cfg,const QString & cat,const QString & id)651 void MusicBrainzImporter::sendTrackListQuery(
652   const ServerImporterConfig* cfg, const QString& cat, const QString& id)
653 {
654   /*
655    * Query looks like this:
656    * http://musicbrainz.org/ws/2/release/978c7ed1-a854-4ef2-bd4e-e7c1317be854?inc=artists+recordings
657    */
658   QString path(QLatin1String("/ws/2/"));
659   path += cat;
660   path += QLatin1Char('/');
661   path += id;
662   path += QLatin1String("?inc=");
663   if (cfg->additionalTags()) {
664     path += QLatin1String("artist-credits+labels+recordings+genres+media+isrcs+"
665                 "discids+artist-rels+label-rels+recording-rels+release-rels");
666   } else {
667     path += QLatin1String("artists+recordings+genres");
668   }
669   if (cfg->coverArt()) {
670     path += QLatin1String("+url-rels");
671   }
672   if (cfg->additionalTags()) {
673     path += QLatin1String("+work-rels+recording-level-rels+work-level-rels");
674   }
675   sendRequest(QLatin1String("musicbrainz.org"), path, QLatin1String("https"),
676               m_headers);
677 }
678