/* This file was part of Clementine. Copyright 2012, David Sansome Strawberry 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. Strawberry 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 Strawberry. If not, see . */ #include "moodbarloader.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/application.h" #include "core/logging.h" #include "moodbarpipeline.h" #include "settings/moodbarsettingspage.h" using namespace std::chrono_literals; #ifdef Q_OS_WIN32 # include #endif MoodbarLoader::MoodbarLoader(Application *app, QObject *parent) : QObject(parent), cache_(new QNetworkDiskCache(this)), thread_(new QThread(this)), kMaxActiveRequests(qMax(1, QThread::idealThreadCount() / 2)), save_(false) { cache_->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/moodbar"); cache_->setMaximumCacheSize(60 * 1024 * 1024); // 60MB - enough for 20,000 moodbars QObject::connect(app, &Application::SettingsChanged, this, &MoodbarLoader::ReloadSettings); ReloadSettings(); } MoodbarLoader::~MoodbarLoader() { thread_->quit(); thread_->wait(1000); } void MoodbarLoader::ReloadSettings() { QSettings s; s.beginGroup(MoodbarSettingsPage::kSettingsGroup); save_ = s.value("save", false).toBool(); s.endGroup(); MaybeTakeNextRequest(); } QStringList MoodbarLoader::MoodFilenames(const QString &song_filename) { const QFileInfo file_info(song_filename); const QString dir_path(file_info.dir().path()); const QString mood_filename = file_info.completeBaseName() + ".mood"; return QStringList() << dir_path + "/." + mood_filename << dir_path + "/" + mood_filename; } MoodbarLoader::Result MoodbarLoader::Load(const QUrl &url, QByteArray *data, MoodbarPipeline **async_pipeline) { if (url.scheme() != "file") { return CannotLoad; } // Are we in the middle of loading this moodbar already? if (requests_.contains(url)) { *async_pipeline = requests_[url]; return WillLoadAsync; } // Check if a mood file exists for this file already const QString filename(url.toLocalFile()); for (const QString &possible_mood_file : MoodFilenames(filename)) { QFile f(possible_mood_file); if (f.exists()) { if (f.open(QIODevice::ReadOnly)) { qLog(Info) << "Loading moodbar data from" << possible_mood_file; *data = f.readAll(); f.close(); return Loaded; } else { qLog(Error) << "Failed to load moodbar data from" << possible_mood_file << f.errorString(); } } } // Maybe it exists in the cache? std::unique_ptr cache_device(cache_->data(url)); if (cache_device) { qLog(Info) << "Loading cached moodbar data for" << filename; *data = cache_device->readAll(); if (!data->isEmpty()) { return Loaded; } } if (!thread_->isRunning()) thread_->start(QThread::IdlePriority); // There was no existing file, analyze the audio file and create one. MoodbarPipeline *pipeline = new MoodbarPipeline(url); pipeline->moveToThread(thread_); QObject::connect(pipeline, &MoodbarPipeline::Finished, this, [this, pipeline, url]() { RequestFinished(pipeline, url); }); requests_[url] = pipeline; queued_requests_ << url; MaybeTakeNextRequest(); *async_pipeline = pipeline; return WillLoadAsync; } void MoodbarLoader::MaybeTakeNextRequest() { Q_ASSERT(QThread::currentThread() == qApp->thread()); if (active_requests_.count() >= kMaxActiveRequests || queued_requests_.isEmpty()) { return; } const QUrl url = queued_requests_.takeFirst(); active_requests_ << url; qLog(Info) << "Creating moodbar data for" << url.toLocalFile(); QMetaObject::invokeMethod(requests_[url], "Start", Qt::QueuedConnection); } void MoodbarLoader::RequestFinished(MoodbarPipeline *request, const QUrl &url) { Q_ASSERT(QThread::currentThread() == qApp->thread()); if (request->success()) { qLog(Info) << "Moodbar data generated successfully for" << url.toLocalFile(); // Save the data in the cache QNetworkCacheMetaData metadata; metadata.setUrl(url); QIODevice *cache_file = cache_->prepare(metadata); if (cache_file) { if (cache_file->write(request->data()) > 0) { cache_->insert(cache_file); } } // Save the data alongside the original as well if we're configured to. if (save_) { QList mood_filenames = MoodFilenames(url.toLocalFile()); const QString mood_filename(mood_filenames[0]); QFile mood_file(mood_filename); if (mood_file.open(QIODevice::WriteOnly)) { if (mood_file.write(request->data()) <= 0) { qLog(Error) << "Error writing to mood file" << mood_filename << mood_file.errorString(); } mood_file.close(); #ifdef Q_OS_WIN32 if (!SetFileAttributes(reinterpret_cast(mood_filename.utf16()), FILE_ATTRIBUTE_HIDDEN)) { qLog(Warning) << "Error setting hidden attribute for file" << mood_filename; } #endif } else { qLog(Error) << "Error opening mood file" << mood_filename << "for writing:" << mood_file.errorString(); } } } // Remove the request from the active list and delete it requests_.remove(url); active_requests_.remove(url); QTimer::singleShot(1s, request, &MoodbarLoader::deleteLater); MaybeTakeNextRequest(); }