File indexing completed on 2024-05-19 04:55:58
0001 /** 0002 * \file batchimporter.cpp 0003 * Batch importer. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 3 Jan 2013 0008 * 0009 * Copyright (C) 2013-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 "batchimporter.h" 0028 #include "serverimporter.h" 0029 #include "trackdatamodel.h" 0030 #include "downloadclient.h" 0031 #include "pictureframe.h" 0032 #include "fileconfig.h" 0033 #include "formatconfig.h" 0034 0035 /** 0036 * Flags to store types of data which have to be imported. 0037 */ 0038 enum DataFlags { 0039 StandardTags = 1, 0040 AdditionalTags = 2, 0041 CoverArt = 4 0042 }; 0043 0044 /** 0045 * Constructor. 0046 * @param netMgr network access manager 0047 */ 0048 BatchImporter::BatchImporter(QNetworkAccessManager* netMgr) 0049 : QObject(netMgr), 0050 m_downloadClient(new DownloadClient(netMgr)), 0051 m_currentImporter(nullptr), m_trackDataModel(nullptr), m_albumModel(nullptr), 0052 m_tagVersion(Frame::TagNone), m_state(Idle), 0053 m_trackListNr(-1), m_sourceNr(-1), m_albumNr(-1), 0054 m_requestedData(0), m_importedData(0) 0055 { 0056 connect(m_downloadClient, &DownloadClient::downloadFinished, 0057 this, &BatchImporter::onImageDownloaded); 0058 m_frameFilter.enableAll(); 0059 } 0060 0061 /** 0062 * Set importers. 0063 * @param importers available importers 0064 * @param trackDataModel track data model used by importers 0065 */ 0066 void BatchImporter::setImporters(const QList<ServerImporter*>& importers, 0067 TrackDataModel* trackDataModel) 0068 { 0069 m_importers = importers; 0070 m_trackDataModel = trackDataModel; 0071 } 0072 0073 /** 0074 * Start batch import. 0075 * @param trackLists list of track data vectors with album tracks 0076 * @param profile batch import profile 0077 * @param tagVersion import destination tag version 0078 */ 0079 void BatchImporter::start(const QList<ImportTrackDataVector>& trackLists, 0080 const BatchImportProfile& profile, 0081 Frame::TagVersion tagVersion) 0082 { 0083 m_trackLists = trackLists; 0084 m_profile = profile; 0085 m_tagVersion = tagVersion; 0086 emit reportImportEvent(Started, profile.getName()); 0087 m_trackListNr = -1; 0088 m_state = CheckNextTrackList; 0089 stateTransition(); 0090 } 0091 0092 /** 0093 * Check if operation is aborted. 0094 * 0095 * @return true if aborted. 0096 */ 0097 bool BatchImporter::isAborted() const 0098 { 0099 return m_state == ImportAborted; 0100 } 0101 0102 /** 0103 * Clear state which is reported by isAborted(). 0104 */ 0105 void BatchImporter::clearAborted() 0106 { 0107 if (m_state == ImportAborted) { 0108 m_state = Idle; 0109 stateTransition(); 0110 } 0111 } 0112 0113 /** 0114 * Abort batch import. 0115 */ 0116 void BatchImporter::abort() 0117 { 0118 State oldState = m_state; 0119 m_state = ImportAborted; 0120 if (oldState == Idle) { 0121 stateTransition(); 0122 } else if (oldState == GettingCover) { 0123 m_downloadClient->cancelDownload(); 0124 stateTransition(); 0125 } 0126 } 0127 0128 void BatchImporter::stateTransition() 0129 { 0130 switch (m_state) { 0131 case Idle: 0132 m_trackListNr = -1; 0133 break; 0134 case CheckNextTrackList: 0135 if (m_trackDataModel) { 0136 bool searchKeyFound = false; 0137 forever { 0138 ++m_trackListNr; 0139 if (m_trackListNr < 0 || m_trackListNr >= m_trackLists.size()) { 0140 break; 0141 } 0142 if (const ImportTrackDataVector& trackList = m_trackLists.at(m_trackListNr); 0143 !trackList.isEmpty()) { 0144 m_currentArtist = trackList.getArtist(); 0145 m_currentAlbum = trackList.getAlbum(); 0146 if (m_currentArtist.isEmpty() && m_currentAlbum.isEmpty()) { 0147 // No tags available, try to guess artist and album from file name 0148 if (TaggedFile* taggedFile = trackList.first().getTaggedFile()) { 0149 FrameCollection frames; 0150 taggedFile->getTagsFromFilename(frames, 0151 FileConfig::instance().fromFilenameFormat()); 0152 m_currentArtist = frames.getArtist(); 0153 m_currentAlbum = frames.getAlbum(); 0154 } 0155 } 0156 if (!m_currentArtist.isEmpty() || !m_currentAlbum.isEmpty()) { 0157 m_trackDataModel->setTrackData(trackList); 0158 searchKeyFound = true; 0159 break; 0160 } 0161 } 0162 } 0163 if (searchKeyFound) { 0164 m_sourceNr = -1; 0165 m_importedData = 0; 0166 m_state = CheckNextSource; 0167 } else { 0168 emit reportImportEvent(Finished, QString()); 0169 emit finished(); 0170 m_state = Idle; 0171 } 0172 stateTransition(); 0173 } 0174 break; 0175 case CheckNextSource: 0176 m_currentImporter = nullptr; 0177 forever { 0178 ++m_sourceNr; 0179 if (m_sourceNr < 0 || m_sourceNr >= m_profile.getSources().size()) { 0180 break; 0181 } 0182 if (const BatchImportProfile::Source& profileSource = 0183 m_profile.getSources().at(m_sourceNr); 0184 (m_currentImporter = getImporter(profileSource.getName())) != nullptr) { 0185 m_requestedData = 0; 0186 if (profileSource.standardTagsEnabled()) 0187 m_requestedData |= StandardTags; 0188 if (m_currentImporter->additionalTags()) { 0189 if (profileSource.additionalTagsEnabled()) 0190 m_requestedData |= AdditionalTags; 0191 if (profileSource.coverArtEnabled()) 0192 m_requestedData |= CoverArt; 0193 } 0194 break; 0195 } 0196 } 0197 if (m_currentImporter) { 0198 emit reportImportEvent(SourceSelected, 0199 QString::fromLatin1(m_currentImporter->name())); 0200 m_state = GettingAlbumList; 0201 } else { 0202 m_state = CheckNextTrackList; 0203 } 0204 stateTransition(); 0205 break; 0206 case GettingAlbumList: 0207 if (m_currentImporter) { 0208 emit reportImportEvent(QueryingAlbumList, 0209 m_currentArtist + QLatin1String(" - ") + m_currentAlbum); 0210 m_albumNr = -1; 0211 m_albumModel = nullptr; 0212 connect(m_currentImporter, &ImportClient::findFinished, 0213 this, &BatchImporter::onFindFinished); 0214 connect(m_currentImporter, &HttpClient::progress, 0215 this, &BatchImporter::onFindProgress); 0216 m_currentImporter->find(m_currentImporter->config(), 0217 m_currentArtist, m_currentAlbum); 0218 } 0219 break; 0220 case CheckNextAlbum: 0221 m_albumListItemId.clear(); 0222 forever { 0223 ++m_albumNr; 0224 if (!m_albumModel || 0225 m_albumNr < 0 || m_albumNr >= m_albumModel->rowCount()) { 0226 break; 0227 } 0228 m_albumModel->getItem(m_albumNr, m_albumListItemText, 0229 m_albumListItemCategory, 0230 m_albumListItemId); 0231 if (!m_albumListItemId.isEmpty()) { 0232 break; 0233 } 0234 } 0235 if (!m_albumListItemId.isEmpty()) { 0236 m_state = GettingTracks; 0237 } else { 0238 m_state = CheckNextSource; 0239 } 0240 stateTransition(); 0241 break; 0242 case GettingTracks: 0243 if (!m_albumListItemId.isEmpty() && m_currentImporter) { 0244 emit reportImportEvent(FetchingTrackList, 0245 m_albumListItemText); 0246 int pendingData = m_requestedData & ~m_importedData; 0247 // Also fetch standard tags, so that accuracy can be measured 0248 m_currentImporter->setStandardTags( 0249 pendingData & (StandardTags | AdditionalTags | CoverArt)); 0250 m_currentImporter->setAdditionalTags(pendingData & AdditionalTags); 0251 m_currentImporter->setCoverArt(pendingData & CoverArt); 0252 connect(m_currentImporter, &ImportClient::albumFinished, 0253 this, &BatchImporter::onAlbumFinished); 0254 connect(m_currentImporter, &HttpClient::progress, 0255 this, &BatchImporter::onAlbumProgress); 0256 m_currentImporter->getTrackList(m_currentImporter->config(), 0257 m_albumListItemCategory, 0258 m_albumListItemId); 0259 } 0260 break; 0261 case GettingCover: 0262 if (m_trackDataModel) { 0263 QUrl imgUrl; 0264 if (m_tagVersion & Frame::tagVersionFromNumber(Frame::Tag_Picture)) { 0265 if (QUrl coverArtUrl = m_trackDataModel->getTrackData().getCoverArtUrl(); 0266 !coverArtUrl.isEmpty()) { 0267 imgUrl = DownloadClient::getImageUrl(coverArtUrl); 0268 if (!imgUrl.isEmpty()) { 0269 emit reportImportEvent(FetchingCoverArt, 0270 coverArtUrl.toString()); 0271 m_downloadClient->startDownload(imgUrl); 0272 } 0273 } 0274 } 0275 if (imgUrl.isEmpty()) { 0276 m_state = CheckIfDone; 0277 stateTransition(); 0278 } 0279 } 0280 break; 0281 case CheckIfDone: 0282 if (m_requestedData & ~m_importedData) { 0283 m_state = CheckNextAlbum; 0284 } else { 0285 m_state = CheckNextTrackList; 0286 } 0287 stateTransition(); 0288 break; 0289 case ImportAborted: 0290 emit reportImportEvent(Aborted, QString()); 0291 break; 0292 } 0293 } 0294 0295 void BatchImporter::onFindFinished(const QByteArray& searchStr) 0296 { 0297 disconnect(m_currentImporter, &ImportClient::findFinished, 0298 this, &BatchImporter::onFindFinished); 0299 disconnect(m_currentImporter, &HttpClient::progress, 0300 this, &BatchImporter::onFindProgress); 0301 if (m_state == ImportAborted) { 0302 stateTransition(); 0303 } else if (m_currentImporter) { 0304 m_currentImporter->parseFindResults(searchStr); 0305 m_albumModel = m_currentImporter->getAlbumListModel(); 0306 m_state = CheckNextAlbum; 0307 stateTransition(); 0308 } 0309 } 0310 0311 void BatchImporter::onFindProgress(const QString& text, int step, int total) 0312 { 0313 if (step == -1 && total == -1) { 0314 disconnect(m_currentImporter, &ImportClient::findFinished, 0315 this, &BatchImporter::onFindFinished); 0316 disconnect(m_currentImporter, &HttpClient::progress, 0317 this, &BatchImporter::onFindProgress); 0318 emit reportImportEvent(Error, text); 0319 m_state = CheckNextAlbum; 0320 stateTransition(); 0321 } 0322 } 0323 0324 void BatchImporter::onAlbumFinished(const QByteArray& albumStr) 0325 { 0326 disconnect(m_currentImporter, &ImportClient::albumFinished, 0327 this, &BatchImporter::onAlbumFinished); 0328 disconnect(m_currentImporter, &HttpClient::progress, 0329 this, &BatchImporter::onAlbumProgress); 0330 if (m_state == ImportAborted) { 0331 stateTransition(); 0332 } else if (m_trackDataModel && m_currentImporter) { 0333 m_currentImporter->parseAlbumResults(albumStr); 0334 0335 int accuracy = m_trackDataModel->calculateAccuracy(); 0336 emit reportImportEvent(TrackListReceived, 0337 tr("Accuracy") + QLatin1Char(' ') + 0338 (accuracy >= 0 0339 ? QString::number(accuracy) + QLatin1Char('%') 0340 : tr("Unknown"))); 0341 if (const BatchImportProfile::Source& profileSource = 0342 m_profile.getSources().at(m_sourceNr); 0343 accuracy >= profileSource.getRequiredAccuracy()) { 0344 if (m_requestedData & (StandardTags | AdditionalTags)) { 0345 // Set imported data in tags of files. 0346 ImportTrackDataVector trackDataVector(m_trackDataModel->getTrackData()); 0347 for (auto it = trackDataVector.begin(); it != trackDataVector.end(); ++it) { 0348 if (TaggedFile* taggedFile = it->getTaggedFile()) { 0349 taggedFile->readTags(false); 0350 it->removeDisabledFrames(m_frameFilter); 0351 TagFormatConfig::instance().formatFramesIfEnabled(*it); 0352 FOR_TAGS_IN_MASK(tagNr, m_tagVersion) { 0353 taggedFile->setFrames(tagNr, *it, false); 0354 } 0355 } 0356 } 0357 trackDataVector.setCoverArtUrl(QUrl()); 0358 m_trackLists[m_trackListNr] = trackDataVector; 0359 } else { 0360 // Revert imported data. 0361 ImportTrackDataVector trackDataVector(m_trackLists.at(m_trackListNr)); 0362 trackDataVector.setCoverArtUrl( 0363 m_trackDataModel->getTrackData().getCoverArtUrl()); 0364 m_trackDataModel->setTrackData(trackDataVector); 0365 } 0366 0367 if (m_requestedData & StandardTags) 0368 m_importedData |= StandardTags; 0369 if (m_requestedData & AdditionalTags) 0370 m_importedData |= AdditionalTags; 0371 } else { 0372 // Accuracy not sufficient => Revert imported data, check next album. 0373 m_trackDataModel->setTrackData(m_trackLists.at(m_trackListNr)); 0374 } 0375 m_state = GettingCover; 0376 stateTransition(); 0377 } 0378 } 0379 0380 void BatchImporter::onAlbumProgress(const QString& text, int step, int total) 0381 { 0382 if (step == -1 && total == -1) { 0383 disconnect(m_currentImporter, &ImportClient::albumFinished, 0384 this, &BatchImporter::onAlbumFinished); 0385 disconnect(m_currentImporter, &HttpClient::progress, 0386 this, &BatchImporter::onAlbumProgress); 0387 emit reportImportEvent(Error, text); 0388 m_state = GettingCover; 0389 stateTransition(); 0390 } 0391 } 0392 0393 void BatchImporter::onImageDownloaded(const QByteArray& data, 0394 const QString& mimeType, const QString& url) 0395 { 0396 if (m_state == ImportAborted) { 0397 stateTransition(); 0398 } else { 0399 if (data.size() >= 1024) { 0400 if (mimeType.startsWith(QLatin1String("image")) && m_trackDataModel) { 0401 emit reportImportEvent(CoverArtReceived, url); 0402 PictureFrame frame(data, url, PictureFrame::PT_CoverFront, mimeType); 0403 ImportTrackDataVector trackDataVector(m_trackDataModel->getTrackData()); 0404 for (auto it = trackDataVector.begin(); it != trackDataVector.end(); ++it) { 0405 if (TaggedFile* taggedFile = it->getTaggedFile()) { 0406 taggedFile->readTags(false); 0407 taggedFile->addFrame(Frame::Tag_Picture, frame); 0408 } 0409 } 0410 m_importedData |= CoverArt; 0411 } 0412 } else { 0413 // Probably an invalid 1x1 picture from Amazon 0414 emit reportImportEvent(CoverArtReceived, 0415 tr("Invalid File")); 0416 } 0417 m_state = CheckIfDone; 0418 stateTransition(); 0419 } 0420 } 0421 0422 ServerImporter* BatchImporter::getImporter(const QString& name) 0423 { 0424 const auto importers = m_importers; 0425 for (ServerImporter* importer : importers) { 0426 if (QString::fromLatin1(importer->name()) == name) { 0427 return importer; 0428 } 0429 } 0430 return nullptr; 0431 }