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 }