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 }