File indexing completed on 2024-05-19 04:56:26
0001 /** 0002 * \file discogsimporter.cpp 0003 * Discogs 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 "discogsimporter.h" 0028 #include <QUrl> 0029 #include <QJsonDocument> 0030 #include <QJsonObject> 0031 #include <QJsonArray> 0032 #include <QRegularExpression> 0033 #include "serverimporterconfig.h" 0034 #include "trackdatamodel.h" 0035 #include "discogsconfig.h" 0036 #include "config.h" 0037 #include "genres.h" 0038 0039 namespace { 0040 0041 /** 0042 * Remove trailing stars and numbers like (2) from a string. 0043 * 0044 * @param str string 0045 * 0046 * @return fixed up string. 0047 */ 0048 QString fixUpArtist(QString str) 0049 { 0050 str.replace(QRegularExpression(QLatin1String(",(\\S)")), QLatin1String(", \\1")); 0051 str.replace(QLatin1String("* / "), QLatin1String(" / ")); 0052 str.replace(QLatin1String("* - "), QLatin1String(" - ")); 0053 str.replace(QLatin1String("*,"), QLatin1String(",")); 0054 str.remove(QRegularExpression(QLatin1String("\\*$"))); 0055 str.remove(QRegularExpression(QLatin1String(R"([*\s]*\(\d+\)\(tracks:[^)]+\))"))); 0056 str.replace(QRegularExpression( 0057 QLatin1String("[*\\s]*\\((?:\\d+|tracks:[^)]+)\\)(\\s*/\\s*,|\\s*&|" 0058 "\\s*And|\\s*and)")), 0059 QLatin1String("\\1")); 0060 str.remove(QRegularExpression(QLatin1String(R"([*\s]*\((?:\d+|tracks:[^)]+)\)$)"))); 0061 return ServerImporter::removeHtml(str); 0062 } 0063 0064 /** 0065 * Create a string with artists contained in an artist list. 0066 * @param artists list containing artist maps 0067 * @return string with artists joined appropriately. 0068 */ 0069 QString getArtistString(const QJsonArray& artists) 0070 { 0071 QString artist; 0072 if (!artists.isEmpty()) { 0073 QString join; 0074 for (const auto& val : artists) { 0075 auto map = val.toObject(); 0076 if (!artist.isEmpty()) { 0077 artist += join; 0078 } 0079 artist += fixUpArtist((map.contains(QLatin1String("name")) 0080 ? map.value(QLatin1String("name")) 0081 : map.contains(QLatin1String("displayName")) 0082 ? map.value(QLatin1String("displayName")) 0083 : map.value(QLatin1String("artist")) 0084 .toObject().value(QLatin1String("name"))) 0085 .toString()); 0086 join = (map.contains(QLatin1String("join")) 0087 ? map.value(QLatin1String("join")) 0088 : map.value(QLatin1String("joiningText"))).toString(); 0089 if (join.isEmpty() || join == QLatin1String(",")) { 0090 join = QLatin1String(", "); 0091 } else { 0092 join = QLatin1Char(' ') + join + QLatin1Char(' '); 0093 } 0094 } 0095 } 0096 return artist; 0097 } 0098 0099 /** 0100 * Add involved people to a frame. 0101 * The format used is (should be converted according to tag specifications): 0102 * involvee 1 (involvement 1)\n 0103 * involvee 2 (involvement 2)\n 0104 * ... 0105 * involvee n (involvement n) 0106 * 0107 * @param frames frame collection 0108 * @param type type of frame 0109 * @param involvement involvement (e.g. instrument) 0110 * @param involvee name of involvee (e.g. musician) 0111 */ 0112 void addInvolvedPeople( 0113 FrameCollection& frames, Frame::Type type, 0114 const QString& involvement, const QString& involvee) 0115 { 0116 QString value = frames.getValue(type); 0117 if (!value.isEmpty()) value += Frame::stringListSeparator(); 0118 value += Frame::joinStringList({involvement, involvee}); 0119 frames.setValue(type, value); 0120 } 0121 0122 /** 0123 * Get frame for a role. 0124 * @param role role or credit of involved people, can be adapted to canonical 0125 * value 0126 * @return suitable frame type, Frame::FT_UnknownFrame if not found. 0127 */ 0128 Frame::Type frameTypeForRole(QString& role) 0129 { 0130 static const struct { 0131 const char* credit; 0132 Frame::Type type; 0133 } creditToType[] = { 0134 { "Composed By", Frame::FT_Composer }, 0135 { "Conductor", Frame::FT_Conductor }, 0136 { "Orchestra", Frame::FT_AlbumArtist }, 0137 { "Lyrics By", Frame::FT_Lyricist }, 0138 { "Written-By", Frame::FT_Author }, 0139 { "Written By", Frame::FT_Author }, 0140 { "Remix", Frame::FT_Remixer }, 0141 { "Music By", Frame::FT_Composer }, 0142 { "Songwriter", Frame::FT_Composer } 0143 }; 0144 for (const auto& c2t : creditToType) { 0145 if (role.contains(QString::fromLatin1(c2t.credit))) { 0146 return c2t.type; 0147 } 0148 } 0149 0150 static const struct { 0151 const char* credit; 0152 const char* arrangement; 0153 } creditToArrangement[] = { 0154 { "Arranged By", "Arranger" }, 0155 { "Mixed By", "Mixer" }, 0156 { "DJ Mix", "DJMixer" }, 0157 { "Dj Mix", "DJMixer" }, 0158 { "Engineer", "Engineer" }, 0159 { "Mastered By", "Engineer" }, 0160 { "Producer", "Producer" }, 0161 { "Co-producer", "Producer" }, 0162 { "Executive Producer", "Producer" } 0163 }; 0164 for (const auto& [credit, arrangement] : creditToArrangement) { 0165 if (role.contains(QString::fromLatin1(credit))) { 0166 role = QString::fromLatin1(arrangement); 0167 return Frame::FT_Arranger; 0168 } 0169 } 0170 0171 static const char* const instruments[] = { 0172 "Performer", "Vocals", "Voice", "Featuring", "Choir", "Chorus", 0173 "Baritone", "Tenor", "Rap", "Scratches", "Drums", "Percussion", 0174 "Keyboards", "Cello", "Piano", "Organ", "Synthesizer", "Keys", 0175 "Wurlitzer", "Rhodes", "Harmonica", "Xylophone", "Guitar", "Bass", 0176 "Strings", "Violin", "Viola", "Banjo", "Harp", "Mandolin", 0177 "Clarinet", "Horn", "Cornet", "Flute", "Oboe", "Saxophone", 0178 "Trumpet", "Tuba", "Trombone" 0179 }; 0180 for (auto instrument : instruments) { 0181 if (role.contains(QString::fromLatin1(instrument))) { 0182 return Frame::FT_Performer; 0183 } 0184 } 0185 0186 return Frame::FT_UnknownFrame; 0187 } 0188 0189 /** 0190 * Set tags from a string with credits lines. 0191 * The string must have lines like "Composed By - Iommi", separated by \\n. 0192 * 0193 * @param str credits string 0194 * @param frames tags will be added to these frames 0195 * 0196 * @return true if credits found. 0197 */ 0198 bool parseCredits(const QString& str, FrameCollection& frames) 0199 { 0200 bool result = false; 0201 QStringList lines = str.split(QLatin1Char('\n')); 0202 for (auto it = lines.constBegin(); it != lines.constEnd(); ++it) { 0203 if (int nameStart = it->indexOf(QLatin1String(" - ")); nameStart != -1) { 0204 const QStringList names = it->mid(nameStart + 3).split(QLatin1String(", ")); 0205 QString name; 0206 for (const QString& namesPart : names) { 0207 if (!name.isEmpty()) { 0208 name += QLatin1String(", "); 0209 } 0210 name += fixUpArtist(namesPart); 0211 } 0212 QStringList credits = it->left(nameStart).split(QLatin1String(", ")); 0213 for (auto cit = credits.constBegin(); cit != credits.constEnd(); ++cit) { 0214 QString role = *cit; 0215 if (Frame::Type frameType = frameTypeForRole(role); 0216 frameType == Frame::FT_Arranger || 0217 frameType == Frame::FT_Performer) { 0218 addInvolvedPeople(frames, frameType, role, name); 0219 result = true; 0220 } else if (frameType != Frame::FT_UnknownFrame) { 0221 frames.setValue(frameType, name); 0222 result = true; 0223 } 0224 } 0225 } 0226 } 0227 return result; 0228 } 0229 0230 /** 0231 * Add name to frame with credits. 0232 * @param frames frame collection 0233 * @param type type of frame 0234 * @param name name of person to credit 0235 */ 0236 void addCredit(FrameCollection& frames, Frame::Type type, const QString& name) 0237 { 0238 QString value = frames.getValue(type); 0239 if (!value.isEmpty()) value += QLatin1String(", "); 0240 value += name; 0241 frames.setValue(type, value); 0242 } 0243 0244 /** 0245 * Extract the URL from "fullsize.__ref" of an image JSON object. 0246 * @param imageValue image JSON value 0247 * @return image URL if present, else null. 0248 */ 0249 QString extractUrlFromImageValue(const QJsonValue& imageValue) { 0250 QRegularExpression sourceUrlRe( 0251 QLatin1String("\"sourceUrl\"\\s*:\\s*\"([^\"]+)\"")); 0252 QString ref = imageValue.toObject() 0253 .value(QLatin1String("fullsize")).toObject() 0254 .value(QLatin1String("__ref")).toString(); 0255 auto match = sourceUrlRe.match(ref); 0256 return match.hasMatch() ? match.captured(1) : QString(); 0257 } 0258 0259 /** 0260 * Stores information about extra artists. 0261 * The information can be used to add frames to the appropriate tracks. 0262 */ 0263 class ExtraArtist { 0264 public: 0265 /** 0266 * Constructor. 0267 * @param obj JSON object containing extra artist information 0268 */ 0269 explicit ExtraArtist(const QJsonObject& obj); 0270 0271 /** 0272 * Add extra artist information to frames. 0273 * @param frames frame collection 0274 * @param trackPos optional position, the extra artist information will 0275 * only be added if this track position is listed in the 0276 * track restrictions or is empty 0277 */ 0278 void addToFrames(FrameCollection& frames, 0279 const QString& trackPos = QString()) const; 0280 0281 /** 0282 * Check if extra artist information is only valid for a subset of the tracks. 0283 * @return true if extra artist has track restriction. 0284 */ 0285 bool hasTrackRestriction() const { return !m_tracks.isEmpty(); } 0286 0287 private: 0288 QString m_name; 0289 QString m_role; 0290 QStringList m_tracks; 0291 }; 0292 0293 /** 0294 * Constructor. 0295 * @param obj JSON object containing extra artist information 0296 */ 0297 ExtraArtist::ExtraArtist(const QJsonObject& obj) : 0298 m_name(fixUpArtist((obj.contains(QLatin1String("name")) 0299 ? obj.value(QLatin1String("name")) 0300 : obj.contains(QLatin1String("displayName")) 0301 ? obj.value(QLatin1String("displayName")) 0302 : obj.value(QLatin1String("artist")) 0303 .toObject().value(QLatin1String("name"))).toString())), 0304 m_role((obj.contains(QLatin1String("role")) 0305 ? obj.value(QLatin1String("role")) 0306 : obj.value(QLatin1String("creditRole"))).toString().trimmed()) 0307 { 0308 static const QRegularExpression tracksSepRe(QLatin1String(",\\s*")); 0309 if (QString tracks = (obj.contains(QLatin1String("tracks")) 0310 ? obj.value(QLatin1String("tracks")) 0311 : obj.value(QLatin1String("applicableTracks"))) 0312 .toString(); 0313 !tracks.isEmpty()) { 0314 m_tracks = tracks.split(tracksSepRe); 0315 } 0316 } 0317 0318 /** 0319 * Add extra artist information to frames. 0320 * @param frames frame collection 0321 * @param trackPos optional position, the extra artist information will 0322 * only be added if this track position is listed in the 0323 * track restrictions or is empty 0324 */ 0325 void ExtraArtist::addToFrames(FrameCollection& frames, 0326 const QString& trackPos) const 0327 { 0328 if (!trackPos.isEmpty() && !m_tracks.contains(trackPos)) 0329 return; 0330 0331 QString role = m_role; 0332 if (Frame::Type frameType = frameTypeForRole(role); 0333 frameType == Frame::FT_Arranger || 0334 frameType == Frame::FT_Performer) { 0335 addInvolvedPeople(frames, frameType, role, m_name); 0336 } else if (frameType != Frame::FT_UnknownFrame) { 0337 addCredit(frames, frameType, m_name); 0338 } 0339 } 0340 0341 0342 /** 0343 * Stores information about a track. 0344 */ 0345 class TrackInfo { 0346 public: 0347 /** 0348 * Constructor. 0349 * @param track JSON object containing track information 0350 */ 0351 explicit TrackInfo(const QJsonObject& track); 0352 0353 void addToFrames(FrameCollection& frames, 0354 const QList<ExtraArtist>& trackExtraArtists, 0355 bool standardTags, bool additionalTags) const; 0356 0357 QString title() const { return m_title; } 0358 QString disc() const { return m_disc; } 0359 QString position() const { return m_position; } 0360 int pos() const { return m_pos; } 0361 int duration() const { return m_duration; } 0362 0363 private: 0364 QString m_title; 0365 QString m_disc; 0366 QString m_position; 0367 int m_pos; 0368 int m_duration; 0369 }; 0370 0371 /** 0372 * Constructor. 0373 * @param track JSON object containing track information 0374 */ 0375 TrackInfo::TrackInfo(const QJsonObject& track) 0376 : m_pos(0), m_duration(0) 0377 { 0378 QRegularExpression discTrackPosRe(QLatin1String("^(\\d+)-(\\d+)$")); 0379 m_position = track.value(QLatin1String("position")).toString(); 0380 bool ok; 0381 m_pos = m_position.toInt(&ok); 0382 if (!ok) { 0383 if (auto match = discTrackPosRe.match(m_position); match.hasMatch()) { 0384 m_disc = match.captured(1); 0385 m_pos = match.captured(2).toInt(); 0386 } 0387 } 0388 m_title = track.value(QLatin1String("title")).toString().trimmed(); 0389 0390 m_duration = 0; 0391 if (track.contains(QLatin1String("duration"))) { 0392 const QStringList durationHms = track.value(QLatin1String("duration")) 0393 .toString().split(QLatin1Char(':')); 0394 for (const auto& val : durationHms) { 0395 m_duration *= 60; 0396 m_duration += val.toInt(); 0397 } 0398 } else { 0399 m_duration = track.value(QLatin1String("durationInSeconds")).toInt(); 0400 } 0401 } 0402 0403 void TrackInfo::addToFrames(FrameCollection& frames, 0404 const QList<ExtraArtist>& trackExtraArtists, 0405 bool standardTags, bool additionalTags) const 0406 { 0407 if (standardTags) { 0408 frames.setTrack(m_pos); 0409 frames.setTitle(m_title); 0410 } 0411 if (additionalTags && !m_disc.isNull()) { 0412 frames.setValue(Frame::FT_Disc, m_disc); 0413 } 0414 if (additionalTags && m_pos == 0 && !m_position.isEmpty()) { 0415 // Support tracks which are not numeric, e.g. "A2" 0416 frames.setValue(Frame::FT_Track, m_position); 0417 } 0418 for (const auto& extraArtist : trackExtraArtists) { 0419 extraArtist.addToFrames(frames, m_position); 0420 } 0421 } 0422 0423 0424 /** 0425 * Parse album results from a JSON object. 0426 * @param map JSON object, returned object from API import, "Release..." 0427 * property when getting it from the HTML output 0428 * @param importer Discogs importer 0429 * @param trackDataModel track data model to update with imported data 0430 * @param data optional top level data 0431 * @return true if at least one title was found. 0432 */ 0433 bool parseJsonAlbumResults(const QJsonObject& map, 0434 const DiscogsImporter* importer, TrackDataModel* trackDataModel, 0435 const QJsonObject& data = QJsonObject()) 0436 { 0437 // releases have the format (JSON, simplified): 0438 // { "styles": ["Heavy Metal"], 0439 // "labels": [{"name": "LMP"}], 0440 // "year": 2003, 0441 // "artists": [{"name": "Wizard (23)"}], 0442 // "images": [ 0443 // { "uri": "http://api.discogs.com/image/R-2487778-1293847958.jpeg", 0444 // "type": "primary" }, 0445 // { "uri": "http://api.discogs.com/image/R-2487778-1293847967.jpeg", 0446 // "type": "secondary" }], 0447 // "id": 2487778, 0448 // "genres": ["Rock"], 0449 // "thumb": "http://api.discogs.com/image/R-150-2487778-1293847958.jpeg", 0450 // "extraartists": [], 0451 // "title": "Odin", 0452 // "tracklist": [ 0453 // {"duration": "5:19", "position": "1", "title": "The Prophecy"}, 0454 // {"duration": "", "position": "Video", "title": "Betrayer"} 0455 // ], 0456 // "released": "2003", 0457 // "formats": [{"name": "CD"}] 0458 // } 0459 QRegularExpression discTrackPosRe(QLatin1String("^(\\d+)-(\\d+)$")); 0460 QRegularExpression yearRe(QLatin1String("^\\d{4}-\\d{2}")); 0461 QList<ExtraArtist> trackExtraArtists; 0462 ImportTrackDataVector trackDataVector(trackDataModel->getTrackData()); 0463 FrameCollection framesHdr; 0464 const bool standardTags = importer->getStandardTags(); 0465 if (standardTags) { 0466 framesHdr.setAlbum(map.value(QLatin1String("title")).toString().trimmed()); 0467 framesHdr.setArtist( 0468 getArtistString((map.contains(QLatin1String("artists")) 0469 ? map.value(QLatin1String("artists")) 0470 : map.value(QLatin1String("primaryArtists"))) 0471 .toArray())); 0472 0473 // The year can be found in "released". 0474 QString released(map.value(QLatin1String("released")).toString()); 0475 if (auto match = yearRe.match(released); match.hasMatch()) { 0476 released.truncate(4); 0477 } 0478 framesHdr.setYear(released.toInt()); 0479 0480 // The genre can be found in "genre" or "style". 0481 // All genres found are checked for an ID3v1 number, starting with those 0482 // in the style field. 0483 // Converted to QVariantList because adding two QJsonArray will add the 0484 // second array as a single element to the first array. 0485 const auto genreList = 0486 map.value(QLatin1String("styles")).toArray().toVariantList() + 0487 map.value(QLatin1String("genres")).toArray().toVariantList(); 0488 QStringList genres, customGenres; 0489 for (const auto& val : genreList) { 0490 if (QString genre = val.toString().trimmed(); !genre.isEmpty()) { 0491 if (int genreNum = Genres::getNumber(genre); genreNum != 255) { 0492 genres.append(QString::fromLatin1(Genres::getName(genreNum))); 0493 } else { 0494 customGenres.append(genre); 0495 } 0496 } 0497 } 0498 genres.append(customGenres); 0499 if (!genres.isEmpty()) { 0500 framesHdr.setGenre(Frame::joinStringList(genres)); 0501 } 0502 } 0503 0504 trackDataVector.setCoverArtUrl(QUrl()); 0505 if (importer->getCoverArt()) { 0506 // Cover art can be found in "images" 0507 if (auto images = map.value(QLatin1String("images")).toArray(); 0508 !images.isEmpty()) { 0509 trackDataVector.setCoverArtUrl( 0510 QUrl(images.first().toObject().value(QLatin1String("uri")) 0511 .toString())); 0512 } 0513 } 0514 0515 const bool additionalTags = importer->getAdditionalTags(); 0516 if (additionalTags) { 0517 // Publisher can be found in "label" 0518 if (auto labels = map.value(QLatin1String("labels")).toArray(); 0519 !labels.isEmpty()) { 0520 auto firstLabelMap = labels.first().toObject(); 0521 if (QString catNo = (firstLabelMap.contains(QLatin1String("catno")) 0522 ? firstLabelMap.value(QLatin1String("catno")) 0523 : firstLabelMap.value(QLatin1String("catalogNumber"))) 0524 .toString().trimmed(); 0525 !catNo.isEmpty() && catNo.toLower() != QLatin1String("none")) { 0526 framesHdr.setValue(Frame::FT_CatalogNumber, catNo); 0527 } 0528 if (!firstLabelMap.contains(QLatin1String("name")) && 0529 firstLabelMap.contains(QLatin1String("label"))) { 0530 firstLabelMap = firstLabelMap.value(QLatin1String("label")).toObject(); 0531 } 0532 framesHdr.setValue(Frame::FT_Publisher, 0533 fixUpArtist(firstLabelMap.value(QLatin1String("name")).toString())); 0534 } 0535 // Media can be found in "formats" 0536 if (auto formats = map.value(QLatin1String("formats")).toArray(); 0537 !formats.isEmpty()) { 0538 framesHdr.setValue(Frame::FT_Media, 0539 formats.first().toObject().value(QLatin1String("name")) 0540 .toString().trimmed()); 0541 } 0542 // Credits can be found in "extraartists" 0543 if (const auto extraartists = 0544 (map.contains(QLatin1String("extraartists")) 0545 ? map.value(QLatin1String("extraartists")) 0546 : map.value(QLatin1String("releaseCredits"))) 0547 .toArray(); 0548 !extraartists.isEmpty()) { 0549 for (const auto& val : extraartists) { 0550 if (ExtraArtist extraArtist(val.toObject()); 0551 extraArtist.hasTrackRestriction()) { 0552 trackExtraArtists.append(extraArtist); 0553 } else { 0554 extraArtist.addToFrames(framesHdr); 0555 } 0556 } 0557 } 0558 // Release country can be found in "country" 0559 if (QString country(map.value(QLatin1String("country")).toString().trimmed()); 0560 !country.isEmpty()) { 0561 framesHdr.setValue(Frame::FT_ReleaseCountry, country); 0562 } 0563 } 0564 0565 FrameCollection frames(framesHdr); 0566 auto it = trackDataVector.begin(); 0567 int trackNr = 1; 0568 bool atTrackDataListEnd = it == trackDataVector.end(); 0569 bool titleFound = false; 0570 0571 auto addFramesToTrackData = 0572 [&atTrackDataListEnd, &trackDataVector, &it, &trackNr, &titleFound]( 0573 FrameCollection& frms, int duration) { 0574 if (!frms.getTitle().isEmpty()) { 0575 titleFound = true; 0576 } 0577 if (frms.getValue(Frame::FT_Track).isEmpty()) { 0578 // Track as string is used instead of "frames.getTrack() == 0" to support 0579 // tracks like "A2" 0580 frms.setTrack(trackNr); 0581 } 0582 if (atTrackDataListEnd) { 0583 ImportTrackData trackData; 0584 trackData.setFrameCollection(frms); 0585 trackData.setImportDuration(duration); 0586 trackDataVector.append(trackData); 0587 } else { 0588 while (!atTrackDataListEnd && !it->isEnabled()) { 0589 ++it; 0590 atTrackDataListEnd = it == trackDataVector.end(); 0591 } 0592 if (!atTrackDataListEnd) { 0593 it->setFrameCollection(frms); 0594 it->setImportDuration(duration); 0595 ++it; 0596 atTrackDataListEnd = it == trackDataVector.end(); 0597 } 0598 } 0599 ++trackNr; 0600 }; 0601 0602 const auto trackList = map.value(map.contains(QLatin1String("tracklist")) 0603 ? QLatin1String("tracklist") 0604 : QLatin1String("tracks")).toArray(); 0605 0606 // Check if all positions are empty. 0607 bool allPositionsEmpty = true; 0608 for (const auto& val : trackList) { 0609 if (!val.toObject().value(QLatin1String("position")).toString().isEmpty()) { 0610 allPositionsEmpty = false; 0611 break; 0612 } 0613 } 0614 0615 for (const auto& val : trackList) { 0616 auto track = val.toObject(); 0617 if (track.size() == 1 && track.contains(QLatin1String("__ref")) && 0618 !data.isEmpty()) { 0619 if (const QJsonObject trackRef = data.value( 0620 track.value(QLatin1String("__ref")).toString()).toObject(); 0621 trackRef.contains(QLatin1String("title"))) { 0622 track = trackRef; 0623 } 0624 } 0625 0626 // Do not include heading tracks 0627 if (track.value(QLatin1String("trackType")).toString() == 0628 QLatin1String("HEADING")) { 0629 continue; 0630 } 0631 0632 TrackInfo trackInfo(track); 0633 if (const auto artists((track.contains(QLatin1String("artists")) 0634 ? track.value(QLatin1String("artists")) 0635 : track.value(QLatin1String("primaryArtists"))) 0636 .toArray()); 0637 !artists.isEmpty()) { 0638 if (standardTags) { 0639 frames.setArtist(getArtistString(artists)); 0640 } 0641 if (additionalTags) { 0642 frames.setValue(Frame::FT_AlbumArtist, framesHdr.getArtist()); 0643 } 0644 } 0645 if (additionalTags) { 0646 if (const auto extraartists((track.contains(QLatin1String("extraartists")) 0647 ? track.value(QLatin1String("extraartists")) 0648 : track.value(QLatin1String("trackCredits"))) 0649 .toArray()); 0650 !extraartists.isEmpty()) { 0651 for (const auto& eaVal : extraartists) { 0652 ExtraArtist extraArtist(eaVal.toObject()); 0653 extraArtist.addToFrames(frames); 0654 } 0655 } 0656 } 0657 if (!allPositionsEmpty && trackInfo.position().isEmpty()) { 0658 if (const auto subTracks((track.contains(QLatin1String("sub_tracks")) 0659 ? track.value(QLatin1String("sub_tracks")) 0660 : track.value(QLatin1String("subTracks"))) 0661 .toArray()); 0662 !subTracks.isEmpty()) { 0663 if (additionalTags) { 0664 frames.setValue(Frame::FT_Subtitle, trackInfo.title()); 0665 } 0666 for (const auto& stVal : subTracks) { 0667 TrackInfo subTrackInfo(stVal.toObject()); 0668 subTrackInfo.addToFrames(frames, trackExtraArtists, 0669 standardTags, additionalTags); 0670 addFramesToTrackData(frames, subTrackInfo.duration()); 0671 } 0672 } 0673 } else if (!trackInfo.title().isEmpty() || trackInfo.duration() != 0) { 0674 trackInfo.addToFrames(frames, trackExtraArtists, 0675 standardTags, additionalTags); 0676 addFramesToTrackData(frames, trackInfo.duration()); 0677 } 0678 frames = framesHdr; 0679 } 0680 // handle redundant tracks 0681 frames.clear(); 0682 while (!atTrackDataListEnd) { 0683 if (it->isEnabled()) { 0684 if (it->getFileDuration() == 0) { 0685 it = trackDataVector.erase(it); 0686 } else { 0687 it->setFrameCollection(frames); 0688 it->setImportDuration(0); 0689 ++it; 0690 } 0691 } else { 0692 ++it; 0693 } 0694 atTrackDataListEnd = it == trackDataVector.end(); 0695 } 0696 trackDataModel->setTrackData(trackDataVector); 0697 return titleFound; 0698 } 0699 0700 } 0701 0702 0703 /** 0704 * Abstract base class for Discogs importer implementations. 0705 */ 0706 class DiscogsImporter::BaseImpl { 0707 public: 0708 BaseImpl(DiscogsImporter* importer, const char* url); 0709 virtual ~BaseImpl(); 0710 0711 virtual void parseFindResults(const QByteArray& searchStr) = 0; 0712 virtual void parseAlbumResults(const QByteArray& albumStr) = 0; 0713 virtual void sendFindQuery( 0714 const ServerImporterConfig* cfg, 0715 const QString& artist, const QString& album) = 0; 0716 virtual void sendTrackListQuery( 0717 const ServerImporterConfig* cfg, 0718 const QString& cat, const QString& id) = 0; 0719 0720 AlbumListModel* albumListModel() { return m_importer->m_albumListModel; } 0721 TrackDataModel* trackDataModel() { return m_importer->m_trackDataModel; } 0722 QMap<QByteArray, QByteArray>& headers() { return m_discogsHeaders; } 0723 0724 protected: 0725 QMap<QByteArray, QByteArray> m_discogsHeaders; 0726 DiscogsImporter* m_importer; 0727 const char* const m_discogsServer; 0728 }; 0729 0730 DiscogsImporter::BaseImpl::BaseImpl(DiscogsImporter* importer, const char* url) 0731 : m_importer(importer), m_discogsServer(url) 0732 { 0733 } 0734 0735 DiscogsImporter::BaseImpl::~BaseImpl() 0736 { 0737 } 0738 0739 0740 /** 0741 * Importer implementation to import HTML data from the Discogs web site. 0742 */ 0743 class DiscogsImporter::HtmlImpl : public DiscogsImporter::BaseImpl { 0744 public: 0745 explicit HtmlImpl(DiscogsImporter* importer); 0746 ~HtmlImpl() override; 0747 0748 void parseFindResults(const QByteArray& searchStr) override; 0749 void parseAlbumResults(const QByteArray& albumStr) override; 0750 void sendFindQuery( 0751 const ServerImporterConfig* cfg, 0752 const QString& artist, const QString& album) override; 0753 void sendTrackListQuery( 0754 const ServerImporterConfig* cfg, 0755 const QString& cat, const QString& id) override; 0756 }; 0757 0758 DiscogsImporter::HtmlImpl::HtmlImpl(DiscogsImporter* importer) 0759 : BaseImpl(importer, "www.discogs.com") 0760 { 0761 m_discogsHeaders["User-Agent"] = 0762 "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) " 0763 "AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 " 0764 "Safari/6533.18.5"; 0765 m_discogsHeaders["Cookie"] = "language2=en"; 0766 } 0767 0768 DiscogsImporter::HtmlImpl::~HtmlImpl() 0769 { 0770 } 0771 0772 void DiscogsImporter::HtmlImpl::parseFindResults(const QByteArray& searchStr) 0773 { 0774 // releases have the format: 0775 // <a href="/artist/256076-Amon-Amarth">Amon Amarth</a> </span> - 0776 // <a class="search_result_title " href="/Amon-Amarth-The-Avenger/release/761529-Amon-Amarth-The-Avenger" data-followable="true">The Avenger</a> 0777 QString str = QString::fromUtf8(searchStr); 0778 QRegularExpression idTitleRe(QLatin1String( 0779 "href=\"/artist/[^>]+?>([^<]+?)</a>[^-]*?-" 0780 "\\s*?<a class=\"search_result_title[ \"]+?href=\"/([^/]*?/?release)/" 0781 "([0-9]+-[^\"]+?)\"[^>]*?>([^<]+?)</a>(.*?card_actions)"), 0782 QRegularExpression::DotMatchesEverythingOption); 0783 0784 QRegularExpression yearRe(QLatin1String("<span class=\"card_release_year\">([^<]+)</span>")); 0785 QRegularExpression formatRe(QLatin1String("<span class=\"card_release_format\">([^<]+)</span>")); 0786 0787 albumListModel()->clear(); 0788 auto it = idTitleRe.globalMatch(str); 0789 while (it.hasNext()) { 0790 auto idTitleMatch = it.next(); 0791 QString artist = fixUpArtist(idTitleMatch.captured(1).trimmed()); 0792 if (QString title = removeHtml(idTitleMatch.captured(4).trimmed()); 0793 !title.isEmpty()) { 0794 QString result(artist + QLatin1String(" - ") + title); 0795 0796 QString metadata = idTitleMatch.captured(5); 0797 if (auto yearMatch = yearRe.match(metadata); yearMatch.hasMatch()) { 0798 result.append(QLatin1String(" (") + yearMatch.captured(1).trimmed() + 0799 QLatin1Char(')')); 0800 } 0801 0802 if (auto formatMatch = formatRe.match(metadata); formatMatch.hasMatch()) { 0803 result.append(QLatin1String(" [") + formatMatch.captured(1).trimmed() + 0804 QLatin1Char(']')); 0805 } 0806 0807 albumListModel()->appendItem( 0808 result, 0809 idTitleMatch.captured(2), 0810 idTitleMatch.captured(3)); 0811 } 0812 } 0813 } 0814 0815 void DiscogsImporter::HtmlImpl::parseAlbumResults(const QByteArray& albumStr) 0816 { 0817 if (int jsonStart = albumStr.indexOf("<script id=\"dsdata\" type=\"application/json\">"); 0818 jsonStart >= 0) { 0819 jsonStart += 44; 0820 if (int jsonEnd = albumStr.indexOf("</script>", jsonStart); 0821 jsonEnd > jsonStart) { 0822 // We have JSON data inside the HTML output, if it is usable, we do not 0823 // have to parse the HTML output. 0824 if (auto doc = QJsonDocument::fromJson(albumStr.mid(jsonStart, jsonEnd - jsonStart)); 0825 !doc.isNull() && doc.isObject()) { 0826 if (const auto dataValue = doc.object().value(QLatin1String("data")); 0827 dataValue.isObject()) { 0828 const auto data = dataValue.toObject(); 0829 QJsonObject release; 0830 QString imgUrl; 0831 0832 // There are multiple releases in the embedded JSON, 0833 // try to find the correct one. 0834 const auto rootQuery = data.value(QLatin1String("ROOT_QUERY")) 0835 .toObject(); 0836 for (auto it = rootQuery.constBegin(); 0837 it != rootQuery.constEnd(); 0838 ++it) { 0839 if (QString releaseRef; 0840 it.key().startsWith(QLatin1String("release")) && 0841 !(releaseRef = it.value().toObject() 0842 .value(QLatin1String("__ref")).toString()).isEmpty()) { 0843 if (QJsonObject releaseObject = data.value(releaseRef).toObject(); 0844 releaseObject.contains(QLatin1String("tracks"))) { 0845 release = releaseObject; 0846 break; 0847 } 0848 } 0849 } 0850 0851 // There are multiple images in the embedded JSON, 0852 // try to find the correct one. 0853 if (!release.isEmpty()) { 0854 for (auto it = release.constBegin(); it != release.constEnd(); ++it) { 0855 if (QString imageRef; 0856 it.key().startsWith(QLatin1String("images")) && 0857 !(imageRef = it.value().toObject() 0858 .value(QLatin1String("edges")).toArray().first().toObject() 0859 .value(QLatin1String("node")).toObject() 0860 .value(QLatin1String("__ref")).toString()).isEmpty()) { 0861 imgUrl = extractUrlFromImageValue(data.value(imageRef)); 0862 } 0863 } 0864 } 0865 0866 for (auto it = data.constBegin(); it != data.constEnd(); ++it) { 0867 if (it.key().startsWith(QLatin1String("Release:"))) { 0868 if (QJsonValue releaseValue; 0869 release.isEmpty() && (releaseValue = it.value()).isObject()) { 0870 if (QJsonObject releaseObject = releaseValue.toObject(); 0871 releaseObject.contains(QLatin1String("tracks"))) { 0872 release = releaseObject; 0873 } 0874 } 0875 } else if (it.key().startsWith(QLatin1String("Image:"))) { 0876 if (imgUrl.isEmpty()) { 0877 imgUrl = extractUrlFromImageValue(it.value()); 0878 } 0879 } 0880 } 0881 if (!release.isEmpty()) { 0882 if (!imgUrl.isEmpty()) { 0883 release.insert(QLatin1String("images"), 0884 QJsonArray({QJsonObject({{QLatin1String("uri"), 0885 imgUrl}})})); 0886 } 0887 if (parseJsonAlbumResults(release, m_importer, trackDataModel(), 0888 data)) { 0889 return; 0890 } 0891 } 0892 } 0893 } 0894 } 0895 } 0896 0897 QRegularExpression nlSpaceRe(QLatin1String("[\r\n]+\\s*")); 0898 QRegularExpression atDiscogsRe(QLatin1String("\\s*\\([^)]+\\) (?:at|-|\\|) Discogs\n?$")); 0899 QString str = QString::fromUtf8(albumStr); 0900 str.remove(QLatin1String(" data-rh=\"\"")).remove(QLatin1String("<!-- -->")) 0901 .replace(QLatin1Char(' ') + QChar(0x2013) + QLatin1Char(' '), 0902 QLatin1String(" - ")); 0903 0904 FrameCollection framesHdr; 0905 int start, end; 0906 const bool standardTags = m_importer->getStandardTags(); 0907 if (standardTags) { 0908 /* 0909 * artist and album can be found in the title: 0910 <title>Amon Amarth - The Avenger (CD, Album, Dig) at Discogs</title> 0911 */ 0912 start = str.indexOf(QLatin1String("<title>")); 0913 if (start >= 0) { 0914 start += 7; // skip <title> 0915 end = str.indexOf(QLatin1String("</title>"), start); 0916 if (end > start) { 0917 QString titleStr = str.mid(start, end - start); 0918 titleStr.replace(atDiscogsRe, QLatin1String("")); 0919 // reduce new lines and space after them 0920 titleStr.replace(nlSpaceRe, QLatin1String(" ")); 0921 start = 0; 0922 end = titleStr.indexOf(QLatin1String(" - "), start); 0923 if (end > start) { 0924 framesHdr.setArtist(fixUpArtist(titleStr.mid(start, end - start))); 0925 start = end + 3; // skip " - " 0926 } 0927 framesHdr.setAlbum(removeHtml(titleStr.mid(start))); 0928 } 0929 } 0930 /* 0931 * the year can be found in "Released:" 0932 <div class="head">Released:</div><div class="content">02 Nov 1999</div> 0933 */ 0934 start = str.indexOf(QLatin1String("Released:<")); 0935 if (start >= 0) { 0936 start += 9; // skip "Released:" 0937 end = str.indexOf(QLatin1String("</div>"), start + 1); 0938 if (end > start) { 0939 QString yearStr = str.mid(start, end - start); 0940 // strip new lines and space after them 0941 yearStr.replace(nlSpaceRe, QLatin1String("")); 0942 yearStr = removeHtml(yearStr); // strip HTML tags and entities 0943 // this should skip day and month numbers 0944 QRegularExpression yearRe(QLatin1String("(\\d{4})")); 0945 if (auto match = yearRe.match(yearStr); match.hasMatch()) { 0946 framesHdr.setYear(match.captured(1).toInt()); 0947 } 0948 } 0949 } 0950 /* 0951 * the genre can be found in "Genre:" or "Style:" (lines with only whitespace 0952 * in between): 0953 <div class="head">Genre:</div><div class="content"> 0954 Rock 0955 </div> 0956 <div class="head">Style:</div><div class="content"> 0957 Viking Metal, 0958 Death Metal 0959 </div> 0960 */ 0961 // All genres found are checked for an ID3v1 number, starting with those 0962 // in the Style field. 0963 QStringList genreList; 0964 static const char* const fields[] = { "Style:", "Genre:" }; 0965 for (auto field : fields) { 0966 start = str.indexOf(QString::fromLatin1(field) + QLatin1Char('<')); 0967 if (start >= 0) { 0968 start += qstrlen(field); // skip field 0969 end = str.indexOf(QLatin1String("</div>"), start + 1); 0970 if (end > start) { 0971 QString genreStr = str.mid(start, end - start); 0972 // strip new lines and space after them 0973 genreStr.replace(nlSpaceRe, QLatin1String("")); 0974 genreStr = removeHtml(genreStr); // strip HTML tags and entities 0975 genreStr.remove(QLatin1String("RockStyle:")); 0976 genreStr.remove(QLatin1String("PopStyle:")); 0977 if (genreStr.indexOf(QLatin1Char(',')) >= 0) { 0978 genreList += genreStr.split(QRegularExpression(QLatin1String(",\\s*"))); 0979 } else { 0980 if (!genreStr.isEmpty()) { 0981 genreList += genreStr; 0982 } 0983 } 0984 } 0985 } 0986 } 0987 QStringList genres; 0988 for (auto it = genreList.begin(); it != genreList.end();) { 0989 if (int genreNum = Genres::getNumber(*it); genreNum != 255) { 0990 genres.append(QString::fromLatin1(Genres::getName(genreNum))); 0991 it = genreList.erase(it); 0992 } else { 0993 ++it; 0994 } 0995 } 0996 genres.append(genreList); 0997 genres.removeDuplicates(); 0998 if (!genres.isEmpty()) { 0999 framesHdr.setGenre(Frame::joinStringList(genres)); 1000 } 1001 } 1002 1003 const bool additionalTags = m_importer->getAdditionalTags(); 1004 if (additionalTags) { 1005 /* 1006 * publisher can be found in "Label:" 1007 */ 1008 start = str.indexOf(QLatin1String("Label:<")); 1009 if (start >= 0) { 1010 start += 6; // skip "Label:" 1011 end = str.indexOf(QLatin1String("</div>"), start + 1); 1012 if (int anchorEnd = str.indexOf(QLatin1String("</a>"), start + 1); 1013 anchorEnd > start && anchorEnd < end) { 1014 end = anchorEnd; 1015 } 1016 if (end > start) { 1017 QString labelStr = str.mid(start, end - start); 1018 // strip new lines and space after them 1019 labelStr.replace(nlSpaceRe, QLatin1String("")); 1020 labelStr = fixUpArtist(labelStr); 1021 QRegularExpression catNoRe(QLatin1String(" \\s*(?:‎)?- +(\\S[^,]*[^, ])")); 1022 if (auto match = catNoRe.match(labelStr); match.hasMatch()) { 1023 int catNoPos = match.capturedStart(); 1024 QString catNo = match.captured(1); 1025 labelStr.truncate(catNoPos); 1026 if (!catNo.isEmpty()) { 1027 framesHdr.setValue(Frame::FT_CatalogNumber, catNo); 1028 } 1029 } 1030 if (labelStr != QLatin1String("Not On Label")) { 1031 framesHdr.setValue(Frame::FT_Publisher, fixUpArtist(labelStr)); 1032 } 1033 } 1034 } 1035 1036 /* 1037 * media can be found in "Format:" 1038 */ 1039 start = str.indexOf(QLatin1String("Format:<")); 1040 if (start >= 0) { 1041 start += 7; // skip "Format:" 1042 end = str.indexOf(QLatin1String("</div>"), start + 1); 1043 if (end > start) { 1044 QString mediaStr = str.mid(start, end - start); 1045 // strip new lines and space after them 1046 mediaStr.replace(nlSpaceRe, QLatin1String("")); 1047 mediaStr = removeHtml(mediaStr); // strip HTML tags and entities 1048 framesHdr.setValue(Frame::FT_Media, mediaStr); 1049 } 1050 } 1051 1052 /* 1053 * Release country can be found in "Country:" 1054 */ 1055 start = str.indexOf(QLatin1String("Country:<")); 1056 if (start >= 0) { 1057 start += 8; // skip "Country:" 1058 end = str.indexOf(QLatin1String("</div>"), start + 1); 1059 if (int anchorEnd = str.indexOf(QLatin1String("</a>"), start + 1); 1060 anchorEnd > start && anchorEnd < end) { 1061 end = anchorEnd; 1062 } 1063 if (end > start) { 1064 QString countryStr = str.mid(start, end - start); 1065 // strip new lines and space after them 1066 countryStr.replace(nlSpaceRe, QLatin1String("")); 1067 countryStr = removeHtml(countryStr); // strip HTML tags and entities 1068 framesHdr.setValue(Frame::FT_ReleaseCountry, countryStr); 1069 } 1070 } 1071 1072 /* 1073 * credits can be found in "Credits" 1074 */ 1075 start = str.indexOf(QLatin1String(">Credits</h")); 1076 if (start >= 0) { 1077 start += 13; // skip "Credits" plus end of element (e.g. "3>") 1078 end = str.indexOf(QLatin1String("</div>"), start + 1); 1079 if (end > start) { 1080 QString creditsStr = str.mid(start, end - start); 1081 // strip new lines and space after them 1082 creditsStr.replace(nlSpaceRe, QLatin1String("")); 1083 creditsStr.replace(QLatin1String("<br />"), QLatin1String("\n")); 1084 creditsStr.replace(QLatin1String("</li>"), QLatin1String("\n")); 1085 creditsStr.replace(QLatin1String("–"), QLatin1String(" - ")); 1086 creditsStr = removeHtml(creditsStr); // strip HTML tags and entities 1087 parseCredits(creditsStr, framesHdr); 1088 } 1089 } 1090 } 1091 1092 ImportTrackDataVector trackDataVector(trackDataModel()->getTrackData()); 1093 trackDataVector.setCoverArtUrl(QUrl()); 1094 if (m_importer->getCoverArt()) { 1095 /* 1096 * cover art can be found in image source 1097 */ 1098 // Using a raw string literal in the next line would disturb doxygen. 1099 start = str.indexOf(QLatin1String("<meta property=\"og:image\" content=\"")); 1100 if (start >= 0) { 1101 start += 35; 1102 end = str.indexOf(QLatin1String("\""), start); 1103 if (end > start) { 1104 trackDataVector.setCoverArtUrl(QUrl(str.mid(start, end - start))); 1105 } 1106 } 1107 } 1108 1109 /* 1110 * album tracks have the format (lines with only whitespace in between): 1111 <div id="tracklist" class="section tracklist" data-toggle="tracklist"> 1112 <td class="tracklist_track_pos">1</td> 1113 <span class="tracklist_track_title" itemprop="name">Bleed For Ancient Gods</span> 1114 <td width="25" class="tracklist_track_duration"> 1115 <meta itemprop="duration" content="PT0H04M31S"> 1116 <span>4:31</span> 1117 </td> 1118 1119 <h1>Tracklist</h1> 1120 <div class="section_content"> 1121 <table> 1122 <tr class="first"> 1123 <td class="track_pos">1</td> 1124 <td> </td> 1125 <td class="track_title">Bleed For Ancient Gods</td> 1126 <td class="track_duration">4:31</td> 1127 <td class="track_itunes"></td> 1128 </tr> 1129 <tr> 1130 <td class="track_pos">2</td> 1131 (..) 1132 </table> 1133 * 1134 * Variations: strange track numbers, no durations, links instead of tracks, 1135 * only "track" instead of "track_title", align attribute in "track_duration" 1136 */ 1137 start = str.indexOf(QLatin1String("id=\"release-tracklist\"")); 1138 if (start >= 0) { 1139 end = str.indexOf(QLatin1String("</table>"), start); 1140 if (end > start) { 1141 str = str.mid(start, end - start); 1142 // strip whitespace 1143 str.replace(nlSpaceRe, QLatin1String("")); 1144 1145 FrameCollection frames(framesHdr); 1146 QRegularExpression posRe(QLatin1String( 1147 R"(<td [^>]*class="trackPos[^"]*">(\d+)</td>)")); 1148 QRegularExpression artistsRe(QLatin1String( 1149 "class=\"trackArtist[^\"]*\">(?:<span[^>]*>)?" 1150 "<a href=\"/artist/[^>]+>([^<]+)</a>")); 1151 QRegularExpression moreArtistsRe(QLatin1String( 1152 "^([^<>]+)<a href=\"/artist/[^>]+>([^<]+)</a>")); 1153 QRegularExpression titleRe(QLatin1String( 1154 "<span class=\"trackTitle[^\"]*\"[^>]*>([^<]+)<")); 1155 QRegularExpression durationRe(QLatin1String( 1156 "<td [^>]*class=\"duration[^\"]*\"[^>]*>(?:<meta[^>]*>)?" 1157 "(?:<span>)?(\\d+):(\\d+)</")); 1158 QRegularExpression indexRe(QLatin1String("<td class=\"track_index\">([^<]+)$")); 1159 QRegularExpression rowEndRe(QLatin1String(R"(</td>[\s\r\n]*</tr>)")); 1160 auto it = trackDataVector.begin(); 1161 bool atTrackDataListEnd = it == trackDataVector.end(); 1162 int trackNr = 1; 1163 start = 0; 1164 auto rowEndIt = rowEndRe.globalMatch(str); 1165 while (rowEndIt.hasNext()) { 1166 auto rowEndMatch = rowEndIt.next(); 1167 end = rowEndMatch.capturedStart(); 1168 QString trackDataStr = str.mid(start, end - start); 1169 QString title; 1170 int duration = 0; 1171 int pos = trackNr; 1172 auto match = titleRe.match(trackDataStr); 1173 if (match.hasMatch()) { 1174 title = removeHtml(match.captured(1)); 1175 } 1176 match = durationRe.match(trackDataStr); 1177 if (match.hasMatch()) { 1178 duration = match.captured(1).toInt() * 60 + 1179 match.captured(2).toInt(); 1180 } 1181 match = posRe.match(trackDataStr); 1182 if (match.hasMatch()) { 1183 pos = match.captured(1).toInt(); 1184 } 1185 if (additionalTags) { 1186 match = artistsRe.match(trackDataStr); 1187 if (match.hasMatch()) { 1188 // use the artist in the header as the album artist 1189 // and the artist in the track as the artist 1190 QString artist(fixUpArtist(match.captured(1))); 1191 // Look if there are more artists 1192 int artistEndPos = match.capturedEnd(); 1193 #if QT_VERSION >= 0x060000 1194 auto moreArtistsIt = moreArtistsRe.globalMatch( 1195 trackDataStr, artistEndPos, QRegularExpression::NormalMatch, 1196 QRegularExpression::AnchorAtOffsetMatchOption); 1197 #else 1198 auto moreArtistsIt = moreArtistsRe.globalMatch( 1199 trackDataStr, artistEndPos, QRegularExpression::NormalMatch, 1200 QRegularExpression::AnchoredMatchOption); 1201 #endif 1202 while (moreArtistsIt.hasNext()) { 1203 match = moreArtistsIt.next(); 1204 artist += match.captured(1); 1205 artist += fixUpArtist(match.captured(2)); 1206 } 1207 if (standardTags) { 1208 frames.setArtist(artist); 1209 } 1210 frames.setValue(Frame::FT_AlbumArtist, framesHdr.getArtist()); 1211 } 1212 } 1213 start = end + 10; // skip </td></tr> 1214 match = indexRe.match(trackDataStr); 1215 if (match.hasMatch()) { 1216 if (additionalTags) { 1217 QString subtitle(removeHtml(match.captured(1))); 1218 framesHdr.setValue(Frame::FT_Description, subtitle); 1219 frames.setValue(Frame::FT_Description, subtitle); 1220 } 1221 continue; 1222 } 1223 if (additionalTags) { 1224 if (int blockquoteStart = 1225 trackDataStr.indexOf(QLatin1String("<blockquote>")); 1226 blockquoteStart >= 0) { 1227 blockquoteStart += 12; 1228 int blockquoteEnd = 1229 trackDataStr.indexOf(QLatin1String("</blockquote>"), 1230 blockquoteStart); 1231 if (blockquoteEnd == -1) { 1232 // If the element is not correctly closed, search for </span> 1233 blockquoteEnd = trackDataStr.indexOf(QLatin1String("</span>"), 1234 blockquoteStart); 1235 } 1236 if (blockquoteEnd > blockquoteStart) { 1237 QString blockquoteStr(trackDataStr.mid(blockquoteStart, 1238 blockquoteEnd - blockquoteStart)); 1239 // additional track info like "Music By, Lyrics By - " 1240 blockquoteStr.replace(QLatin1String("<br />"), 1241 QLatin1String("\n")); 1242 blockquoteStr.replace(QLatin1String("</li>"), 1243 QLatin1String("\n")); 1244 blockquoteStr.replace(QLatin1String("</span>"), 1245 QLatin1String("\n")); 1246 blockquoteStr.replace(QLatin1String(" – "), 1247 QLatin1String(" - ")); 1248 blockquoteStr.replace(QLatin1String("–"), 1249 QLatin1String(" - ")); 1250 blockquoteStr = removeHtml(blockquoteStr); 1251 parseCredits(blockquoteStr, frames); 1252 } 1253 } 1254 } 1255 1256 if (!title.isEmpty() || duration != 0) { 1257 if (standardTags) { 1258 frames.setTrack(pos); 1259 frames.setTitle(title); 1260 } 1261 if (atTrackDataListEnd) { 1262 ImportTrackData trackData; 1263 trackData.setFrameCollection(frames); 1264 trackData.setImportDuration(duration); 1265 trackDataVector.push_back(trackData); 1266 } else { 1267 while (!atTrackDataListEnd && !it->isEnabled()) { 1268 ++it; 1269 atTrackDataListEnd = it == trackDataVector.end(); 1270 } 1271 if (!atTrackDataListEnd) { 1272 it->setFrameCollection(frames); 1273 it->setImportDuration(duration); 1274 ++it; 1275 atTrackDataListEnd = it == trackDataVector.end(); 1276 } 1277 } 1278 ++trackNr; 1279 } 1280 frames = framesHdr; 1281 } 1282 1283 // handle redundant tracks 1284 frames.clear(); 1285 while (!atTrackDataListEnd) { 1286 if (it->isEnabled()) { 1287 if (it->getFileDuration() == 0) { 1288 it = trackDataVector.erase(it); 1289 } else { 1290 it->setFrameCollection(frames); 1291 it->setImportDuration(0); 1292 ++it; 1293 } 1294 } else { 1295 ++it; 1296 } 1297 atTrackDataListEnd = it == trackDataVector.end(); 1298 } 1299 } 1300 } 1301 trackDataModel()->setTrackData(trackDataVector); 1302 } 1303 1304 void DiscogsImporter::HtmlImpl::sendFindQuery( 1305 const ServerImporterConfig*, 1306 const QString& artist, const QString& album) 1307 { 1308 /* 1309 * Query looks like this: 1310 * http://www.discogs.com/search/?q=amon+amarth+avenger&type=release&layout=sm 1311 */ 1312 m_importer->sendRequest(QString::fromLatin1(m_discogsServer), 1313 QString(QLatin1String("/search/?q=")) + 1314 encodeUrlQuery(artist + QLatin1Char(' ') + album) + 1315 QLatin1String("&type=release&layout=sm"), QLatin1String("https"), 1316 m_discogsHeaders); 1317 } 1318 1319 void DiscogsImporter::HtmlImpl::sendTrackListQuery( 1320 const ServerImporterConfig*, const QString& cat, const QString& id) 1321 { 1322 /* 1323 * Query looks like this: 1324 * http://www.discogs.com/release/761529 1325 */ 1326 m_importer->sendRequest(QString::fromLatin1(m_discogsServer), QLatin1Char('/') + 1327 cat + 1328 QLatin1Char('/') + id, QLatin1String("https"), m_discogsHeaders); 1329 } 1330 1331 1332 /** 1333 * Importer implementation to import JSON data via the Discogs API. 1334 * A token is required to get data from the Discogs API. 1335 */ 1336 class DiscogsImporter::JsonImpl : public DiscogsImporter::BaseImpl { 1337 public: 1338 explicit JsonImpl(DiscogsImporter* importer); 1339 ~JsonImpl() override; 1340 1341 void parseFindResults(const QByteArray& searchStr) override; 1342 void parseAlbumResults(const QByteArray& albumStr) override; 1343 void sendFindQuery( 1344 const ServerImporterConfig* cfg, 1345 const QString& artist, const QString& album) override; 1346 void sendTrackListQuery( 1347 const ServerImporterConfig* cfg, 1348 const QString& cat, const QString& id) override; 1349 }; 1350 1351 DiscogsImporter::JsonImpl::JsonImpl(DiscogsImporter* importer) 1352 : BaseImpl(importer, "api.discogs.com") 1353 { 1354 m_discogsHeaders["User-Agent"] = "Kid3/" VERSION 1355 " +https://kid3.kde.org"; 1356 } 1357 1358 DiscogsImporter::JsonImpl::~JsonImpl() 1359 { 1360 } 1361 1362 void DiscogsImporter::JsonImpl::parseFindResults(const QByteArray& searchStr) 1363 { 1364 // search results have the format (JSON, simplified): 1365 // {"results": [{"style": ["Heavy Metal"], "title": "Wizard (23) - Odin", 1366 // "type": "release", "id": 2487778}]} 1367 albumListModel()->clear(); 1368 if (auto doc = QJsonDocument::fromJson(searchStr); !doc.isNull()) { 1369 auto obj = doc.object(); 1370 const auto results = obj.value(QLatin1String("results")).toArray(); 1371 for (const auto& val : results) { 1372 auto result = val.toObject(); 1373 if (QString title = 1374 fixUpArtist(result.value(QLatin1String("title")).toString()); 1375 !title.isEmpty()) { 1376 if (QString year = result.value(QLatin1String("year")).toString().trimmed(); 1377 !year.isEmpty()) { 1378 title += QLatin1String(" (") + year + QLatin1Char(')'); 1379 } 1380 if (const auto fmts = result.value(QLatin1String("format")).toArray(); 1381 !fmts.isEmpty()) { 1382 QStringList formats; 1383 for (const auto& fmt : fmts) { 1384 if (QString format = fmt.toString().trimmed(); !format.isEmpty()) { 1385 formats.append(format); 1386 } 1387 } 1388 if (!formats.isEmpty()) { 1389 title += QLatin1String(" [") + 1390 formats.join(QLatin1String(", ")) + 1391 QLatin1Char(']'); 1392 } 1393 } 1394 albumListModel()->appendItem( 1395 title, 1396 QLatin1String("releases"), 1397 QString::number(result.value(QLatin1String("id")).toInt())); 1398 } 1399 } 1400 } 1401 } 1402 1403 void DiscogsImporter::JsonImpl::parseAlbumResults(const QByteArray& albumStr) 1404 { 1405 auto doc = QJsonDocument::fromJson(albumStr); 1406 if (doc.isNull()) { 1407 return; 1408 } 1409 auto map = doc.object(); 1410 if (map.isEmpty()) { 1411 return; 1412 } 1413 1414 parseJsonAlbumResults(map, m_importer, trackDataModel()); 1415 } 1416 1417 void DiscogsImporter::JsonImpl::sendFindQuery( 1418 const ServerImporterConfig*, 1419 const QString& artist, const QString& album) 1420 { 1421 // Query looks like this: 1422 // http://api.discogs.com//database/search?type=release&title&q=amon+amarth+avenger 1423 m_importer->sendRequest(QString::fromLatin1(m_discogsServer), 1424 QLatin1String("/database/search?type=release&title&q=") + 1425 encodeUrlQuery(artist + QLatin1Char(' ') + album), QLatin1String("https"), 1426 m_discogsHeaders); 1427 } 1428 1429 void DiscogsImporter::JsonImpl::sendTrackListQuery( 1430 const ServerImporterConfig*, const QString& cat, const QString& id) 1431 { 1432 // Query looks like this: 1433 // http://api.discogs.com/releases/761529 1434 m_importer->sendRequest(QString::fromLatin1(m_discogsServer), QLatin1Char('/') + 1435 cat + 1436 QLatin1Char('/') + id, QLatin1String("https"), m_discogsHeaders); 1437 } 1438 1439 1440 /** 1441 * Constructor. 1442 * 1443 * @param netMgr network access manager 1444 * @param trackDataModel track data to be filled with imported values 1445 */ 1446 DiscogsImporter::DiscogsImporter(QNetworkAccessManager* netMgr, 1447 TrackDataModel* trackDataModel) 1448 : ServerImporter(netMgr, trackDataModel), 1449 m_htmlImpl(new HtmlImpl(this)), m_jsonImpl(new JsonImpl(this)), 1450 m_impl(m_htmlImpl) 1451 { 1452 setObjectName(QLatin1String("DiscogsImporter")); 1453 } 1454 1455 /** 1456 * Destructor. 1457 */ 1458 DiscogsImporter::~DiscogsImporter() 1459 { 1460 m_impl = nullptr; 1461 delete m_jsonImpl; 1462 delete m_htmlImpl; 1463 } 1464 1465 /** 1466 * Name of import source. 1467 * @return name. 1468 */ 1469 const char* DiscogsImporter::name() const { 1470 return QT_TRANSLATE_NOOP("@default", "Discogs"); 1471 } 1472 1473 /** anchor to online help, 0 to disable */ 1474 const char* DiscogsImporter::helpAnchor() const { return "import-discogs"; } 1475 1476 /** configuration, 0 if not used */ 1477 ServerImporterConfig* DiscogsImporter::config() const { 1478 return &DiscogsConfig::instance(); 1479 } 1480 1481 /** additional tags option, false if not used */ 1482 bool DiscogsImporter::additionalTags() const { return true; } 1483 1484 /** 1485 * Process finished findCddbAlbum request. 1486 * 1487 * @param searchStr search data received 1488 */ 1489 void DiscogsImporter::parseFindResults(const QByteArray& searchStr) 1490 { 1491 m_impl->parseFindResults(searchStr); 1492 } 1493 1494 /** 1495 * Parse result of album request and populate m_trackDataModel with results. 1496 * 1497 * @param albumStr album data received 1498 */ 1499 void DiscogsImporter::parseAlbumResults(const QByteArray& albumStr) 1500 { 1501 m_impl->parseAlbumResults(albumStr); 1502 } 1503 1504 /** 1505 * Send a query command to search on the server. 1506 * 1507 * @param cfg import source configuration 1508 * @param artist artist to search 1509 * @param album album to search 1510 */ 1511 void DiscogsImporter::sendFindQuery( 1512 const ServerImporterConfig* cfg, 1513 const QString& artist, const QString& album) 1514 { 1515 // If an URL is entered in the first search field, its result will be directly 1516 // available in the album results list. 1517 if (artist.startsWith(QLatin1String("https://www.discogs.com/"))) { 1518 constexpr int catBegin = 24; 1519 if (int catEnd = artist.indexOf(QLatin1Char('/'), catBegin); 1520 catEnd > catBegin) { 1521 m_htmlImpl->albumListModel()->clear(); 1522 m_htmlImpl->albumListModel()->appendItem( 1523 artist, 1524 artist.mid(catBegin, catEnd - catBegin), 1525 artist.mid(catEnd + 1)); 1526 return; 1527 } 1528 } 1529 m_impl = selectImpl(cfg); 1530 m_impl->sendFindQuery(cfg, artist, album); 1531 } 1532 1533 /** 1534 * Send a query command to fetch the track list 1535 * from the server. 1536 * 1537 * @param cfg import source configuration 1538 * @param cat category 1539 * @param id ID 1540 */ 1541 void DiscogsImporter::sendTrackListQuery( 1542 const ServerImporterConfig* cfg, const QString& cat, const QString& id) 1543 { 1544 m_impl = selectImpl(cfg); 1545 m_impl->sendTrackListQuery(cfg, cat, id); 1546 } 1547 1548 /** 1549 * Set token to access Discogs API. 1550 * You have to create an account on Discogs and then generate a token 1551 * (Settings/Developers, Generate new token). The token can then be used for 1552 * the "Discogs Auth Flow" in the header "Authorization: Discogs token=value" 1553 * If a token is found in the configuration, the importer using the Discogs 1554 * API is used, else the HTML importer. 1555 * @param cfg configuration which can contain token 1556 */ 1557 DiscogsImporter::BaseImpl* DiscogsImporter::selectImpl( 1558 const ServerImporterConfig* cfg) const 1559 { 1560 if (cfg) { 1561 if (QByteArray token = cfg->property("token").toByteArray(); 1562 !token.isEmpty()) { 1563 m_jsonImpl->headers()["Authorization"] = "Discogs token=" + token; 1564 return m_jsonImpl; 1565 } 1566 } 1567 return m_htmlImpl; 1568 }