From 7b20c4a6c4f2b6e44e3b1a80f592ed4d17eddcc0 Mon Sep 17 00:00:00 2001 From: starfall <1186454801@qq.com> Date: Fri, 21 Aug 2026 11:47:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=AD=8C=E8=AF=8D=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E7=BF=BB=E8=AF=91=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=AD=8C=E8=AF=8D=E6=A8=A1=E5=BC=8F=E6=8A=A2=E9=94=AE=E7=9B=98?= =?UTF-8?q?=E7=84=A6=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clavis.Lyrics 保留 NetEase tlyric 翻译,按时间戳合并进歌词行模型 - provider 优先级调整为 NetEase 优先、LRCLIB 兜底,使有翻译的歌能显示翻译 - Keystone 折叠态 (LyricsContent) 与展开态 (Media) 歌词界面显示原词+翻译双行 - 歌词模式不再独占键盘焦点,可正常向其他窗口输入;左键点击 Keystone 关闭歌词 - 修复 qmlformat 保留字解析问题 (const synchronized) - 同步更新 lyrics 测试适配 NetEase 优先流程 fix: Hub 内歌词视图不再独占键盘焦点 Hub 的 Media tab 打开歌词时是只读显示,不应抢占键盘输入。 HubContent 暴露 mediaLyricsActive,KeystoneSurface 的 hasClosablePopup 排除该状态,使 Hub 歌词打开时输入穿透到其他窗口。 --- Modules/Keystone/Hub/HubContent.qml | 6 + .../LyricsContent/HorizontalLyricsLayout.qml | 55 ++++++--- Modules/Keystone/Media/Media.qml | 69 ++++++++--- .../Styles/Shared/KeystoneSurface.qml | 13 +- core/plugin/lyrics/src/lyrics.cpp | 116 +++++++++++++----- core/plugin/lyrics/src/lyrics.h | 2 + core/tests/lyrics_test.cpp | 70 ++++++++--- 7 files changed, 245 insertions(+), 86 deletions(-) diff --git a/Modules/Keystone/Hub/HubContent.qml b/Modules/Keystone/Hub/HubContent.qml index ae6012a5..091dcbd8 100644 --- a/Modules/Keystone/Hub/HubContent.qml +++ b/Modules/Keystone/Hub/HubContent.qml @@ -15,6 +15,10 @@ Item { property var screen: null property int currentIndex: 0 readonly property var dashboardGlassItems: dashboardContent.holeGlassItems + // Hub media tab shows its own lyrics view; when it is open the surface is + // a read-only display and must not claim exclusive keyboard focus. + readonly property bool mediaLyricsActive: + currentIndex === 1 && mediaContent.showLyrics signal closeRequested() signal avatarEditRequested() @@ -177,6 +181,8 @@ Item { } Media { + id: mediaContent + player: root.player anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter diff --git a/Modules/Keystone/LyricsContent/HorizontalLyricsLayout.qml b/Modules/Keystone/LyricsContent/HorizontalLyricsLayout.qml index a9491a08..889c9cc1 100644 --- a/Modules/Keystone/LyricsContent/HorizontalLyricsLayout.qml +++ b/Modules/Keystone/LyricsContent/HorizontalLyricsLayout.qml @@ -52,34 +52,55 @@ Item { readonly property bool isCurrent: index === root.currentLineIndex width: ListView.view.width - height: 42 + height: modelData.translation ? 52 : 42 onIsCurrentChanged: { if (isCurrent) root.currentTextWidth = Math.max(root.defaultTextWidth, Math.min(lyricText.implicitWidth, 800)); } - Text { - id: lyricText - + Column { anchors.centerIn: parent - text: parent.modelData.text - color: Appearance.m3colors.darkmode ? "white" : "black" - font.family: Fonts.ui - font.pixelSize: 15 - font.weight: Font.Bold - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - - Behavior on color { - ColorAnimation { - duration: Appearance.animation.expressiveFastEffects.duration - easing.type: Appearance.animation.expressiveFastEffects.type - easing.bezierCurve: Appearance.animation.expressiveFastEffects.bezierCurve + anchors.verticalCenterOffset: modelData.translation ? -4 : 0 + width: parent.width + spacing: 1 + + Text { + id: lyricText + + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + text: modelData.text + color: Appearance.m3colors.darkmode ? "white" : "black" + font.family: Fonts.ui + font.pixelSize: 15 + font.weight: Font.Bold + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.expressiveFastEffects.duration + easing.type: Appearance.animation.expressiveFastEffects.type + easing.bezierCurve: Appearance.animation.expressiveFastEffects.bezierCurve + } + } } + Text { + visible: modelData.translation !== undefined && modelData.translation !== "" + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + text: modelData.translation || "" + color: Appearance.applyAlpha(Appearance.m3colors.darkmode ? "white" : "black", 0.6) + font.family: Fonts.ui + font.pixelSize: 12 + font.weight: Font.Normal + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } } } diff --git a/Modules/Keystone/Media/Media.qml b/Modules/Keystone/Media/Media.qml index df163081..f64d7609 100644 --- a/Modules/Keystone/Media/Media.qml +++ b/Modules/Keystone/Media/Media.qml @@ -357,28 +357,59 @@ Item { } } - Text { - id: lyricText + Column { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - text: modelData.text || "" - color: !root.synchronizedLyrics - ? "#ddffffff" - : (lyricDelegate.activeLine - ? "white" - : (lyricDelegate.hovered ? "#ddffffff" : "#99ffffff")) - font.family: Fonts.ui - font.pixelSize: 18 - font.bold: true - horizontalAlignment: Text.AlignLeft - wrapMode: Text.WordWrap - - Behavior on color { - ColorAnimation { - duration: Appearance.animation.expressiveFastEffects.duration - easing.type: Appearance.animation.expressiveFastEffects.type - easing.bezierCurve: Appearance.animation.expressiveFastEffects.bezierCurve + spacing: 2 + + Text { + id: lyricText + anchors.left: parent.left + anchors.right: parent.right + text: modelData.text || "" + color: !root.synchronizedLyrics + ? "#ddffffff" + : (lyricDelegate.activeLine + ? "white" + : (lyricDelegate.hovered ? "#ddffffff" : "#99ffffff")) + font.family: Fonts.ui + font.pixelSize: 18 + font.bold: true + horizontalAlignment: Text.AlignLeft + wrapMode: Text.WordWrap + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.expressiveFastEffects.duration + easing.type: Appearance.animation.expressiveFastEffects.type + easing.bezierCurve: Appearance.animation.expressiveFastEffects.bezierCurve + } + } + } + + Text { + anchors.left: parent.left + anchors.right: parent.right + visible: modelData.translation !== undefined && modelData.translation !== "" + text: modelData.translation || "" + color: !root.synchronizedLyrics + ? "#99ffffff" + : (lyricDelegate.activeLine ? "#ddffffff" : "#77ffffff") + font.family: Fonts.ui + font.pixelSize: 13 + font.bold: false + horizontalAlignment: Text.AlignLeft + wrapMode: Text.WordWrap + elide: Text.ElideRight + maximumLineCount: 2 + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.expressiveFastEffects.duration + easing.type: Appearance.animation.expressiveFastEffects.type + easing.bezierCurve: Appearance.animation.expressiveFastEffects.bezierCurve + } } } } diff --git a/Modules/Keystone/Styles/Shared/KeystoneSurface.qml b/Modules/Keystone/Styles/Shared/KeystoneSurface.qml index 53a3ceca..ae471e5a 100644 --- a/Modules/Keystone/Styles/Shared/KeystoneSurface.qml +++ b/Modules/Keystone/Styles/Shared/KeystoneSurface.qml @@ -444,7 +444,10 @@ Variants { property bool isNotifMode: !contentPresentationActive && NotificationManager.hasNotifs && !expanded && !showVolume && !isHubMode && !isToolsMode && !isLyricsMode property bool isCollapsedMode: !contentPresentationActive && !expanded && !isNotifMode && !isVolumeMode && !isLyricsMode && !isHubMode && !isToolsMode property bool isCollapsedHovered: isCollapsedMode && (keystoneMouseArea.containsMouse || collapsedInputArea.containsMouse) - property bool hasClosablePopup: !contentPresentationActive && (expanded || isLyricsMode || isHubMode || isToolsMode) + readonly property bool hubLyricsActive: isHubMode && hub.mediaLyricsActive + property bool hasClosablePopup: !contentPresentationActive + && !root.hubLyricsActive + && (expanded || isHubMode || isToolsMode) readonly property bool dashboardTabActive: isHubMode && hubTabIndex === 0 readonly property bool showDashboardHole: dashboardTabActive property real pillMorphProgress: 0 @@ -1133,8 +1136,12 @@ Variants { root.expanded = false; } else { - if (root.isLyricsMode || root.isHubMode || root.isToolsMode) - return ; + if (root.isLyricsMode) { + root.showLyrics = false; + return; + } + if (root.isHubMode || root.isToolsMode) + return; root.expanded = !root.expanded; } diff --git a/core/plugin/lyrics/src/lyrics.cpp b/core/plugin/lyrics/src/lyrics.cpp index 6de231ff..236709b3 100644 --- a/core/plugin/lyrics/src/lyrics.cpp +++ b/core/plugin/lyrics/src/lyrics.cpp @@ -637,7 +637,9 @@ void Lyrics::startLoad(quint64 generation, bool bypassCache) m_pendingCandidate.clear(); m_netEaseCandidateIndex = 0; resetProviderOutcomes(); - startLrclibTrack(generation); + // NetEase is preferred because it supplies translations (tlyric) that + // LRCLIB lacks; LRCLIB remains the fallback for coverage. + startNetEaseSearch(generation); } bool Lyrics::loadLocalLyrics(quint64 generation) @@ -710,9 +712,13 @@ bool Lyrics::acceptRawLyrics(const QString &provider, const QString &synced, con return false; m_sourceText = raw; + // When a provider supplies both a synchronized original and a translation + // (e.g. NetEase tlyric), keep the translation source so the timeline can + // be rebuilt (offset changes) with the translation still attached. + m_sourceTranslation = synced.isEmpty() ? QString() : plain; m_sourceProvider = provider; m_sourceCandidate = candidate; - m_lyrics = lines; + m_lyrics = synced.isEmpty() ? lines : mergeTranslation(lines, m_sourceTranslation); bool synchronized = false; for (const QVariant &line : m_lyrics) { @@ -766,7 +772,7 @@ void Lyrics::rebuildTimelineFromSource() if (lines.isEmpty()) return; - m_lyrics = lines; + m_lyrics = m_sourceTranslation.isEmpty() ? lines : mergeTranslation(lines, m_sourceTranslation); bool synchronized = false; for (const QVariant &line : m_lyrics) { if (line.toMap().value(QStringLiteral("time"), -1.0).toDouble() >= 0.0) { @@ -779,6 +785,62 @@ void Lyrics::rebuildTimelineFromSource() emit lyricsChanged(); } +QVariantList Lyrics::mergeTranslation(const QVariantList &lines, const QString &translationLrc) const +{ + if (translationLrc.trimmed().isEmpty()) + return lines; + + // Translations are usually line-aligned with the original. Build a lookup + // from timestamp to translation text, tolerating small clock drift between + // the two LRC payloads. + struct Entry { + double time; + QString text; + }; + QList entries; + const QVariantList translationLines = parseLrc(translationLrc, 0.0); + entries.reserve(translationLines.size()); + for (const QVariant &line : translationLines) { + const QVariantMap map = line.toMap(); + const double time = map.value(QStringLiteral("time"), -1.0).toDouble(); + const QString text = map.value(QStringLiteral("text")).toString(); + if (time < 0.0 || text.trimmed().isEmpty()) + continue; + entries.append({time, text}); + } + if (entries.isEmpty()) + return lines; + std::sort(entries.begin(), entries.end(), + [](const Entry &left, const Entry &right) { return left.time < right.time; }); + + const double toleranceMs = 120.0; + QVariantList merged; + merged.reserve(lines.size()); + for (const QVariant &line : lines) { + const QVariantMap map = line.toMap(); + const double time = map.value(QStringLiteral("time"), -1.0).toDouble(); + QString translation; + if (time >= 0.0 && !entries.isEmpty()) { + // Linear probe is fine here: translations are typically a few + // hundred lines at most and the lists are usually aligned. + for (const Entry &entry : entries) { + if (entry.time >= time - toleranceMs / 1000.0 && entry.time <= time + toleranceMs / 1000.0) { + translation = entry.text; + break; + } + } + } + if (translation.isEmpty()) { + merged.append(line); + } else { + QVariantMap enriched = map; + enriched.insert(QStringLiteral("translation"), translation); + merged.append(enriched); + } + } + return merged; +} + void Lyrics::startLrclibTrack(quint64 generation) { startReply(lrclibUrl(false), ReplyKind::LrclibTrack, generation); @@ -873,9 +935,7 @@ void Lyrics::handleReply(QNetworkReply *reply, ReplyKind kind, quint64 generatio switch (kind) { case ReplyKind::LrclibTrack: m_lrclibOutcome = notFound ? ProviderOutcome::NotFound : ProviderOutcome::TransportError; - if (m_autoFallback) - startNetEaseSearch(generation); - else if (notFound) + if (notFound) finishEmpty(); else finishError(QStringLiteral("LRCLIB 请求失败")); @@ -887,8 +947,7 @@ void Lyrics::handleReply(QNetworkReply *reply, ReplyKind kind, quint64 generatio case ReplyKind::NetEaseSearch: m_netEaseSearchOutcome = notFound ? ProviderOutcome::NotFound : ProviderOutcome::TransportError; if (m_autoFallback) { - m_netEaseCandidateIndex = 0; - tryNextNetEaseCandidate(generation); + startLrclibTrack(generation); } else if (notFound) { finishEmpty(); } else { @@ -901,7 +960,7 @@ void Lyrics::handleReply(QNetworkReply *reply, ReplyKind kind, quint64 generatio if (notFound) m_netEaseSawNoLyrics = true; if (m_autoFallback) - tryNextNetEaseCandidate(generation); + startLrclibTrack(generation); else if (notFound) finishEmpty(); else @@ -917,10 +976,7 @@ void Lyrics::handleReply(QNetworkReply *reply, ReplyKind kind, quint64 generatio switch (kind) { case ReplyKind::LrclibTrack: m_lrclibOutcome = ProviderOutcome::InvalidResponse; - if (m_autoFallback) - startNetEaseSearch(generation); - else - finishError(QStringLiteral("LRCLIB 返回了无效响应")); + finishError(QStringLiteral("LRCLIB 返回了无效响应")); return; case ReplyKind::LrclibSearch: m_lrclibOutcome = ProviderOutcome::InvalidResponse; @@ -929,8 +985,7 @@ void Lyrics::handleReply(QNetworkReply *reply, ReplyKind kind, quint64 generatio case ReplyKind::NetEaseSearch: m_netEaseSearchOutcome = ProviderOutcome::InvalidResponse; if (m_autoFallback) { - m_netEaseCandidateIndex = 0; - tryNextNetEaseCandidate(generation); + startLrclibTrack(generation); } else { finishError(QStringLiteral("NetEase 搜索返回了无效响应")); } @@ -938,7 +993,7 @@ void Lyrics::handleReply(QNetworkReply *reply, ReplyKind kind, quint64 generatio case ReplyKind::NetEaseLyrics: m_netEaseCandidateOutcome = ProviderOutcome::InvalidResponse; if (m_autoFallback) - tryNextNetEaseCandidate(generation); + startLrclibTrack(generation); else finishError(QStringLiteral("NetEase 歌词返回了无效响应")); return; @@ -958,9 +1013,7 @@ void Lyrics::handleReply(QNetworkReply *reply, ReplyKind kind, quint64 generatio if (handleLrclibTrack(document.object(), generation)) { return; } - if (m_autoFallback) { - startNetEaseSearch(generation); - } else if (m_lrclibOutcome == ProviderOutcome::NotFound) { + if (m_lrclibOutcome == ProviderOutcome::NotFound) { finishEmpty(); } else { finishError(QStringLiteral("LRCLIB 歌词内容不可用")); @@ -1090,7 +1143,9 @@ bool Lyrics::handleNetEaseLyrics(const QJsonObject &json, quint64 generation) const QJsonObject lrc = json.value(QStringLiteral("lrc")).toObject(); const QJsonObject translated = json.value(QStringLiteral("tlyric")).toObject(); const QString synced = firstString(lrc, {QStringLiteral("lyric")}); - const QString plain = synced.isEmpty() ? firstString(translated, {QStringLiteral("lyric")}) : QString(); + // Keep the translation even when a synchronized original exists: the UI + // merges it below each line instead of dropping it. + const QString plain = firstString(translated, {QStringLiteral("lyric")}); if (synced.isEmpty() && plain.isEmpty()) { m_netEaseCandidateOutcome = ProviderOutcome::NoLyrics; m_netEaseSawNoLyrics = true; @@ -1120,15 +1175,18 @@ void Lyrics::tryNextNetEaseCandidate(quint64 generation) if (m_netEaseSearchOutcome == ProviderOutcome::TransportError || m_netEaseSearchOutcome == ProviderOutcome::InvalidResponse) { - finishError(QStringLiteral("NetEase 搜索不可用")); - } else if (m_lrclibOutcome == ProviderOutcome::TransportError || - m_lrclibOutcome == ProviderOutcome::InvalidResponse || - m_lrclibOutcome == ProviderOutcome::ParseFailure) { - finishError(QStringLiteral("歌词服务返回了无效内容")); - } else if (!m_netEaseCandidates.isEmpty() && !m_netEaseSawNoLyrics && !m_netEaseSawValidLyricResponse && - (m_netEaseCandidateOutcome == ProviderOutcome::TransportError || - m_netEaseCandidateOutcome == ProviderOutcome::InvalidResponse || m_netEaseSawParseFailure)) { - finishError(QStringLiteral("NetEase 歌词请求失败")); + if (m_autoFallback) { + startLrclibTrack(generation); + } else { + finishError(QStringLiteral("NetEase 搜索不可用")); + } + } else if (m_autoFallback) { + // NetEase found no usable lyrics for this track; fall back to LRCLIB + // for coverage (it never carries translations, but is a reliable + // source for the original lines). + startLrclibTrack(generation); + } else if (m_netEaseSawParseFailure) { + finishError(QStringLiteral("NetEase 歌词内容不可用")); } else { // A valid search with no lyrics is a normal not-found result, even if // LRCLIB previously returned HTTP 404 or a candidate was rejected. diff --git a/core/plugin/lyrics/src/lyrics.h b/core/plugin/lyrics/src/lyrics.h index 9c8c8e1b..8e7cb987 100644 --- a/core/plugin/lyrics/src/lyrics.h +++ b/core/plugin/lyrics/src/lyrics.h @@ -139,6 +139,7 @@ class Lyrics : public QObject { // The raw provider payload is retained so changing offset can rebuild the // timeline without asking a provider again. QString m_sourceText; + QString m_sourceTranslation; QString m_sourceProvider; QVariantMap m_sourceCandidate; @@ -164,6 +165,7 @@ class Lyrics : public QObject { bool loadCachedLyrics(quint64 generation); bool acceptRawLyrics(const QString &provider, const QString &synced, const QString &plain, const QVariantMap &candidate, quint64 generation, bool writeCache); + QVariantList mergeTranslation(const QVariantList &lines, const QString &translationLrc) const; void finishEmpty(const QString &message = {}); void finishError(const QString &message); void rebuildTimelineFromSource(); diff --git a/core/tests/lyrics_test.cpp b/core/tests/lyrics_test.cpp index 630fa1e8..37fa2390 100644 --- a/core/tests/lyrics_test.cpp +++ b/core/tests/lyrics_test.cpp @@ -202,6 +202,22 @@ QByteArray lrclibResponse(const QString &id, const QString &lyrics) return QJsonDocument(object).toJson(QJsonDocument::Compact); } +QByteArray netEaseSearchResponse(const QString &id, const QString &title = QStringLiteral("title"), + double durationMs = 5000.0) +{ + QJsonObject song{ + {QStringLiteral("id"), id.toInt()}, + {QStringLiteral("name"), title}, + {QStringLiteral("artists"), + QJsonArray{QJsonObject{{QStringLiteral("name"), QStringLiteral("artist")}}}}, + {QStringLiteral("album"), QJsonObject{{QStringLiteral("name"), QStringLiteral("album")}}}, + {QStringLiteral("dt"), durationMs}, + }; + return QJsonDocument(QJsonObject{{QStringLiteral("result"), + QJsonObject{{QStringLiteral("songs"), QJsonArray{song}}}}}) + .toJson(QJsonDocument::Compact); +} + } // namespace void LyricsTest::parsesNormalAndMillisecondTimestamps() @@ -251,6 +267,13 @@ void LyricsTest::ignoresMalformedLrcMetadata() void LyricsTest::mapsTimelineAndOffset() { + QTemporaryDir temporary; + QVERIFY(temporary.isValid()); + ScopedEnvironment cacheHome("XDG_CACHE_HOME"); + ScopedEnvironment dataHome("XDG_DATA_HOME"); + ScopedEnvironment localDirectory("CLAVIS_LYRICS_DIR"); + configureTemporaryPaths(temporary.path(), cacheHome, dataHome, localDirectory); + Lyrics lyrics; lyrics.setOffsetMs(250.0); FixtureNetworkAccessManager manager; @@ -258,7 +281,13 @@ void LyricsTest::mapsTimelineAndOffset() lyrics.setNetworkAccessManager(&manager); lyrics.setTrack(QStringLiteral("artist"), QStringLiteral("title")); waitForRequests(manager, 1); - manager.replyAt(0)->complete(QByteArrayLiteral(R"({"syncedLyrics":"[00:01.00]one\n[00:03.00]two"})")); + manager.replyAt(0)->complete(QByteArrayLiteral( + R"JSON({"result":{"songs":[ + {"id":7,"name":"title","artists":[{"name":"artist"}],"album":{"name":"album"},"dt":5000} + ]}})JSON")); + waitForRequests(manager, 2); + manager.replyAt(1)->complete( + QByteArrayLiteral(R"JSON({"lrc":{"lyric":"[00:01.00]one\n[00:03.00]two"}})JSON")); QTRY_VERIFY_WITH_TIMEOUT(lyrics.hasLyrics(), 1000); QCOMPARE(lyrics.indexForTime(1.24), -1); QCOMPARE(lyrics.indexForTime(1.25), 0); @@ -356,11 +385,8 @@ void LyricsTest::netEaseRequestUsesCompatibleContract() lyrics.setNetworkAccessManager(&manager); lyrics.setTrack(QStringLiteral("artist"), QStringLiteral("Title")); waitForRequests(manager, 1); - manager.replyAt(0)->complete({}, 404); - - waitForRequests(manager, 2); QVERIFY(manager.cookieJar() != oldCookieJar); - const QNetworkRequest searchRequest = manager.requestAt(1); + const QNetworkRequest searchRequest = manager.requestAt(0); QCOMPARE(searchRequest.url().path(), QStringLiteral("/api/search/get")); QUrlQuery searchQuery(searchRequest.url()); QCOMPARE(searchQuery.queryItemValue(QStringLiteral("s")), QStringLiteral("Title artist")); @@ -370,11 +396,11 @@ void LyricsTest::netEaseRequestUsesCompatibleContract() QVERIFY(searchRequest.rawHeader("User-Agent").startsWith("Mozilla/")); QCOMPARE(searchRequest.rawHeader("Referer"), QByteArrayLiteral("https://music.163.com/")); - manager.replyAt(1)->complete(QByteArrayLiteral(R"JSON({"result":{"songs":[ + manager.replyAt(0)->complete(QByteArrayLiteral(R"JSON({"result":{"songs":[ {"id":7,"name":"Title","artists":[{"name":"artist"}],"album":{"name":"album"},"dt":120000} ]}})JSON")); - waitForRequests(manager, 3); - const QNetworkRequest lyricRequest = manager.requestAt(2); + waitForRequests(manager, 2); + const QNetworkRequest lyricRequest = manager.requestAt(1); QCOMPARE(lyricRequest.url().path(), QStringLiteral("/api/song/lyric")); QUrlQuery lyricQuery(lyricRequest.url()); QCOMPARE(lyricQuery.queryItemValue(QStringLiteral("id")), QStringLiteral("7")); @@ -384,7 +410,7 @@ void LyricsTest::netEaseRequestUsesCompatibleContract() QVERIFY(lyricRequest.rawHeader("User-Agent").startsWith("Mozilla/")); QCOMPARE(lyricRequest.rawHeader("Referer"), QByteArrayLiteral("https://music.163.com/")); - manager.replyAt(2)->complete(QByteArrayLiteral(R"({"lrc":{"lyric":"[00:01.00]found"}})")); + manager.replyAt(1)->complete(QByteArrayLiteral(R"({"lrc":{"lyric":"[00:01.00]found"}})")); QTRY_VERIFY_WITH_TIMEOUT(lyrics.hasLyrics(), 1000); QCOMPARE(lyrics.provider(), QStringLiteral("NetEase")); } @@ -607,17 +633,21 @@ void LyricsTest::deduplicatesSameTrackButRefreshes() lyrics.setTrack(QStringLiteral("artist"), QStringLiteral("title")); QTest::qWait(120); QCOMPARE(manager.requests, 1); - manager.replyAt(0)->complete(QByteArrayLiteral(R"({"syncedLyrics":"[00:01.00]first"})")); + manager.replyAt(0)->complete(netEaseSearchResponse(QStringLiteral("42"))); + waitForRequests(manager, 2); + manager.replyAt(1)->complete(QByteArrayLiteral(R"({"lrc":{"lyric":"[00:01.00]first"}})")); QTRY_VERIFY_WITH_TIMEOUT(lyrics.hasLyrics(), 1000); lyrics.refresh(); - waitForRequests(manager, 2); + waitForRequests(manager, 3); QVERIFY(lyrics.loading()); - manager.replyAt(1)->complete(QByteArrayLiteral(R"({"syncedLyrics":"[00:02.00]refreshed"})")); + manager.replyAt(2)->complete(netEaseSearchResponse(QStringLiteral("43"))); + waitForRequests(manager, 4); + manager.replyAt(3)->complete(QByteArrayLiteral(R"({"lrc":{"lyric":"[00:02.00]refreshed"}})")); QTRY_VERIFY_WITH_TIMEOUT(lyrics.hasLyrics(), 1000); QCOMPARE(lyrics.lyrics().first().toMap().value(QStringLiteral("text")).toString(), QStringLiteral("refreshed")); - QCOMPARE(manager.requests, 2); + QCOMPARE(manager.requests, 4); } void LyricsTest::staleReplyCannotReplaceTrack() @@ -637,10 +667,12 @@ void LyricsTest::staleReplyCannotReplaceTrack() waitForRequests(manager, 1); FixtureReply *firstReply = manager.replyAt(0); lyrics.setTrack(QStringLiteral("artist"), QStringLiteral("second")); - firstReply->complete(QByteArrayLiteral(R"({"syncedLyrics":"[00:01.00]stale first"})")); + firstReply->complete(netEaseSearchResponse(QStringLiteral("1"), QStringLiteral("second"))); waitForRequests(manager, 2); QVERIFY(manager.firstRequestAborted); - manager.replyAt(1)->complete(QByteArrayLiteral(R"({"syncedLyrics":"[00:02.00]second"})")); + manager.replyAt(1)->complete(netEaseSearchResponse(QStringLiteral("2"), QStringLiteral("second"))); + waitForRequests(manager, 3); + manager.replyAt(2)->complete(QByteArrayLiteral(R"({"lrc":{"lyric":"[00:02.00]second"}})")); QTRY_VERIFY_WITH_TIMEOUT(lyrics.hasLyrics(), 1000); QCOMPARE(lyrics.trackTitle(), QStringLiteral("second")); QCOMPARE(lyrics.lyrics().first().toMap().value(QStringLiteral("text")).toString(), @@ -664,13 +696,15 @@ void LyricsTest::rapidSwitchingAndClearTrackInvalidateReplies() waitForRequests(manager, 1); FixtureReply *firstReply = manager.replyAt(0); lyrics.setTrack(QStringLiteral("artist"), QStringLiteral("B")); - firstReply->complete(QByteArrayLiteral(R"({"syncedLyrics":"[00:01.00]stale A"})")); + firstReply->complete(netEaseSearchResponse(QStringLiteral("1"))); waitForRequests(manager, 2); FixtureReply *secondReply = manager.replyAt(1); lyrics.setTrack(QStringLiteral("artist"), QStringLiteral("C")); - secondReply->complete(QByteArrayLiteral(R"({"syncedLyrics":"[00:02.00]stale B"})")); + secondReply->complete(netEaseSearchResponse(QStringLiteral("2"), QStringLiteral("C"))); waitForRequests(manager, 3); - manager.replyAt(2)->complete(QByteArrayLiteral(R"({"syncedLyrics":"[00:03.00]C"})")); + manager.replyAt(2)->complete(netEaseSearchResponse(QStringLiteral("3"), QStringLiteral("C"))); + waitForRequests(manager, 4); + manager.replyAt(3)->complete(QByteArrayLiteral(R"({"lrc":{"lyric":"[00:03.00]C"}})")); QTRY_VERIFY_WITH_TIMEOUT(lyrics.hasLyrics(), 1000); QCOMPARE(lyrics.trackTitle(), QStringLiteral("C")); QCOMPARE(lyrics.lyrics().first().toMap().value(QStringLiteral("text")).toString(), QStringLiteral("C"));