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 }