File indexing completed on 2024-05-19 04:56:30

0001 /**
0002  * \file musicbrainzimporter.cpp
0003  * MusicBrainz release database importer.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 13 Oct 2006
0008  *
0009  * Copyright (C) 2006-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "musicbrainzimporter.h"
0028 #include <QDomDocument>
0029 #include <QUrl>
0030 #include <QRegularExpression>
0031 #include "serverimporterconfig.h"
0032 #include "trackdatamodel.h"
0033 #include "musicbrainzconfig.h"
0034 #include "genres.h"
0035 
0036 /**
0037  * Constructor.
0038  *
0039  * @param netMgr network access manager
0040  * @param trackDataModel track data to be filled with imported values
0041  */
0042 MusicBrainzImporter::MusicBrainzImporter(
0043   QNetworkAccessManager* netMgr, TrackDataModel *trackDataModel)
0044   : ServerImporter(netMgr, trackDataModel)
0045 {
0046   setObjectName(QLatin1String("MusicBrainzImporter"));
0047   m_headers["User-Agent"] = "curl/7.52.1";
0048 }
0049 
0050 /**
0051  * Name of import source.
0052  * @return name.
0053  */
0054 const char* MusicBrainzImporter::name() const {
0055   return QT_TRANSLATE_NOOP("@default", "MusicBrainz Release");
0056 }
0057 
0058 /** NULL-terminated array of server strings, 0 if not used */
0059 const char** MusicBrainzImporter::serverList() const
0060 {
0061   return nullptr;
0062 }
0063 
0064 /** default server, 0 to disable */
0065 const char* MusicBrainzImporter::defaultServer() const {
0066   return nullptr;
0067 }
0068 
0069 /** anchor to online help, 0 to disable */
0070 const char* MusicBrainzImporter::helpAnchor() const {
0071   return "import-musicbrainzrelease";
0072 }
0073 
0074 /** configuration, 0 if not used */
0075 ServerImporterConfig* MusicBrainzImporter::config() const {
0076   return &MusicBrainzConfig::instance();
0077 }
0078 
0079 /** additional tags option, false if not used */
0080 bool MusicBrainzImporter::additionalTags() const { return true; }
0081 
0082 /**
0083  * Process finished findCddbAlbum request.
0084  *
0085  * @param searchStr search data received
0086  */
0087 void MusicBrainzImporter::parseFindResults(const QByteArray& searchStr)
0088 {
0089   /* simplified XML result:
0090 <metadata>
0091   <release-list offset="0" count="3">
0092     <release ext:score="100" id="978c7ed1-a854-4ef2-bd4e-e7c1317be854">
0093       <title>Odin</title>
0094       <artist-credit>
0095         <name-credit>
0096           <artist id="d1075cad-33e3-496b-91b0-d4670aabf4f8">
0097             <name>Wizard</name>
0098             <sort-name>Wizard</sort-name>
0099           </artist>
0100         </name-credit>
0101       </artist-credit>
0102     </release>
0103   */
0104   int start = searchStr.indexOf("<?xml");
0105   int end = searchStr.indexOf("</metadata>");
0106   QByteArray xmlStr = searchStr;
0107   if (start >= 0 && end > start) {
0108     xmlStr = xmlStr.mid(start, end + 11 - start);
0109   }
0110   if (QDomDocument doc; doc.setContent(xmlStr, false)) {
0111     m_albumListModel->clear();
0112     QDomElement releaseList =
0113       doc.namedItem(QLatin1String("metadata")).toElement()
0114          .namedItem(QLatin1String("release-list")).toElement();
0115     for (QDomNode releaseNode = releaseList.namedItem(QLatin1String("release"));
0116          !releaseNode.isNull();
0117          releaseNode = releaseNode.nextSibling()) {
0118       QDomElement release = releaseNode.toElement();
0119       QString id = release.attribute(QLatin1String("id"));
0120       QString title = release.namedItem(QLatin1String("title")).toElement()
0121           .text();
0122       QDomElement artist = release.namedItem(QLatin1String("artist-credit"))
0123           .toElement().namedItem(QLatin1String("name-credit")).toElement()
0124           .namedItem(QLatin1String("artist")).toElement();
0125       QString name = artist.namedItem(QLatin1String("name")).toElement().text();
0126       m_albumListModel->appendItem(
0127         name + QLatin1String(" - ") + title,
0128         QLatin1String("release"),
0129         id);
0130     }
0131   }
0132 }
0133 
0134 namespace {
0135 
0136 /**
0137  * Uppercase the first characters of each word in a string.
0138  *
0139  * @param str string with words to uppercase
0140  *
0141  * @return string with first letters in uppercase.
0142  */
0143 QString upperCaseFirstLetters(const QString& str)
0144 {
0145   QString result(str);
0146   int len = result.length();
0147   int pos = 0;
0148   while (pos < len) {
0149     result[pos] = result.at(pos).toUpper();
0150     pos = result.indexOf(QLatin1Char(' '), pos);
0151     if (pos++ == -1) {
0152       break;
0153     }
0154   }
0155   return result;
0156 }
0157 
0158 /**
0159  * Add involved people to a frame.
0160  * The format used is (should be converted according to tag specifications):
0161  * involvee 1 (involvement 1)\n
0162  * involvee 2 (involvement 2)\n
0163  * ...
0164  * involvee n (involvement n)
0165  *
0166  * @param frames      frame collection
0167  * @param type        type of frame
0168  * @param involvement involvement (e.g. instrument)
0169  * @param involvee    name of involvee (e.g. musician)
0170  */
0171 void addInvolvedPeople(
0172   FrameCollection& frames, Frame::Type type,
0173   const QString& involvement, const QString& involvee)
0174 {
0175   QString value = frames.getValue(type);
0176   if (!value.isEmpty()) value += Frame::stringListSeparator();
0177   value += Frame::joinStringList({upperCaseFirstLetters(involvement), involvee});
0178   frames.setValue(type, value);
0179 }
0180 
0181 /**
0182  * Set tags from an XML node with a relation list.
0183  *
0184  * @param relationList relation-list with target-type Artist
0185  * @param frames       tags will be added to these frames
0186  *
0187  * @return true if credits found.
0188  */
0189 bool parseCredits(const QDomElement& relationList, FrameCollection& frames)
0190 {
0191   bool result = false;
0192   QDomNode relation(relationList.firstChild());
0193   while (!relation.isNull()) {
0194     if (QString artist(relation.toElement().namedItem(QLatin1String("artist"))
0195                                .toElement().namedItem(QLatin1String("name"))
0196                                .toElement().text());
0197         !artist.isEmpty()) {
0198       if (QString type(relation.toElement().attribute(QLatin1String("type")));
0199           type == QLatin1String("instrument")) {
0200         if (QDomNode attributeList(relation.toElement()
0201                                            .namedItem(QLatin1String("attribute-list")));
0202             !attributeList.isNull()) {
0203           addInvolvedPeople(frames, Frame::FT_Performer,
0204             attributeList.firstChild().toElement().text(), artist);
0205         }
0206       } else if (type == QLatin1String("vocal")) {
0207         addInvolvedPeople(frames, Frame::FT_Performer, type, artist);
0208       } else {
0209         static const struct {
0210           const char* credit;
0211           Frame::Type type;
0212         } creditToType[] = {
0213           { "composer", Frame::FT_Composer },
0214           { "conductor", Frame::FT_Conductor },
0215           { "performing orchestra", Frame::FT_AlbumArtist },
0216           { "lyricist", Frame::FT_Lyricist },
0217           { "publisher", Frame::FT_Publisher },
0218           { "remixer", Frame::FT_Remixer }
0219         };
0220         bool found = false;
0221         for (const auto& c2t : creditToType) {
0222           if (type == QString::fromLatin1(c2t.credit)) {
0223             frames.setValue(c2t.type, artist);
0224             found = true;
0225             break;
0226           }
0227         }
0228         if (!found && type != QLatin1String("tribute")) {
0229           addInvolvedPeople(frames, Frame::FT_Arranger, type, artist);
0230         }
0231       }
0232     }
0233     result = true;
0234     relation = relation.nextSibling();
0235   }
0236   return result;
0237 }
0238 
0239 /**
0240  * Transform the lower case genres returned by MusicBrainz to match the
0241  * standard genre names.
0242  * @param genre lower case genre
0243  * @return capitalized canonical genre.
0244  */
0245 QString fixUpGenre(QString genre)
0246 {
0247   if (genre.isEmpty()) {
0248     return genre;
0249   }
0250   for (int i = 0; i < genre.length(); ++i) {
0251     if (i == 0 || genre.at(i - 1) == QLatin1Char('-') ||
0252         genre.at(i - 1) == QLatin1Char(' ') ||
0253         genre.at(i - 1) == QLatin1Char('&')) {
0254       genre[i] = genre[i].toUpper();
0255     }
0256   }
0257   genre.replace(QLatin1String(" And "), QLatin1String(" & "))
0258        .replace(QLatin1String("Ebm"), QLatin1String("EBM"))
0259        .replace(QLatin1String("Edm"), QLatin1String("EDM"))
0260        .replace(QLatin1String("Idm"), QLatin1String("IDM"))
0261        .replace(QLatin1String("Uk"), QLatin1String("UK"));
0262   return genre;
0263 }
0264 
0265 /**
0266  * Get genres from an XML node with a genre-list.
0267  * @param element XML node which could have a genre-list
0268  * @return genres separated by frame string list separator, null if not found.
0269  */
0270 QString parseGenres(const QDomElement& element)
0271 {
0272   if (QDomNode genreList =
0273         element.namedItem(QLatin1String("genre-list"));
0274       !genreList.isNull()) {
0275     QStringList genres, customGenres;
0276     for (QDomNode genreNode = genreList.namedItem(QLatin1String("genre"));
0277          !genreNode.isNull();
0278          genreNode = genreNode.nextSibling()) {
0279       if (!genreNode.isNull()) {
0280         if (QString genre = fixUpGenre(genreNode.toElement()
0281               .namedItem(QLatin1String("name")).toElement().text());
0282             !genre.isEmpty()) {
0283           if (int genreNum = Genres::getNumber(genre); genreNum != 255) {
0284             genres.append(QString::fromLatin1(Genres::getName(genreNum)));
0285           } else {
0286             customGenres.append(genre);
0287           }
0288         }
0289       }
0290     }
0291     genres.append(customGenres);
0292     return Frame::joinStringList(genres);
0293   }
0294   return QString();
0295 }
0296 
0297 }
0298 
0299 /**
0300  * Parse result of album request and populate m_trackDataModel with results.
0301  *
0302  * @param albumStr album data received
0303  */
0304 void MusicBrainzImporter::parseAlbumResults(const QByteArray& albumStr)
0305 {
0306   /*
0307 <metadata>
0308   <release id="978c7ed1-a854-4ef2-bd4e-e7c1317be854">
0309     <title>Odin</title>
0310     <artist-credit>
0311       <name-credit>
0312         <artist id="d1075cad-33e3-496b-91b0-d4670aabf4f8">
0313           <name>Wizard</name>
0314           <sort-name>Wizard</sort-name>
0315         </artist>
0316       </name-credit>
0317     </artist-credit>
0318     <date>2003-08-19</date>
0319     <asin>B00008OUEN</asin>
0320     <medium-list count="1">
0321       <medium>
0322         <position>1</position>
0323         <track-list count="11" offset="0">
0324           <track>
0325             <position>1</position>
0326             <recording id="dac7c002-432f-4dcb-ad57-5ebde8e258b0">
0327               <title>The Prophecy</title>
0328               <length>319173</length>
0329             </recording>
0330   */
0331   int start = albumStr.indexOf("<?xml");
0332   int end = albumStr.indexOf("</metadata>");
0333   QByteArray xmlStr = start >= 0 && end > start ?
0334     albumStr.mid(start, end + 11 - start) : albumStr;
0335   if (QDomDocument doc; doc.setContent(xmlStr, false)) {
0336     QDomElement release =
0337       doc.namedItem(QLatin1String("metadata")).toElement()
0338          .namedItem(QLatin1String("release")).toElement();
0339     FrameCollection framesHdr;
0340     const bool standardTags = getStandardTags();
0341     if (standardTags) {
0342       framesHdr.setAlbum(release.namedItem(QLatin1String("title")).toElement()
0343                          .text());
0344       QDomElement artist = release.namedItem(QLatin1String("artist-credit"))
0345           .toElement().namedItem(QLatin1String("name-credit"))
0346           .toElement().namedItem(QLatin1String("artist"))
0347           .toElement();
0348       framesHdr.setArtist(artist.namedItem(QLatin1String("name"))
0349                           .toElement().text());
0350       if (QString genre = parseGenres(artist); !genre.isEmpty()) {
0351         framesHdr.setGenre(genre);
0352       }
0353       if (QString date(release.namedItem(QLatin1String("date")).toElement().text());
0354           !date.isEmpty()) {
0355         QRegularExpression dateRe(QLatin1String(R"(^(\d{4})(?:-\d{2})?(?:-\d{2})?$)"));
0356         int year;
0357         if (auto match = dateRe.match(date); match.hasMatch()) {
0358           year = match.captured(1).toInt();
0359         } else {
0360           year = date.toInt();
0361         }
0362         if (year != 0) {
0363           framesHdr.setYear(year);
0364         }
0365       }
0366     }
0367 
0368     ImportTrackDataVector trackDataVector(m_trackDataModel->getTrackData());
0369     trackDataVector.setCoverArtUrl(QUrl());
0370     const bool coverArt = getCoverArt();
0371     if (coverArt) {
0372       if (QString asin(release.namedItem(QLatin1String("asin")).toElement().text());
0373           !asin.isEmpty()) {
0374         trackDataVector.setCoverArtUrl(
0375           QUrl(QLatin1String("http://www.amazon.com/dp/") + asin));
0376       }
0377     }
0378 
0379     const bool additionalTags = getAdditionalTags();
0380     if (additionalTags) {
0381       // label can be found in the label-info-list
0382       if (QDomElement labelInfoList(
0383             release.namedItem(QLatin1String("label-info-list")).toElement());
0384           !labelInfoList.isNull()) {
0385         if (QDomElement labelInfo(
0386               labelInfoList.namedItem(QLatin1String("label-info")).toElement());
0387             !labelInfo.isNull()) {
0388           if (QString label(labelInfo.namedItem(QLatin1String("label"))
0389                                      .namedItem(QLatin1String("name"))
0390                                      .toElement().text());
0391               !label.isEmpty()) {
0392             framesHdr.setValue(Frame::FT_Publisher, label);
0393           }
0394           if (QString catNo(labelInfo.namedItem(QLatin1String("catalog-number"))
0395                                      .toElement().text());
0396               !catNo.isEmpty()) {
0397             framesHdr.setValue(Frame::FT_CatalogNumber, catNo);
0398           }
0399         }
0400       }
0401       // Release country can be found in "country"
0402       if (QString country(release.namedItem(QLatin1String("country"))
0403             .toElement().text());
0404           !country.isEmpty()) {
0405         framesHdr.setValue(Frame::FT_ReleaseCountry, country);
0406       }
0407     }
0408 
0409     if (additionalTags || coverArt) {
0410       QDomNode relationListNode(release.firstChild());
0411       while (!relationListNode.isNull()) {
0412         if (relationListNode.nodeName() == QLatin1String("relation-list")) {
0413           if (QDomElement relationList(relationListNode.toElement());
0414               !relationList.isNull()) {
0415             if (QString targetType(relationList.attribute(QLatin1String("target-type")));
0416                 targetType == QLatin1String("artist")) {
0417               if (additionalTags) {
0418                 parseCredits(relationList, framesHdr);
0419               }
0420             } else if (targetType == QLatin1String("url")) {
0421               if (coverArt) {
0422                 QDomNode relationNode(relationList.firstChild());
0423                 while (!relationNode.isNull()) {
0424                   if (relationNode.nodeName() == QLatin1String("relation")) {
0425                     if (QDomElement relation(relationNode.toElement());
0426                         !relation.isNull()) {
0427                       if (QString type(relation.attribute(QLatin1String("type")));
0428                           type == QLatin1String("cover art link") ||
0429                           type == QLatin1String("amazon asin")) {
0430                         QString coverArtUrl =
0431                             relation.namedItem(QLatin1String("target"))
0432                             .toElement().text();
0433                         // https://www.amazon.de/gp/product/ does not work,
0434                         // fix such links.
0435                         coverArtUrl.replace(
0436                             QRegularExpression(QLatin1String(
0437                                   "https://www\\.amazon\\.[^/]+/gp/product/")),
0438                             QLatin1String("http://images.amazon.com/images/P/"));
0439                         if (!coverArtUrl.endsWith(QLatin1String(".jpg"))) {
0440                           coverArtUrl += QLatin1String(".jpg");
0441                         }
0442                         trackDataVector.setCoverArtUrl(
0443                           QUrl(coverArtUrl));
0444                       }
0445                     }
0446                   }
0447                   relationNode = relationNode.nextSibling();
0448                 }
0449               }
0450             }
0451           }
0452         }
0453         relationListNode = relationListNode.nextSibling();
0454       }
0455     }
0456 
0457     auto it = trackDataVector.begin();
0458     bool atTrackDataListEnd = it == trackDataVector.end();
0459     int discNr = 1, trackNr = 1;
0460     bool ok;
0461     FrameCollection frames(framesHdr);
0462     QDomElement mediumList = release.namedItem(QLatin1String("medium-list"))
0463         .toElement();
0464     int mediumCount = mediumList.attribute(QLatin1String("count")).toInt();
0465     for (QDomNode mediumNode = mediumList.namedItem(QLatin1String("medium"));
0466          !mediumNode.isNull();
0467          mediumNode = mediumNode.nextSibling()) {
0468       int position = mediumNode.namedItem(QLatin1String("position"))
0469           .toElement().text().toInt(&ok);
0470       if (ok) {
0471         discNr = position;
0472       }
0473       QDomElement trackList = mediumNode.namedItem(QLatin1String("track-list"))
0474           .toElement();
0475       for (QDomNode trackNode = trackList.namedItem(QLatin1String("track"));
0476            !trackNode.isNull();
0477            trackNode = trackNode.nextSibling()) {
0478         if (mediumCount > 1 && additionalTags) {
0479           frames.setValue(Frame::FT_Disc, QString::number(discNr));
0480         }
0481         QDomElement track = trackNode.toElement();
0482         position = track.namedItem(QLatin1String("position")).toElement()
0483             .text().toInt(&ok);
0484         if (ok) {
0485           trackNr = position;
0486         }
0487         if (standardTags) {
0488           frames.setTrack(trackNr);
0489         }
0490         int duration = track.namedItem(QLatin1String("length")).toElement()
0491             .text().toInt();
0492         if (QDomElement recording = track.namedItem(QLatin1String("recording"))
0493                                          .toElement();
0494             !recording.isNull()) {
0495           if (standardTags) {
0496             frames.setTitle(recording.namedItem(QLatin1String("title"))
0497                             .toElement().text());
0498           }
0499           int length = recording.namedItem(QLatin1String("length"))
0500               .toElement().text().toInt(&ok);
0501           if (ok) {
0502             duration = length;
0503           }
0504           if (QDomNode artistNode =
0505                 recording.namedItem(QLatin1String("artist-credit"));
0506               !artistNode.isNull()) {
0507             QDomElement artistElement = artistNode.toElement()
0508                 .namedItem(QLatin1String("name-credit")).toElement()
0509                 .namedItem(QLatin1String("artist")).toElement();
0510             if (QString artist = artistElement
0511                   .namedItem(QLatin1String("name")).toElement().text();
0512                 !artist.isEmpty()) {
0513               // use the artist in the header as the album artist
0514               // and the artist in the track as the artist
0515               if (standardTags) {
0516                 frames.setArtist(artist);
0517               }
0518               if (additionalTags) {
0519                 frames.setValue(Frame::FT_AlbumArtist, framesHdr.getArtist());
0520               }
0521             }
0522             if (QString genre = parseGenres(artistElement); !genre.isEmpty()) {
0523               frames.setGenre(genre);
0524             }
0525           }
0526           if (QString genre = parseGenres(recording); !genre.isEmpty()) {
0527             frames.setGenre(genre);
0528           }
0529           if (additionalTags) {
0530             QDomNode relationListNode(recording.firstChild());
0531             while (!relationListNode.isNull()) {
0532               if (relationListNode.nodeName() == QLatin1String("relation-list")) {
0533                 if (QDomElement relationList(relationListNode.toElement());
0534                     !relationList.isNull()) {
0535                   if (QString targetType(
0536                         relationList.attribute(QLatin1String("target-type")));
0537                       targetType == QLatin1String("artist")) {
0538                     parseCredits(relationList, frames);
0539                   } else if (targetType == QLatin1String("work")) {
0540                     if (QDomNode workRelationListNode(relationList
0541                           .namedItem(QLatin1String("relation"))
0542                           .namedItem(QLatin1String("work"))
0543                           .namedItem(QLatin1String("relation-list")));
0544                         !workRelationListNode.isNull()) {
0545                       parseCredits(workRelationListNode.toElement(), frames);
0546                     }
0547                   }
0548                 }
0549               }
0550               relationListNode = relationListNode.nextSibling();
0551             }
0552           }
0553         }
0554         duration /= 1000;
0555         if (atTrackDataListEnd) {
0556           ImportTrackData trackData;
0557           trackData.setFrameCollection(frames);
0558           trackData.setImportDuration(duration);
0559           trackDataVector.push_back(trackData);
0560         } else {
0561           while (!atTrackDataListEnd && !it->isEnabled()) {
0562             ++it;
0563             atTrackDataListEnd = it == trackDataVector.end();
0564           }
0565           if (!atTrackDataListEnd) {
0566             it->setFrameCollection(frames);
0567             it->setImportDuration(duration);
0568             ++it;
0569             atTrackDataListEnd = it == trackDataVector.end();
0570           }
0571         }
0572         ++trackNr;
0573         frames = framesHdr;
0574       }
0575       ++discNr;
0576     }
0577     // handle redundant tracks
0578     frames.clear();
0579     while (!atTrackDataListEnd) {
0580       if (it->isEnabled()) {
0581         if (it->getFileDuration() == 0) {
0582           it = trackDataVector.erase(it);
0583         } else {
0584           it->setFrameCollection(frames);
0585           it->setImportDuration(0);
0586           ++it;
0587         }
0588       } else {
0589         ++it;
0590       }
0591       atTrackDataListEnd = it == trackDataVector.end();
0592     }
0593     m_trackDataModel->setTrackData(trackDataVector);
0594   }
0595 }
0596 
0597 /**
0598  * Send a query command to search on the server.
0599  *
0600  * @param cfg      import source configuration
0601  * @param artist   artist to search
0602  * @param album    album to search
0603  */
0604 void MusicBrainzImporter::sendFindQuery(
0605   const ServerImporterConfig* cfg,
0606   const QString& artist, const QString& album)
0607 {
0608   Q_UNUSED(cfg)
0609   // If an URL is entered in the first search field, its result will be directly
0610   // available in the album results list.
0611   if (artist.startsWith(QLatin1String("https://musicbrainz.org/"))) {
0612     constexpr int catBegin = 24;
0613     if (int catEnd = artist.indexOf(QLatin1Char('/'), catBegin);
0614         catEnd > catBegin) {
0615       m_albumListModel->clear();
0616       m_albumListModel->appendItem(
0617             artist,
0618             artist.mid(catBegin, catEnd - catBegin),
0619             artist.mid(catEnd + 1));
0620       return;
0621     }
0622   }
0623   /*
0624    * Query looks like this:
0625    * http://musicbrainz.org/ws/2/release?query=artist:wizard%20AND%20release:odin
0626    */
0627   QString path(QLatin1String("/ws/2/release?query="));
0628   if (!artist.isEmpty()) {
0629     QString artistQuery(artist.contains(QLatin1Char(' '))
0630                         ? QLatin1Char('"') + artist + QLatin1Char('"')
0631                         : artist);
0632     if (!album.isEmpty()) {
0633       artistQuery += QLatin1String(" AND ");
0634     }
0635     path += QLatin1String("artist:");
0636     path += QString::fromLatin1(QUrl::toPercentEncoding(artistQuery));
0637   }
0638   if (!album.isEmpty()) {
0639     QString albumQuery(album.contains(QLatin1Char(' '))
0640                         ? QLatin1Char('"') + album + QLatin1Char('"')
0641                         : album);
0642     path += QLatin1String("release:");
0643     path += QString::fromLatin1(QUrl::toPercentEncoding(albumQuery));
0644   }
0645   sendRequest(QLatin1String("musicbrainz.org"), path, QLatin1String("https"),
0646               m_headers);
0647 }
0648 
0649 /**
0650  * Send a query command to fetch the track list
0651  * from the server.
0652  *
0653  * @param cfg      import source configuration
0654  * @param cat      category
0655  * @param id       ID
0656  */
0657 void MusicBrainzImporter::sendTrackListQuery(
0658   const ServerImporterConfig* cfg, const QString& cat, const QString& id)
0659 {
0660   /*
0661    * Query looks like this:
0662    * http://musicbrainz.org/ws/2/release/978c7ed1-a854-4ef2-bd4e-e7c1317be854?inc=artists+recordings
0663    */
0664   QString path(QLatin1String("/ws/2/"));
0665   path += cat;
0666   path += QLatin1Char('/');
0667   path += id;
0668   path += QLatin1String("?inc=");
0669   if (cfg->additionalTags()) {
0670     path += QLatin1String("artist-credits+labels+recordings+genres+media+isrcs+"
0671                 "discids+artist-rels+label-rels+recording-rels+release-rels");
0672   } else {
0673     path += QLatin1String("artists+recordings+genres");
0674   }
0675   if (cfg->coverArt()) {
0676     path += QLatin1String("+url-rels");
0677   }
0678   if (cfg->additionalTags()) {
0679     path += QLatin1String("+work-rels+recording-level-rels+work-level-rels");
0680   }
0681   sendRequest(QLatin1String("musicbrainz.org"), path, QLatin1String("https"),
0682               m_headers);
0683 }