diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml deleted file mode 100644 index 98e8c5c1..00000000 --- a/modules/dashboard/LyricMenu.qml +++ /dev/null @@ -1,411 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Caelestia.Config -import qs.components -import qs.components.controls -import qs.services - -StyledRect { - id: root - - required property real contentHeight - - function searchCandidates(title, artist) { - LyricsService.currentRequestId++; - LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); - } - - implicitHeight: contentHeight - - radius: Tokens.rounding.extraLarge - color: Colours.tPalette.m3surfaceContainer - - Loader { - asynchronous: true - anchors.fill: parent - active: root.height > 0 - - sourceComponent: ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.medium - - // Header: icon, backend selector, refresh, toggle - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.extraSmall - - MaterialIcon { - text: "lyrics" - fill: 1 - color: Colours.palette.m3primary - fontStyle: Tokens.font.icon.size(Tokens.spacing.large).build() - } - - Rectangle { - Layout.preferredHeight: 24 - Layout.preferredWidth: 80 - radius: Tokens.rounding.medium - color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15) - - StyledText { - anchors.centerIn: parent - text: LyricsService.preferredBackend - font: Tokens.font.body.small - color: Colours.palette.m3primary - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - const backends = ["Auto", "Local", "NetEase"]; - const currentIndex = backends.indexOf(LyricsService.preferredBackend); - const nextIndex = (currentIndex + 1) % backends.length; - LyricsService.preferredBackend = backends[nextIndex]; - LyricsService.loadLyrics(); - } - } - } - - Rectangle { - Layout.preferredHeight: 24 - Layout.preferredWidth: 60 - radius: Tokens.rounding.medium - visible: LyricsService.preferredBackend === "Auto" - color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15) - - StyledText { - anchors.centerIn: parent - text: LyricsService.backend - font: Tokens.font.body.small - color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary - } - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: "refresh" - type: IconButton.Text - onClicked: LyricsService.loadLyrics() - } - - StyledSwitch { - checked: LyricsService.lyricsVisible - onToggled: LyricsService.toggleVisibility() - } - } - - StyledText { - Layout.fillWidth: true - text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:" - color: Colours.palette.m3outline - font: Tokens.font.body.small - elide: Text.ElideRight - visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0 - } - - // Local file info (shown in Local mode) - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 48 - visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0 - radius: Tokens.rounding.medium - color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1) - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.extraSmall - spacing: 0 - - StyledText { - Layout.fillWidth: true - text: { - const path = LyricsService.loadedLocalFile; - const parts = path.split('/'); - return parts[parts.length - 1]; - } - font: Tokens.font.body.small - color: Colours.palette.m3tertiary - elide: Text.ElideMiddle - } - - StyledText { - Layout.fillWidth: true - text: { - const path = LyricsService.loadedLocalFile; - const parts = path.split('/'); - if (parts.length > 2) { - return parts.slice(-3, -1).join('/'); - } - return ""; - } - font: Tokens.font.body.small - color: Colours.palette.m3outline - elide: Text.ElideMiddle - } - } - } - - // Candidates list - Loader { - Layout.fillWidth: true - Layout.fillHeight: true - - active: LyricsService.preferredBackend !== "Local" - - sourceComponent: ListView { - id: candidatesView - - model: LyricsService.candidatesModel - clip: true - spacing: Tokens.spacing.small - visible: LyricsService.candidatesModel.count > 0 - opacity: visible ? 1 : 0 - - delegate: Item { - id: delegateRoot - - required property real id - required property string title - required property string artist - - property bool hovered: false - property bool pressed: false - - width: ListView.view.width * 0.98 - height: 70 - - anchors.horizontalCenter: parent?.horizontalCenter - scale: hovered ? 1.02 : 1.0 - - Behavior on scale { - NumberAnimation { - duration: Tokens.anim.durations.small - easing.type: Easing.OutCubic - } - } - - Rectangle { - id: background - - anchors.fill: parent - radius: Tokens.rounding.medium - - color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) - - border.width: delegateRoot.hovered ? 1 : 0 - border.color: Colours.palette.m3primary - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - Behavior on border.width { - NumberAnimation { - duration: Tokens.anim.durations.small - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onEntered: delegateRoot.hovered = true - onExited: delegateRoot.hovered = false - onPressed: delegateRoot.pressed = true - onReleased: delegateRoot.pressed = false - onClicked: LyricsService.selectCandidate(delegateRoot.id) - } - - Row { - anchors.fill: parent - anchors.margins: Tokens.padding.medium - spacing: Tokens.spacing.small - - // Active indicator bar - Rectangle { - width: 4 - height: parent.height * 0.6 - radius: 2 - anchors.verticalCenter: parent.verticalCenter - color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - } - - Column { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 30 - spacing: 4 - - Text { - text: delegateRoot.title - font: Tokens.font.body.builders.medium.weight(Font.Bold).build() - color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface - width: parent.width - elide: Text.ElideRight - - Behavior on color { - ColorAnimation { - duration: Tokens.anim.durations.small - } - } - } - - Text { - text: delegateRoot.artist - font: Tokens.font.body.small - color: Colours.palette.m3onSurfaceVariant - elide: Text.ElideRight - } - } - } - } - } - } - - Item { - Layout.fillHeight: true - visible: LyricsService.candidatesModel.count == 0 && LyricsService.preferredBackend !== "Local" - } - - // Manual search - ColumnLayout { - Layout.fillWidth: true - spacing: Tokens.padding.extraSmall - - StyledText { - Layout.fillWidth: true - text: "Manual Search" - font: Tokens.font.body.small - color: Colours.palette.m3onSurfaceVariant - elide: Text.ElideRight - } - - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.extraSmall - - StyledInputField { - id: searchTitle - - Layout.fillWidth: true - horizontalAlignment: TextInput.AlignLeft - - Binding { - target: searchTitle - property: "text" - value: (Players.active?.trackTitle ?? qsTr("title")) || qsTr("title") - } - } - - StyledInputField { - id: searchArtist - - Layout.fillWidth: true - horizontalAlignment: TextInput.AlignLeft - - Binding { - target: searchArtist - property: "text" - value: (Players.active?.trackArtist ?? qsTr("artist")) || qsTr("artist") - } - } - - IconButton { - icon: "search" - onClicked: root.searchCandidates(searchTitle.text, searchArtist.text) - } - } - } - - // Offset controls - RowLayout { - Layout.fillWidth: true - spacing: Tokens.padding.extraSmall - - MaterialIcon { - text: "contrast_square" - fontStyle: Tokens.font.icon.large - color: Colours.palette.m3secondary - } - - StyledText { - text: "Offset" - color: Colours.palette.m3outline - font: Tokens.font.body.medium - } - - Item { - Layout.fillWidth: true - } - - IconButton { - icon: "remove" - type: IconButton.Text - onClicked: { - LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1)); - LyricsService.savePrefs(); - } - } - - TextInput { - id: offsetInput - - horizontalAlignment: TextInput.AlignHCenter - color: Colours.palette.m3secondary - font: Tokens.font.body.medium - selectByMouse: true - text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" - onEditingFinished: { - let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); - let val = parseFloat(cleaned); - if (!isNaN(val)) { - LyricsService.offset = parseFloat(val.toFixed(1)); - LyricsService.savePrefs(); - } else { - offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; - } - } - - Binding { - target: offsetInput - property: "text" - value: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" - when: !offsetInput.activeFocus - } - - Connections { - function onCurrentRequestIdChanged() { - offsetInput.focus = false; - } - - target: LyricsService - } - } - - IconButton { - icon: "add" - type: IconButton.Text - onClicked: { - LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1)); - LyricsService.savePrefs(); - } - } - } - } - } -} diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml deleted file mode 100644 index a1c29bda..00000000 --- a/modules/dashboard/LyricsView.qml +++ /dev/null @@ -1,111 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Quickshell -import Caelestia.Config -import qs.components -import qs.components.containers -import qs.services - -StyledListView { - id: root - - readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0 - - clip: true - model: LyricsService.model - currentIndex: LyricsService.currentIndex - visible: lyricsActuallyVisible || hideTimer.running - preferredHighlightBegin: height / 2 - 30 - preferredHighlightEnd: height / 2 + 30 - highlightRangeMode: ListView.ApplyRange - highlightFollowsCurrentItem: true - highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Tokens.anim.durations.normal - layer.enabled: true - layer.effect: ShaderEffect { - required property Item source - property real fadeMargin: 0.5 - - fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") - } - onLyricsActuallyVisibleChanged: { - if (!lyricsActuallyVisible) - hideTimer.restart(); - } - onModelChanged: { - if (model && model.count > 0) { - Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); - } - } - delegate: Item { - id: delegateRoot - - required property string lyricLine - required property real time - required property int index - readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 - property bool isCurrent: ListView.isCurrentItem - - width: ListView.view.width - height: hasContent ? (lyricText.contentHeight + Tokens.spacing.largeIncreased) : 0 - - MultiEffect { - id: effect - - anchors.fill: lyricText - source: lyricText - scale: lyricText.scale - enabled: delegateRoot.isCurrent - visible: delegateRoot.isCurrent - - blurEnabled: true - blur: 0.4 - - shadowEnabled: true - shadowColor: Colours.palette.m3primary - shadowOpacity: 0.5 - shadowBlur: 0.6 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 0 - - autoPaddingEnabled: true - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time) - } - - Text { - id: lyricText - - text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" - width: parent.width * 0.85 - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - font: delegateRoot.isCurrent ? Tokens.font.body.builders.medium.weight(Font.Bold).build() : Tokens.font.body.medium - color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - scale: delegateRoot.isCurrent ? 1.15 : 1.0 - - Behavior on color { - CAnim { - duration: Tokens.anim.durations.small - } - } - Behavior on scale { - Anim { - type: Anim.StandardSmall - } - } - } - } - - Timer { - id: hideTimer - - interval: 300 // long enough to bridge the track switch gap - running: false - repeat: false - } -} diff --git a/modules/dashboard/media/Details.qml b/modules/dashboard/media/Details.qml index e206cf37..32d3c560 100644 --- a/modules/dashboard/media/Details.qml +++ b/modules/dashboard/media/Details.qml @@ -30,13 +30,7 @@ ColumnLayout { interval: GlobalConfig.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true - onTriggered: { - if (!Players.active) - return; - - LyricsService.updatePosition(); - Players.active.positionChanged(); - } + onTriggered: Players.active?.positionChanged() } StyledText { diff --git a/services/LyricsService.qml b/services/LyricsService.qml deleted file mode 100644 index 26a75e9b..00000000 --- a/services/LyricsService.qml +++ /dev/null @@ -1,403 +0,0 @@ -pragma Singleton - -import "../utils/scripts/lrcparser.js" as Lrc -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia -import Caelestia.Config -import qs.utils - -Singleton { - id: root - - property var player: Players.active - property int currentIndex: -1 - property bool loading: false - property bool isManualSeeking: false - property bool lyricsVisible: GlobalConfig.services.showLyrics - property string backend: "Local" - property string preferredBackend: GlobalConfig.services.lyricsBackend - property real currentSongId: 0 - property string loadedLocalFile: "" - property real offset - property int currentRequestId: 0 - property var lyricsMap: ({}) - - readonly property string lyricsDir: Paths.absolutePath(GlobalConfig.paths.lyricsDir) - readonly property string lyricsMapFile: lyricsDir + "/lyrics_map.json" - readonly property alias model: lyricsModel - readonly property alias candidatesModel: fetchedCandidatesModel - readonly property var _netEaseHeaders: ({ - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", - "Referer": "https://music.163.com/" - }) - - function getMetadata() { - if (!player || !player.metadata) - return null; - let artist = player.metadata["xesam:artist"]; - const title = player.metadata["xesam:title"]; - if (Array.isArray(artist)) - artist = artist.join(", "); - return { - artist: artist || "Unknown", - title: title || "Unknown" - }; - } - - function _metaKey(meta) { - return `${meta.artist} - ${meta.title}`; - } - - function savePrefs() { - let meta = getMetadata(); - if (!meta) - return; - let key = _metaKey(meta); - let existing = root.lyricsMap[key] ?? {}; - root.lyricsMap[key] = { - offset: root.offset, - backend: root.backend, - neteaseId: existing.neteaseId ?? null - }; - // reassign to notify QML bindings of the map change - root.lyricsMap = root.lyricsMap; - saveLyricsMap.command = ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, "'\\''")}' > "${root.lyricsMapFile}"`]; - saveLyricsMap.running = true; - } - - function toggleVisibility() { - GlobalConfig.services.showLyrics = !GlobalConfig.services.showLyrics; - } - - function loadLyrics() { - loadDebounce.restart(); - } - - function _doLoadLyrics() { - const meta = getMetadata(); - if (!meta) { - lyricsModel.clear(); - root.currentIndex = -1; - return; - } - - loading = true; - lyricsModel.clear(); - currentIndex = -1; - root.currentSongId = 0; - - root.currentRequestId++; - let requestId = root.currentRequestId; - - let key = _metaKey(meta); - let saved = root.lyricsMap[key]; - root.offset = saved?.offset ?? 0.0; - - if (root.preferredBackend === "NetEase") { - root.backend = "NetEase"; - fetchNetEase(meta.title, meta.artist, requestId); - return; - } - - if (root.preferredBackend === "Local") { - root.backend = "Local"; - let cleanDir = lyricsDir.replace(/\/$/, ""); - let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; - - // Search for files matching "Artist - Title.lrc" pattern - const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); - const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); - const escapedTitle = titleStr.replace(/'/g, "'\\''"); - const escapedArtist = artistStr.replace(/'/g, "'\\''"); - findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; - findLyricsInSubdirs.requestId = requestId; - findLyricsInSubdirs.running = true; - - lrcFile.path = ""; - lrcFile.path = flatPath; - return; - } - - // Auto mode: try local first - root.backend = "Local"; - let cleanDir = lyricsDir.replace(/\/$/, ""); - let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; - - const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); - const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); - const escapedTitle = titleStr.replace(/'/g, "'\\''"); - const escapedArtist = artistStr.replace(/'/g, "'\\''"); - findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; - findLyricsInSubdirs.requestId = requestId; - findLyricsInSubdirs.running = true; - - lrcFile.path = ""; - lrcFile.path = flatPath; - fetchNetEaseCandidates(meta.title, meta.artist, requestId); - } - - function updateModel(parsedArray) { - root.currentIndex = -1; - lyricsModel.clear(); - for (let line of parsedArray) { - lyricsModel.append({ - time: line.time, - lyricLine: line.text - }); - } - } - - function fallbackToOnline() { - let meta = getMetadata(); - if (!meta) - return; - fetchNetEase(meta.title, meta.artist, root.currentRequestId); - } - - // NetEase - - // searches NetEase and populates the candidates model. returns the result array via the onResults callback - function _searchNetEase(title, artist, reqId, onResults) { - Requests.resetCookies(); - const query = encodeURIComponent(`${title} ${artist}`); - const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`; - - Requests.get(url, text => { - if (reqId !== root.currentRequestId) - return; - const res = JSON.parse(text); - const songs = res.result?.songs || []; - - fetchedCandidatesModel.clear(); - for (let s of songs) { - fetchedCandidatesModel.append({ - id: s.id, - title: s.name || "Unknown Title", - artist: s.artists?.map(a => a.name).join(", ") || "Unknown Artist" - }); - } - - onResults(songs); - }, err => {}, root._netEaseHeaders); - } - - // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list. - function fetchNetEaseCandidates(title, artist, reqId) { - _searchNetEase(title, artist, reqId, _songs => {}); - } - - // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics. - function fetchNetEase(title, artist, reqId) { - _searchNetEase(title, artist, reqId, songs => { - const bestMatch = songs.find(s => { - const inputArtist = String(artist || "").toLowerCase(); - const sArtist = String(s.artists?.[0]?.name || "").toLowerCase(); - return inputArtist.includes(sArtist) || sArtist.includes(inputArtist); - }); - - if (!bestMatch) { - return; // No reliable lyrics found - } - - let key = `${artist} - ${title}`; - root.lyricsMap[key] = { - offset: root.lyricsMap[key]?.offset ?? 0.0, - backend: "NetEase", - neteaseId: bestMatch.id - }; - root.currentSongId = bestMatch.id; - savePrefs(); - fetchNetEaseLyrics(bestMatch.id, reqId); - }); - } - - function fetchNetEaseLyrics(id, reqId) { - const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`; - Requests.get(url, text => { - if (reqId !== root.currentRequestId) - return; - const res = JSON.parse(text); - if (res.lrc?.lyric) { - updateModel(Lrc.parseLrc(res.lrc.lyric)); - loading = false; - } - }); - } - - function selectCandidate(songId) { - let meta = getMetadata(); - if (!meta) - return; - root.backend = "NetEase"; - root.currentSongId = songId; - let key = _metaKey(meta); - root.lyricsMap[key] = { - offset: root.lyricsMap[key]?.offset ?? 0.0, - neteaseId: songId - }; - savePrefs(); - fetchNetEaseLyrics(songId, currentRequestId); - } - - function updatePosition() { - if (isManualSeeking || loading || !player || lyricsModel.count === 0) - return; - - let pos = player.position - root.offset; - let newIdx = -1; - for (let i = lyricsModel.count - 1; i >= 0; i--) { - if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor - newIdx = i; - break; - } - } - - if (newIdx !== currentIndex) { - root.currentIndex = newIdx; - } - } - - function jumpTo(index, time) { - root.isManualSeeking = true; - root.currentIndex = index; - - if (player) { - player.position = time + root.offset + 0.01; // compensate for rounding - } - - seekTimer.restart(); - } - - onPreferredBackendChanged: { - if (GlobalConfig.services.lyricsBackend !== preferredBackend) { - GlobalConfig.services.lyricsBackend = preferredBackend; - } - } - - ListModel { - id: lyricsModel - } - - ListModel { - id: fetchedCandidatesModel - } - - Timer { - id: seekTimer - - interval: 500 - onTriggered: root.isManualSeeking = false - } - - // If no local lyrics were loaded within the interval, fall back to NetEase - Timer { - id: fallbackTimer - - interval: 200 - onTriggered: { - if (lyricsModel.count === 0) { - root.backend = "NetEase"; - fallbackToOnline(); - } - } - } - - Timer { - id: loadDebounce - - interval: 50 - onTriggered: root._doLoadLyrics() - } - - FileView { - id: lyricsMapFileView - - path: root.lyricsMapFile - printErrors: false - onLoaded: { - try { - root.lyricsMap = JSON.parse(text()); - } catch (e) { - root.lyricsMap = {}; - } - } - } - - FileView { - id: lrcFile - - printErrors: false - onLoaded: { - fallbackTimer.stop(); - let parsed = Lrc.parseLrc(text()); - if (parsed.length > 0) { - root.backend = "Local"; - root.loadedLocalFile = path; - updateModel(parsed); - loading = false; - } else if (root.preferredBackend === "Local") { - // Local mode only - fail immediately - root.backend = "NetEase"; - fallbackToOnline(); - } - // In Auto mode, let the Process onExited handle fallback - } - } - - Connections { - function onActiveChanged() { - root.player = Players.active; - loadLyrics(); - } - - target: Players - } - - Connections { - function onMetadataChanged() { - loadLyrics(); - } - - target: root.player - ignoreUnknownSignals: true - } - - Process { - id: saveLyricsMap - - command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] - } - - Process { - id: findLyricsInSubdirs - - property int requestId: -1 - property bool foundFile: false - - stdout: SplitParser { - onRead: data => { - if (findLyricsInSubdirs.requestId === root.currentRequestId) { - const foundPath = data.trim(); - if (foundPath && foundPath.length > 0) { - findLyricsInSubdirs.foundFile = true; - fallbackTimer.stop(); - root.loadedLocalFile = foundPath; - lrcFile.path = ""; - lrcFile.path = foundPath; - } - } - } - } - - onExited: (exitCode, exitStatus) => { // qmllint disable signal-handler-parameters - if (requestId === root.currentRequestId && !foundFile && root.preferredBackend === "Auto") { - if (lyricsModel.count === 0) { - fallbackTimer.restart(); - } - } - foundFile = false; - } - } -} diff --git a/utils/scripts/lrcparser.js b/utils/scripts/lrcparser.js deleted file mode 100644 index 847779ed..00000000 --- a/utils/scripts/lrcparser.js +++ /dev/null @@ -1,62 +0,0 @@ -function parseLrc(text) { - if (!text) return []; - let lines = text.split("\n"); - let result = []; - - let timeRegex = /\[(\d+):(\d+\.\d+|\d+)\]/g; - - // Blacklist for credits/metadata often found in NetEase lyrics - const creditKeywords = [ - "作词", "作曲", "编曲", "制作", "收录", "演奏", "词:", "曲:", "Lyricist", "Composer", "Arranger", "Producer", "Mixing", "Mastering" - ]; - - for (let line of lines) { - - timeRegex.lastIndex = 0; - let matches = []; - let match; - - while ((match = timeRegex.exec(line)) !== null) { - matches.push(match); - } - - if (matches.length === 0) continue; - - let lyric = line.replace(timeRegex, "").trim(); - - let min = parseInt(matches[0][1]); - let sec = parseFloat(matches[0][2]); - let totalTime = min * 60 + sec; - - // Only filter credits if they appear in the first 20 seconds - if (totalTime < 20) { - let isCreditFormat = creditKeywords.some(k => lyric.includes(k)); - if (isCreditFormat && (lyric.includes(":") || lyric.includes(":") || lyric.length < 25)) { - continue; - } - } - - for (let match of matches) { - let min = parseInt(match[1]); - let sec = parseFloat(match[2]); - - result.push({ - time: min * 60 + sec, - text: lyric - }); - } - } - - result.sort((a, b) => a.time - b.time); - return result; -} - -function getCurrentLine(lyrics, position) { - const epsilon = 0.1; // 100ms tolerance - for (let i = lyrics.length - 1; i >= 0; i--) { - if ((position + epsilon) >= lyrics[i].time) { - return i; - } - } - return -1; -}