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