feat: cache lyrics

This commit is contained in:
2 * r + 2 * t 2026-05-05 02:08:28 +10:00
parent ac24024c03
commit 2512347179
2 changed files with 81 additions and 1 deletions

View file

@ -5,6 +5,7 @@
#include "../Config/userpaths.hpp" #include "../Config/userpaths.hpp"
#include <qdiriterator.h> #include <qdiriterator.h>
#include <qfileinfo.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qnetworkcookiejar.h> #include <qnetworkcookiejar.h>
#include <qsavefile.h> #include <qsavefile.h>
@ -135,6 +136,21 @@ void Lyrics::setSelectedCandidate(const LyricCandidate& value) {
cancelInFlight(); cancelInFlight();
const int reqId = newRequestId(); const int reqId = newRequestId();
if (b == LRCLIB || b == NetEase) {
const QString cached = readCachedLrc(b, value.id());
if (!cached.isEmpty()) {
const auto lines = parseLrc(cached);
if (!lines.isEmpty()) {
setLines(lines, b);
setLoading(false);
if (!m_settingFromPrefs) {
persistTrackPrefs();
}
return;
}
}
}
if (b == LRCLIB) { if (b == LRCLIB) {
fetchLrclibById(value.id(), reqId); fetchLrclibById(value.id(), reqId);
} else if (b == NetEase) { } else if (b == NetEase) {
@ -520,6 +536,7 @@ void Lyrics::tryLrclib(int reqId) {
return; return;
} }
writeCachedLrc(LRCLIB, QString::number(id), synced);
setLines(lines, LRCLIB); setLines(lines, LRCLIB);
const LyricCandidate cand(LRCLIB, QString::number(id), obj.value(u"trackName"_s).toString(), const LyricCandidate cand(LRCLIB, QString::number(id), obj.value(u"trackName"_s).toString(),
obj.value(u"artistName"_s).toString(), obj.value(u"albumName"_s).toString(), obj.value(u"artistName"_s).toString(), obj.value(u"albumName"_s).toString(),
@ -677,7 +694,7 @@ void Lyrics::fetchLrclibById(const QString& id, int reqId) {
auto* reply = getJson(url, lrclibHeaders()); auto* reply = getJson(url, lrclibHeaders());
trackReply(reqId, reply); trackReply(reqId, reply);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId] { QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, reqId, id] {
reply->deleteLater(); reply->deleteLater();
if (reqId != m_currentRequestId) { if (reqId != m_currentRequestId) {
return; return;
@ -694,6 +711,7 @@ void Lyrics::fetchLrclibById(const QString& id, int reqId) {
setLoading(false); setLoading(false);
return; return;
} }
writeCachedLrc(LRCLIB, id, synced);
setLines(parseLrc(synced), LRCLIB); setLines(parseLrc(synced), LRCLIB);
setLoading(false); setLoading(false);
}); });
@ -728,6 +746,7 @@ void Lyrics::fetchNetEaseLyricsById(const QString& id, int reqId) {
setLoading(false); setLoading(false);
return; return;
} }
writeCachedLrc(NetEase, id, lrc);
setLines(parseLrc(lrc), NetEase); setLines(parseLrc(lrc), NetEase);
setLoading(false); setLoading(false);
}); });
@ -870,6 +889,62 @@ LyricsBackend Lyrics::backendFromKey(const QString& key) {
return Auto; return Auto;
} }
const QString& Lyrics::cacheDir() {
static const QString s_dir = [] {
QString cache = qEnvironmentVariable("XDG_CACHE_HOME");
if (cache.isEmpty()) {
cache = QDir::homePath() + u"/.cache"_s;
}
return cache + u"/caelestia/lyrics"_s;
}();
return s_dir;
}
QString Lyrics::cachePathFor(LyricsBackend backend, const QString& id) {
if (id.isEmpty() || backend == Auto || backend == Local) {
return {};
}
return u"%1/%2/%3.lrc"_s.arg(cacheDir(), backendKey(backend), sanitizeFilenamePart(id));
}
QString Lyrics::readCachedLrc(LyricsBackend backend, const QString& id) {
const QString path = cachePathFor(backend, id);
if (path.isEmpty()) {
return {};
}
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
return {};
}
return QString::fromUtf8(f.readAll());
}
void Lyrics::writeCachedLrc(LyricsBackend backend, const QString& id, const QString& text) {
if (text.isEmpty()) {
return;
}
const QString path = cachePathFor(backend, id);
if (path.isEmpty()) {
return;
}
QDir().mkpath(QFileInfo(path).absolutePath());
QSaveFile out(path);
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
qCWarning(lcLyrics) << "cannot open" << path << "for write:" << out.errorString();
return;
}
const QByteArray bytes = text.toUtf8();
if (out.write(bytes) != bytes.size()) {
qCWarning(lcLyrics) << "short write to" << path;
out.cancelWriting();
return;
}
if (!out.commit()) {
qCWarning(lcLyrics) << "commit failed for" << path << ":" << out.errorString();
}
}
QString Lyrics::tryReadLocalLrc(const QString& dir, const QString& artist, const QString& title) { QString Lyrics::tryReadLocalLrc(const QString& dir, const QString& artist, const QString& title) {
if (artist.isEmpty() && title.isEmpty()) { if (artist.isEmpty() && title.isEmpty()) {
return {}; return {};

View file

@ -107,6 +107,11 @@ private:
[[nodiscard]] static QString backendKey(LyricsBackend value); [[nodiscard]] static QString backendKey(LyricsBackend value);
[[nodiscard]] static LyricsBackend backendFromKey(const QString& key); [[nodiscard]] static LyricsBackend backendFromKey(const QString& key);
[[nodiscard]] static const QString& cacheDir();
[[nodiscard]] static QString cachePathFor(LyricsBackend backend, const QString& id);
[[nodiscard]] static QString readCachedLrc(LyricsBackend backend, const QString& id);
static void writeCachedLrc(LyricsBackend backend, const QString& id, const QString& text);
[[nodiscard]] static QVector<LyricLine> parseLrc(const QString& text); [[nodiscard]] static QVector<LyricLine> parseLrc(const QString& text);
[[nodiscard]] static QString tryReadLocalLrc(const QString& dir, const QString& artist, const QString& title); [[nodiscard]] static QString tryReadLocalLrc(const QString& dir, const QString& artist, const QString& title);
[[nodiscard]] static QString findLocalLrcRecursive(const QString& dir, const QString& artist, const QString& title); [[nodiscard]] static QString findLocalLrcRecursive(const QString& dir, const QString& artist, const QString& title);