File indexing completed on 2024-05-19 15:54:40
0001 // SPDX-FileCopyrightText: 2023 Jonah BrĂ¼chert <jbb@kaidan.im> 0002 // 0003 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 0005 #include "thumbnailsource.h" 0006 0007 #include <QStandardPaths> 0008 #include <QNetworkRequest> 0009 #include <QNetworkReply> 0010 #include <QDir> 0011 #include <QtConcurrent> 0012 #include <QGuiApplication> 0013 #include <QImage> 0014 #include <QStringBuilder> 0015 0016 #include "asyncytmusic.h" 0017 #include "library.h" 0018 0019 void ThumbnailSource::setVideoId(const QString &id) { 0020 if (m_videoId == id) { 0021 return; 0022 } 0023 0024 m_videoId = id; 0025 Q_EMIT videoIdChanged(); 0026 setCachedPath({}); 0027 0028 const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) % QDir::separator() % "thumbnails"; 0029 QDir(cacheDir).mkpath(QStringLiteral(".")); 0030 0031 // Clear cache if it is old, so people can profit from memory usage improvements from downscaling, 0032 // and get the new cropped thumbnails 0033 auto cacheVersionFile = QString(cacheDir % "/.cache_version"); 0034 0035 constexpr auto CURRENT_CACHE_VERSION = 1; 0036 0037 auto getCacheVersion = [cacheVersionFile]() { 0038 QFile file(cacheVersionFile); 0039 if (!file.open(QFile::ReadOnly)) { 0040 return 0; 0041 } 0042 auto version = file.read(3); // Read at most three characters, we will not need more soon 0043 return version.toInt(); 0044 }; 0045 0046 if (!QFile::exists(cacheVersionFile) || getCacheVersion() < CURRENT_CACHE_VERSION) { 0047 qDebug() << "Deleting and re-generating thumbnail cache"; 0048 0049 QDir dir(cacheDir); 0050 const auto entries = dir.entryList(QDir::Files); 0051 for (const auto &thumbnail : entries) { 0052 if (thumbnail.endsWith(QLatin1String(".webp"))) { 0053 QFile::remove(cacheDir % "/" % thumbnail); 0054 } 0055 } 0056 QFile file(cacheVersionFile); 0057 if (file.open(QFile::WriteOnly)) { 0058 file.seek(0); 0059 file.write(QString::number(CURRENT_CACHE_VERSION).toUtf8()); 0060 } 0061 } 0062 0063 const QString cacheLocation = cacheDir % QDir::separator() % id % ".webp"; 0064 0065 if (QFile::exists(cacheLocation)) { 0066 setCachedPath(QUrl::fromLocalFile(cacheLocation)); 0067 return; 0068 } 0069 0070 auto *reply = Library::instance().nam().get(QNetworkRequest(QUrl("https://i.ytimg.com/vi_webp/" % m_videoId % "/maxresdefault.webp"))); 0071 0072 auto storeResult = [this, cacheLocation, id](QNetworkReply *reply) { 0073 if (reply->error() != QNetworkReply::NoError) { 0074 return; 0075 } 0076 auto data = reply->readAll(); 0077 reply->deleteLater(); 0078 auto future = QtConcurrent::run([data = std::move(data), cacheLocation]() { 0079 // Scale cover down to save memory 0080 int targetHeight = 200 * qGuiApp->devicePixelRatio(); 0081 auto scaled = QImage::fromData(data) 0082 .scaledToHeight(targetHeight); 0083 0084 int targetLeft = scaled.width() / 2 - targetHeight / 2; 0085 auto cropped = scaled 0086 .copy(QRect(targetLeft, 0, targetHeight, targetHeight)); 0087 0088 cropped.save(cacheLocation); 0089 }); 0090 0091 QCoro::connect(std::move(future), this, [this, cacheLocation, id]() { 0092 // Check if video id was changed since we started fetching 0093 if (id == m_videoId) { 0094 setCachedPath(QUrl::fromLocalFile(cacheLocation)); 0095 } 0096 }); 0097 }; 0098 0099 connect(reply, &QNetworkReply::errorOccurred, this, [this, storeResult](auto error) { 0100 if (error == QNetworkReply::NetworkError::ContentNotFoundError) { 0101 qDebug() << "Naive thumbnail resolution failed, falling back to yt-dlp (slower)"; 0102 0103 QCoro::connect(YTMusicThread::instance()->extractVideoInfo(m_videoId), this, [this, storeResult](auto info) { 0104 auto *reply = Library::instance().nam().get(QNetworkRequest(QUrl(QString::fromStdString(info.thumbnail)))); 0105 connect(reply, &QNetworkReply::finished, this, [reply, storeResult]() { 0106 storeResult(reply); 0107 }); 0108 }); 0109 } 0110 }); 0111 0112 connect(reply, &QNetworkReply::finished, this, [reply, storeResult]() { 0113 storeResult(reply); 0114 }); 0115 }