/* This file is part of Clementine. Copyright 2010, David Sansome Clementine is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Clementine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Clementine. If not, see . */ #include "albumcoverfetcher.h" #include "albumcoverfetchersearch.h" #include "coverprovider.h" #include "coverproviders.h" #include "core/logging.h" #include "core/network.h" #include #include #include #include #include const int AlbumCoverFetcherSearch::kSearchTimeoutMs = 10000; const int AlbumCoverFetcherSearch::kImageLoadTimeoutMs = 2500; const int AlbumCoverFetcherSearch::kTargetSize = 500; const float AlbumCoverFetcherSearch::kGoodScore = 1.85; AlbumCoverFetcherSearch::AlbumCoverFetcherSearch(const CoverSearchRequest& request, QNetworkAccessManager* network, QObject* parent) : QObject(parent), request_(request), image_load_timeout_(new NetworkTimeouts(kImageLoadTimeoutMs, this)), network_(network) { // we will terminate the search after kSearchTimeoutMs miliseconds if we are not // able to find all of the results before that point in time QTimer::singleShot(kSearchTimeoutMs, this, SLOT(TerminateSearch())); } void AlbumCoverFetcherSearch::TerminateSearch() { foreach (int id, pending_requests_.keys()) { pending_requests_.take(id)->CancelSearch(id); } AllProvidersFinished(); } void AlbumCoverFetcherSearch::Start() { CoverProviders* providers = &CoverProviders::instance(); foreach(CoverProvider* provider, providers->List()) { connect(provider, SIGNAL(SearchFinished(int,QList)), SLOT(ProviderSearchFinished(int,QList))); const int id = providers->NextId(); const bool success = provider->StartSearch( request_.artist, request_.album, id); if (success) { pending_requests_[id] = provider; } } // end this search before it even began if there are no providers... if(pending_requests_.isEmpty()) { TerminateSearch(); } } static bool CompareCategories(const CoverSearchResult& a, const CoverSearchResult& b) { return a.category < b.category; } void AlbumCoverFetcherSearch::ProviderSearchFinished( int id, const QList& results) { if (!pending_requests_.contains(id)) return; CoverProvider* provider = pending_requests_.take(id); CoverSearchResults results_copy(results); // Add categories to the results if the provider didn't specify them for (int i=0 ; iname(); } } // Add results from the current provider to our pool results_.append(results_copy); // do we have more providers left? if(!pending_requests_.isEmpty()) { return; } AllProvidersFinished(); } void AlbumCoverFetcherSearch::AllProvidersFinished() { // if we only wanted to do the search then we're done if (request_.search) { emit SearchFinished(request_.id, results_); return; } // no results? if (results_.isEmpty()) { emit AlbumCoverFetched(request_.id, QImage()); return; } // Now we have to load some images and figure out which one is the best. // We'll sort the list of results by category, then load the first few images // from each category and use some heuristics to score them. If no images // are good enough we'll keep loading more images until we find one that is // or we run out of results. qStableSort(results_.begin(), results_.end(), CompareCategories); FetchMoreImages(); } void AlbumCoverFetcherSearch::FetchMoreImages() { // Try the first one in each category. QString last_category; for (int i=0 ; iget(QNetworkRequest(result.image_url)); connect(image_reply, SIGNAL(finished()), SLOT(ProviderCoverFetchFinished())); pending_image_loads_ << image_reply; image_load_timeout_->AddReply(image_reply); } if (pending_image_loads_.isEmpty()) { // There were no more results? Time to give up. SendBestImage(); } } void AlbumCoverFetcherSearch::ProviderCoverFetchFinished() { QNetworkReply* reply = qobject_cast(sender()); reply->deleteLater(); pending_image_loads_.removeAll(reply); if (reply->error() != QNetworkReply::NoError) { qLog(Info) << "Error requesting" << reply->url() << reply->errorString(); } else { QImage image; if (!image.loadFromData(reply->readAll())) { qLog(Info) << "Error decoding image data from" << reply->url(); } else { const float score = ScoreImage(image); candidate_images_.insertMulti(score, image); qLog(Debug) << reply->url() << "scored" << score; } } if (pending_image_loads_.isEmpty()) { // We've fetched everything we wanted to fetch for now, check if we have an // image that's good enough. float best_score = 0.0; if (!candidate_images_.isEmpty()) { best_score = candidate_images_.keys().last(); } qLog(Debug) << "Best image so far has a score of" << best_score; if (best_score >= kGoodScore) { SendBestImage(); } else { FetchMoreImages(); } } } float AlbumCoverFetcherSearch::ScoreImage(const QImage& image) const { // Invalid images score nothing if (image.isNull()) { return 0.0; } // A 500x500px image scores 1.0, bigger scores higher const float size_score = std::sqrt(float(image.width() * image.height())) / kTargetSize; // A 1:1 image scores 1.0, anything else scores less const float aspect_score = 1.0 - float(image.height() - image.width()) / std::max(image.height(), image.width()); return size_score + aspect_score; } void AlbumCoverFetcherSearch::SendBestImage() { QImage image; if (!candidate_images_.isEmpty()) { image = candidate_images_.values().back(); } emit AlbumCoverFetched(request_.id, image); }