File indexing completed on 2024-05-19 04:56:26
0001 /** 0002 * \file freedbimporter.cpp 0003 * freedb.org importer. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 18 Jan 2004 0008 * 0009 * Copyright (C) 2004-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 "freedbimporter.h" 0028 #include <QRegularExpression> 0029 #include "serverimporterconfig.h" 0030 #include "trackdatamodel.h" 0031 #include "freedbconfig.h" 0032 #include "config.h" 0033 #include "genres.h" 0034 0035 namespace { 0036 0037 constexpr char gnudbServer[] = "www.gnudb.org:80"; 0038 0039 } 0040 0041 /** 0042 * Constructor. 0043 * 0044 * @param netMgr network access manager 0045 * @param trackDataModel track data to be filled with imported values 0046 */ 0047 FreedbImporter::FreedbImporter(QNetworkAccessManager* netMgr, 0048 TrackDataModel *trackDataModel) 0049 : ServerImporter(netMgr, trackDataModel) 0050 { 0051 setObjectName(QLatin1String("FreedbImporter")); 0052 } 0053 0054 /** 0055 * Name of import source. 0056 * @return name. 0057 */ 0058 const char* FreedbImporter::name() const { return QT_TRANSLATE_NOOP("@default", "gnudb.org"); } 0059 0060 /** NULL-terminated array of server strings, 0 if not used */ 0061 const char** FreedbImporter::serverList() const 0062 { 0063 static const char* servers[] = { 0064 "www.gnudb.org:80", 0065 "gnudb.gnudb.org:80", 0066 "freedb.org:80", 0067 "freedb.freedb.org:80", 0068 "at.freedb.org:80", 0069 "au.freedb.org:80", 0070 "ca.freedb.org:80", 0071 "es.freedb.org:80", 0072 "fi.freedb.org:80", 0073 "lu.freedb.org:80", 0074 "ru.freedb.org:80", 0075 "uk.freedb.org:80", 0076 "us.freedb.org:80", 0077 nullptr // end of StrList 0078 }; 0079 return servers; 0080 } 0081 0082 /** default server, 0 to disable */ 0083 const char* FreedbImporter::defaultServer() const { return "www.gnudb.org:80"; } 0084 0085 /** default CGI path, 0 to disable */ 0086 const char* FreedbImporter::defaultCgiPath() const { return "/~cddb/cddb.cgi"; } 0087 0088 /** anchor to online help, 0 to disable */ 0089 const char* FreedbImporter::helpAnchor() const { return "import-freedb"; } 0090 0091 /** configuration, 0 if not used */ 0092 ServerImporterConfig* FreedbImporter::config() const { return &FreedbConfig::instance(); } 0093 0094 /** 0095 * Process finished findCddbAlbum request. 0096 * 0097 * @param searchStr search data received 0098 */ 0099 void FreedbImporter::parseFindResults(const QByteArray& searchStr) 0100 { 0101 /* 0102 <h2>Search Results, 1 albums found:</h2> 0103 <br><br> 0104 <a href="http://www.gnudb.org/cd/ro920b810c"><b>Catharsis / Imago</b></a><br> 0105 Tracks: 12, total time: 49:07, year: 2002, genre: Metal<br> 0106 <a href="http://www.gnudb.org/gnudb/rock/920b810c" target=_blank>Discid: rock / 920b810c</a><br> 0107 */ 0108 bool isUtf8 = false; 0109 if (int charSetPos = searchStr.indexOf("charset="); charSetPos != -1) { 0110 charSetPos += 8; 0111 QByteArray charset(searchStr.mid(charSetPos, 5)); 0112 isUtf8 = charset == "utf-8" || charset == "UTF-8"; 0113 } 0114 QString str = isUtf8 ? QString::fromUtf8(searchStr) 0115 : QString::fromLatin1(searchStr); 0116 QRegularExpression titleRe(QLatin1String(R"(<a href="[^"]+/cd/[^"]+"><b>([^<]+)</b></a>)")); 0117 QRegularExpression catIdRe(QLatin1String("Discid: ([a-z]+)[\\s/]+([0-9a-f]+)")); 0118 QStringList lines = str.split(QRegularExpression(QLatin1String("[\\r\\n]+"))); 0119 QString title; 0120 bool inEntries = false; 0121 m_albumListModel->clear(); 0122 for (auto it = lines.constBegin(); it != lines.constEnd(); ++it) { 0123 if (inEntries) { 0124 auto match = titleRe.match(*it); 0125 if (match.hasMatch()) { 0126 title = match.captured(1); 0127 } 0128 match = catIdRe.match(*it); 0129 if (match.hasMatch()) { 0130 m_albumListModel->appendItem( 0131 title, 0132 match.captured(1), 0133 match.captured(2)); 0134 } 0135 } else if (it->indexOf(QLatin1String(" albums found:")) != -1) { 0136 inEntries = true; 0137 } 0138 } 0139 } 0140 0141 namespace { 0142 0143 /** 0144 * Parse the track durations from freedb.org. 0145 * 0146 * @param text text buffer containing data from freedb.org 0147 * @param trackDuration list for results 0148 */ 0149 void parseFreedbTrackDurations( 0150 const QString& text, 0151 QList<int>& trackDuration) 0152 { 0153 /* Example freedb format: 0154 # Track frame offsets: 0155 # 150 0156 # 2390 0157 # 23387 0158 # 44650 0159 # 61322 0160 # 94605 0161 # 121710 0162 # 144637 0163 # 176820 0164 # 187832 0165 # 218930 0166 # 0167 # Disc length: 3114 seconds 0168 */ 0169 trackDuration.clear(); 0170 QRegularExpression discLenRe(QLatin1String("Disc length:\\s*\\d+")); 0171 if (auto match = discLenRe.match(text); match.hasMatch()) { 0172 int discLenPos = match.capturedStart(); 0173 int len = match.capturedLength(); 0174 discLenPos += 12; 0175 #if QT_VERSION >= 0x060000 0176 int discLen = text.mid(discLenPos, len - 12).toInt(); 0177 #else 0178 int discLen = text.midRef(discLenPos, len - 12).toInt(); 0179 #endif 0180 if (int trackOffsetPos = text.indexOf(QLatin1String("Track frame offsets"), 0); 0181 trackOffsetPos != -1) { 0182 int lastOffset = -1; 0183 QRegularExpression re(QLatin1String("#\\s*\\d+")); 0184 auto it = re.globalMatch(text); 0185 while (it.hasNext()) { 0186 auto toMatch = it.next(); 0187 trackOffsetPos = toMatch.capturedStart(); 0188 if (trackOffsetPos >= discLenPos) { 0189 break; 0190 } 0191 len = toMatch.capturedLength(); 0192 trackOffsetPos += 1; 0193 #if QT_VERSION >= 0x060000 0194 int trackOffset = text.mid(trackOffsetPos, len - 1).toInt(); 0195 #else 0196 int trackOffset = text.midRef(trackOffsetPos, len - 1).toInt(); 0197 #endif 0198 if (lastOffset != -1) { 0199 int duration = (trackOffset - lastOffset) / 75; 0200 trackDuration.append(duration); 0201 } 0202 lastOffset = trackOffset; 0203 } 0204 if (lastOffset != -1) { 0205 int duration = (discLen * 75 - lastOffset) / 75; 0206 trackDuration.append(duration); 0207 } 0208 } 0209 } 0210 } 0211 0212 /** 0213 * Parse the album specific data (artist, album, year, genre) from freedb.org. 0214 * 0215 * @param text text buffer containing data from freedb.org 0216 * @param frames tags to put result 0217 */ 0218 void parseFreedbAlbumData(const QString& text, FrameCollection& frames) 0219 { 0220 QRegularExpression fdre(QLatin1String(R"(DTITLE=\s*(\S[^\r\n]*\S)\s*/\s*(\S[^\r\n]*\S)[\r\n])")); 0221 auto match = fdre.match(text); 0222 if (match.hasMatch()) { 0223 frames.setArtist(match.captured(1)); 0224 frames.setAlbum(match.captured(2)); 0225 } 0226 fdre.setPattern(QLatin1String(R"(EXTD=[^\r\n]*YEAR:\s*(\d+)\D)")); 0227 match = fdre.match(text); 0228 if (match.hasMatch()) { 0229 frames.setYear(match.captured(1).toInt()); 0230 } 0231 fdre.setPattern(QLatin1String(R"(EXTD=[^\r\n]*ID3G:\s*(\d+)\D)")); 0232 match = fdre.match(text); 0233 if (match.hasMatch()) { 0234 frames.setGenre(QString::fromLatin1(Genres::getName(match.captured(1).toInt()))); 0235 } 0236 } 0237 0238 } 0239 0240 /** 0241 * Parse result of album request and populate m_trackDataModel with results. 0242 * 0243 * @param albumStr album data received 0244 */ 0245 void FreedbImporter::parseAlbumResults(const QByteArray& albumStr) 0246 { 0247 QString text = QString::fromUtf8(albumStr); 0248 FrameCollection framesHdr; 0249 QList<int> trackDuration; 0250 parseFreedbTrackDurations(text, trackDuration); 0251 parseFreedbAlbumData(text, framesHdr); 0252 0253 ImportTrackDataVector trackDataVector(m_trackDataModel->getTrackData()); 0254 trackDataVector.setCoverArtUrl(QUrl()); 0255 FrameCollection frames(framesHdr); 0256 auto it = trackDataVector.begin(); 0257 auto tdit = trackDuration.constBegin(); 0258 bool atTrackDataListEnd = it == trackDataVector.end(); 0259 int tracknr = 0; 0260 for (;;) { 0261 QRegularExpression fdre(QString(QLatin1String(R"(TTITLE%1=([^\r\n]+)[\r\n])")).arg(tracknr)); 0262 QString title; 0263 auto fdIt = fdre.globalMatch(text); 0264 while (fdIt.hasNext()) { 0265 auto match = fdIt.next(); 0266 title += match.captured(1); 0267 } 0268 if (!title.isNull()) { 0269 frames.setTrack(tracknr + 1); 0270 frames.setTitle(title); 0271 } else { 0272 break; 0273 } 0274 int duration = tdit != trackDuration.constEnd() ? 0275 *tdit++ : 0; 0276 if (atTrackDataListEnd) { 0277 ImportTrackData trackData; 0278 trackData.setFrameCollection(frames); 0279 trackData.setImportDuration(duration); 0280 trackDataVector.push_back(trackData); 0281 } else { 0282 while (!atTrackDataListEnd && !it->isEnabled()) { 0283 ++it; 0284 atTrackDataListEnd = it == trackDataVector.end(); 0285 } 0286 if (!atTrackDataListEnd) { 0287 it->setFrameCollection(frames); 0288 it->setImportDuration(duration); 0289 ++it; 0290 atTrackDataListEnd = it == trackDataVector.end(); 0291 } 0292 } 0293 frames = framesHdr; 0294 ++tracknr; 0295 } 0296 frames.clear(); 0297 while (!atTrackDataListEnd) { 0298 if (it->isEnabled()) { 0299 if (it->getFileDuration() == 0) { 0300 it = trackDataVector.erase(it); 0301 } else { 0302 it->setFrameCollection(frames); 0303 it->setImportDuration(0); 0304 ++it; 0305 } 0306 } else { 0307 ++it; 0308 } 0309 atTrackDataListEnd = it == trackDataVector.end(); 0310 } 0311 m_trackDataModel->setTrackData(trackDataVector); 0312 } 0313 0314 /** 0315 * Send a query command in to search on the server. 0316 * 0317 * @param artist artist to search 0318 * @param album album to search 0319 */ 0320 void FreedbImporter::sendFindQuery( 0321 const ServerImporterConfig*, 0322 const QString& artist, const QString& album) 0323 { 0324 // If an URL is entered in the first search field, its result will be directly 0325 // available in the album results list. 0326 if (artist.startsWith(QLatin1String("https://gnudb.org/"))) { 0327 constexpr int catBegin = 18; 0328 if (int catEnd = artist.indexOf(QLatin1Char('/'), catBegin); 0329 catEnd > catBegin) { 0330 static QStringList categories({ 0331 QLatin1String("blues"), 0332 QLatin1String("classical"), 0333 QLatin1String("country"), 0334 QLatin1String("data"), 0335 QLatin1String("folk"), 0336 QLatin1String("jazz"), 0337 QLatin1String("newage"), 0338 QLatin1String("reggae"), 0339 QLatin1String("rock"), 0340 QLatin1String("soundtrack"), 0341 QLatin1String("misc") 0342 }); 0343 QString id = artist.mid(catEnd + 1); 0344 for (const auto& category : categories) { 0345 if (id.startsWith(category.mid(0, 2))) { 0346 m_albumListModel->clear(); 0347 m_albumListModel->appendItem(artist, category, id.mid(2)); 0348 return; 0349 } 0350 } 0351 } 0352 } 0353 // At the moment, only www.gnudb.org has a working search 0354 // so we always use this server for find queries. 0355 sendRequest(QString::fromLatin1(gnudbServer), QLatin1String("/search/") + 0356 encodeUrlQuery(artist + QLatin1Char(' ') + album)); 0357 } 0358 0359 /** 0360 * Send a query command to fetch the track list 0361 * from the server. 0362 * 0363 * @param cfg import source configuration 0364 * @param cat category 0365 * @param id ID 0366 */ 0367 void FreedbImporter::sendTrackListQuery( 0368 const ServerImporterConfig* cfg, const QString& cat, const QString& id) 0369 { 0370 sendRequest(cfg->server(), 0371 cfg->cgiPath() + QLatin1String("?cmd=cddb+read+") + cat + QLatin1Char('+') + id + 0372 QLatin1String("&hello=noname+localhost+Kid3+" VERSION "&proto=6")); 0373 }