qs: better lyrics
This commit is contained in:
@@ -25,7 +25,7 @@ Rectangle {
|
|||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
UText {
|
UText {
|
||||||
text: LyricsService.lyrics[LyricsService.currentIndex] || ""
|
text: LyricsService.lyrics.get(LyricsService.currentIndex)?.line || ""
|
||||||
family: Fonts.sans
|
family: Fonts.sans
|
||||||
pointSize: Style.fontSizeM
|
pointSize: Style.fontSizeM
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
@@ -42,7 +42,7 @@ Rectangle {
|
|||||||
baseSize: parent.height - Style.marginXS * 2
|
baseSize: parent.height - Style.marginXS * 2
|
||||||
iconSize: Style.fontSizeM
|
iconSize: Style.fontSizeM
|
||||||
onClicked: {
|
onClicked: {
|
||||||
LyricsService.increaseOffset();
|
LyricsService.decreaseOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ Rectangle {
|
|||||||
baseSize: parent.height - Style.marginXS * 2
|
baseSize: parent.height - Style.marginXS * 2
|
||||||
iconSize: Style.fontSizeM
|
iconSize: Style.fontSizeM
|
||||||
onClicked: {
|
onClicked: {
|
||||||
LyricsService.decreaseOffset();
|
LyricsService.increaseOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,27 +16,45 @@ UBox {
|
|||||||
LyricsService.unregisterComponent("LyricsCard");
|
LyricsService.unregisterComponent("LyricsCard");
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ListView {
|
||||||
id: lyricsColumn
|
id: lyricsList
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.marginS
|
anchors.margins: Style.marginS
|
||||||
|
spacing: Style.marginM
|
||||||
Repeater {
|
|
||||||
model: LyricsService.lyrics
|
model: LyricsService.lyrics
|
||||||
|
currentIndex: LyricsService.currentIndex
|
||||||
|
clip: true
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (currentIndex >= 0)
|
||||||
|
positionViewAtIndex(currentIndex, ListView.Center);
|
||||||
|
|
||||||
UText {
|
}
|
||||||
Layout.fillWidth: true
|
Component.onCompleted: {
|
||||||
text: modelData
|
Qt.callLater(function() {
|
||||||
font.pointSize: index === LyricsService.currentIndex ? Style.fontSizeS : Style.fontSizeXS
|
if (currentIndex >= 0)
|
||||||
font.weight: index === LyricsService.currentIndex ? Style.fontWeightBold : Style.fontWeightRegular
|
positionViewAtIndex(currentIndex, ListView.Center);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: UText {
|
||||||
|
width: ListView.view.width
|
||||||
|
text: model.line || ""
|
||||||
|
font.pointSize: ListView.isCurrentItem ? Style.fontSizeS : Style.fontSizeXS
|
||||||
|
font.weight: ListView.isCurrentItem ? Style.fontWeightBold : Style.fontWeightRegular
|
||||||
font.family: Fonts.sans
|
font.family: Fonts.sans
|
||||||
color: index === LyricsService.currentIndex ? Colors.mOnSurface : Colors.mOnSurfaceVariant
|
color: ListView.isCurrentItem ? Colors.mOnSurface : Colors.mOnSurfaceVariant
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.WrapAnywhere
|
wrapMode: Text.WrapAnywhere
|
||||||
maximumLineCount: 1
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
LyricsService.seekToLyric(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,27 @@ import qs.Constants
|
|||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
spacing: Style.marginS
|
||||||
|
|
||||||
UText {
|
UText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.maximumWidth: buttonsGrid.width
|
Layout.maximumWidth: buttonsGrid.width
|
||||||
Layout.bottomMargin: Style.marginS
|
pointSize: Style.fontSizeS
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: (LyricsService.offset > 0 ? "+" + LyricsService.offset : LyricsService.offset) + " ms"
|
text: "Offset (ms)"
|
||||||
|
}
|
||||||
|
|
||||||
|
UText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.maximumWidth: buttonsGrid.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: (LyricsService.lyricsOffset > 0 ? "+" + LyricsService.lyricsOffset : LyricsService.lyricsOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
UDivider {
|
||||||
|
implicitWidth: buttonsGrid.implicitWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
@@ -29,7 +43,7 @@ ColumnLayout {
|
|||||||
colorFg: Colors.mCyan
|
colorFg: Colors.mCyan
|
||||||
iconName: "arrow-bar-up"
|
iconName: "arrow-bar-up"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
LyricsService.increaseOffset();
|
LyricsService.decreaseOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +54,7 @@ ColumnLayout {
|
|||||||
colorFg: Colors.mPurple
|
colorFg: Colors.mPurple
|
||||||
iconName: "arrow-bar-down"
|
iconName: "arrow-bar-down"
|
||||||
onClicked: {
|
onClicked: {
|
||||||
LyricsService.decreaseOffset();
|
LyricsService.increaseOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,17 +69,6 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UIconButton {
|
|
||||||
id: fasterButton
|
|
||||||
|
|
||||||
baseSize: 32
|
|
||||||
colorFg: Colors.mRed
|
|
||||||
iconName: "trash"
|
|
||||||
onClicked: {
|
|
||||||
LyricsService.clearCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UIconButton {
|
UIconButton {
|
||||||
id: barLyricsButton
|
id: barLyricsButton
|
||||||
|
|
||||||
@@ -78,18 +81,6 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UIconButton {
|
|
||||||
id: textButton
|
|
||||||
|
|
||||||
baseSize: 32
|
|
||||||
colorFg: Colors.mYellow
|
|
||||||
iconName: "align-box-left-bottom"
|
|
||||||
onClicked: {
|
|
||||||
LyricsService.showLyricsText();
|
|
||||||
controlCenterPanel.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UIconButton {
|
UIconButton {
|
||||||
baseSize: 32
|
baseSize: 32
|
||||||
colorFg: Colors.mOrange
|
colorFg: Colors.mOrange
|
||||||
|
|||||||
@@ -9,46 +9,25 @@ pragma Singleton
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int linesCount: 3
|
property bool isFetchingLyrics: false
|
||||||
property int linesAhead: linesCount / 2
|
// Lyrics state
|
||||||
readonly property int currentIndex: linesCount - linesAhead - 1
|
// { "time": 0, "line": "lyric line" }
|
||||||
readonly property string offsetFile: Paths.cacheDir + "/spotify-lyrics-offset.txt"
|
// time is in ms
|
||||||
property int offset: 0 // in ms
|
property ListModel lyrics
|
||||||
readonly property int offsetStep: 500 // in ms
|
property int lyricsOffset: 0 // in ms
|
||||||
|
property int currentIndex: -1
|
||||||
|
// Player state
|
||||||
|
// this property will be updated regardless of shouldRun for simplicity
|
||||||
|
property int internalPosition: 0
|
||||||
|
// Reference counting
|
||||||
property var _registered: ({
|
property var _registered: ({
|
||||||
})
|
})
|
||||||
readonly property int _registeredCount: Object.keys(_registered).length
|
readonly property int _registeredCount: Object.keys(_registered).length
|
||||||
readonly property bool shouldRun: _registeredCount > 0
|
readonly property bool shouldRun: _registeredCount > 0
|
||||||
// with linesCount=3 and linesAhead=1, lyrics will be like:
|
// Bar state
|
||||||
// line 1
|
|
||||||
// line 2 <- current line
|
|
||||||
// line 3
|
|
||||||
property var lyrics: Array(linesCount).fill(" ")
|
|
||||||
readonly property bool showLyricsBar: ShellState.lyricsState.showLyricsBar || false
|
readonly property bool showLyricsBar: ShellState.lyricsState.showLyricsBar || false
|
||||||
|
|
||||||
function toggleLyricsBar() {
|
signal lyricsUpdated()
|
||||||
ShellState.lyricsState = {
|
|
||||||
"showLyricsBar": !root.showLyricsBar
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSyncing() {
|
|
||||||
Logger.d("Lyrics", "Starting lyrics syncing");
|
|
||||||
// fill lyrics with empty lines
|
|
||||||
lyrics = Array(linesCount).fill(" ");
|
|
||||||
listenProcess.exec(["sh", "-c", `pkill -x spotify-lyrics -u $USER; spotify-lyrics listen -l ${linesCount} -a ${linesAhead} -f ${offsetFile}`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopSyncing() {
|
|
||||||
Logger.d("Lyrics", "Stopping lyrics syncing");
|
|
||||||
// kinda ugly but works, meanwhile:
|
|
||||||
// listenProcess.signal(9)
|
|
||||||
// listenProcess.signal(15)
|
|
||||||
// listenProcess.running = false
|
|
||||||
// counting on exec() to terminate previous exec()
|
|
||||||
// all don't work
|
|
||||||
Quickshell.execDetached(["sh", "-c", `pkill -x spotify-lyrics -u $USER`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerComponent(componentId) {
|
function registerComponent(componentId) {
|
||||||
root._registered[componentId] = true;
|
root._registered[componentId] = true;
|
||||||
@@ -64,105 +43,232 @@ Singleton {
|
|||||||
Logger.d("Lyrics", "Component unregistered:", componentId, "- total:", root._registeredCount);
|
Logger.d("Lyrics", "Component unregistered:", componentId, "- total:", root._registeredCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeOffset() {
|
function _requestFetchLyrics() {
|
||||||
offsetFileView.setText(String(offset));
|
if (!root.shouldRun)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
fetchLyricsTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchLyrics() {
|
||||||
|
root.lyrics.clear();
|
||||||
|
root.lyricsUpdated();
|
||||||
|
root.currentIndex = -1;
|
||||||
|
if (!root.shouldRun)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
root.isFetchingLyrics = true;
|
||||||
|
if (!MediaService.currentPlayer) {
|
||||||
|
root.isFetchingLyrics = false;
|
||||||
|
return ;
|
||||||
|
} else if (MediaService.currentPlayer.identity.toLowerCase() === "spotify")
|
||||||
|
lyricsProcess.request("spotify");
|
||||||
|
else if (MediaService.currentPlayer.identity.toLowerCase() === "elisa")
|
||||||
|
lyricsProcess.request("elisa");
|
||||||
|
else
|
||||||
|
root.isFetchingLyrics = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLRC(text) {
|
||||||
|
if (!root.shouldRun)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
const lines = text.split("\n");
|
||||||
|
let newLyrics = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
if (line.length === 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const lyricLine = line.replace(/\[.*?]/g, "").trim();
|
||||||
|
const regex = /\[(\d{2}):(\d{2})(?:\.(\d{2,3}))?]/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(line)) !== null) {
|
||||||
|
const minutes = parseInt(match[1], 10);
|
||||||
|
const seconds = parseInt(match[2], 10);
|
||||||
|
const milliseconds = match[3] ? parseInt(match[3].padEnd(3, "0"), 10) : 0;
|
||||||
|
const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds;
|
||||||
|
newLyrics.push({
|
||||||
|
"time": time,
|
||||||
|
"line": lyricLine
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newLyrics.sort((a, b) => {
|
||||||
|
return a.time - b.time;
|
||||||
|
});
|
||||||
|
root.lyrics.clear();
|
||||||
|
root.lyrics.append(newLyrics);
|
||||||
|
root.isFetchingLyrics = false;
|
||||||
|
root.lyricsUpdated();
|
||||||
|
updateIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIndex() {
|
||||||
|
if (!root.shouldRun)
|
||||||
|
return ;
|
||||||
|
|
||||||
|
if (root.lyrics.count === 0) {
|
||||||
|
if (root.currentIndex !== -1)
|
||||||
|
root.currentIndex = -1;
|
||||||
|
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
const effectiveTime = root.internalPosition + root.lyricsOffset;
|
||||||
|
let low = 0;
|
||||||
|
let high = root.lyrics.count - 1;
|
||||||
|
let bestMatch = -1;
|
||||||
|
while (low <= high) {
|
||||||
|
let mid = (low + high) >> 1;
|
||||||
|
let midTime = root.lyrics.get(mid).time;
|
||||||
|
if (midTime <= effectiveTime) {
|
||||||
|
bestMatch = mid;
|
||||||
|
low = mid + 1;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.currentIndex !== bestMatch)
|
||||||
|
root.currentIndex = bestMatch;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function increaseOffset() {
|
function increaseOffset() {
|
||||||
offset += offsetStep;
|
root.lyricsOffset += 500;
|
||||||
saveState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function decreaseOffset() {
|
function decreaseOffset() {
|
||||||
offset -= offsetStep;
|
root.lyricsOffset -= 500;
|
||||||
saveState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetOffset() {
|
function resetOffset() {
|
||||||
offset = 0;
|
if (root.lyricsOffset !== 0)
|
||||||
saveState();
|
root.lyricsOffset = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCache() {
|
function toggleLyricsBar() {
|
||||||
action.command = ["sh", "-c", "spotify-lyrics clear $(spotify-lyrics trackid)"];
|
ShellState.lyricsState = {
|
||||||
action.startDetached();
|
"showLyricsBar": !ShellState.lyricsState.showLyricsBar
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLyricsText() {
|
function seekToLyric(index) {
|
||||||
action.command = ["sh", "-c", "wezterm start -- sh -c 'spotify-lyrics fetch 2>/dev/null | less'"];
|
if (index < 0 || index >= root.lyrics.count)
|
||||||
action.startDetached();
|
return ;
|
||||||
|
|
||||||
|
const time = root.lyrics.get(index).time - root.lyricsOffset;
|
||||||
|
MediaService.seek(time / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOffsetChanged: {
|
|
||||||
if (root.showLyricsBar)
|
|
||||||
SendNotification.show("Lyrics Offset Changed", `Current offset: ${offset} ms`);
|
|
||||||
|
|
||||||
writeOffset();
|
|
||||||
}
|
|
||||||
onShouldRunChanged: {
|
onShouldRunChanged: {
|
||||||
if (shouldRun)
|
Logger.d("Lyrics", "Should run changed:", root.shouldRun);
|
||||||
startSyncing();
|
if (!root.shouldRun) {
|
||||||
|
root.lyrics.clear();
|
||||||
|
root.isFetchingLyrics = false;
|
||||||
|
} else {
|
||||||
|
root._requestFetchLyrics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onInternalPositionChanged: updateIndex()
|
||||||
|
onLyricsOffsetChanged: updateIndex()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCurrentPlayerChanged() {
|
||||||
|
_requestFetchLyrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTrackTitleChanged() {
|
||||||
|
_requestFetchLyrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTrackArtistChanged() {
|
||||||
|
_requestFetchLyrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTrackAlbumChanged() {
|
||||||
|
_requestFetchLyrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCurrentPositionChanged() {
|
||||||
|
root.internalPosition = MediaService.currentPosition * 1000;
|
||||||
|
if (shouldRun && MediaService.isPlaying)
|
||||||
|
updateInternalTimer.restart();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
target: MediaService
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: fetchLyricsTimer
|
||||||
|
|
||||||
|
interval: 100
|
||||||
|
repeat: false
|
||||||
|
running: false
|
||||||
|
onTriggered: fetchLyrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: lyricsProcess
|
||||||
|
|
||||||
|
property string queuedPlayer: ""
|
||||||
|
|
||||||
|
function request(player) {
|
||||||
|
this.queuedPlayer = player;
|
||||||
|
if (this.running)
|
||||||
|
this.running = false;
|
||||||
else
|
else
|
||||||
stopSyncing();
|
handleQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
function handleQueue() {
|
||||||
id: listenProcess
|
if (!this.queuedPlayer) {
|
||||||
|
root.isFetchingLyrics = false;
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
const player = this.queuedPlayer.toLowerCase();
|
||||||
|
this.queuedPlayer = "";
|
||||||
|
if (player === "spotify") {
|
||||||
|
this.command = ["lrcfetch", "fetch", "--player", "spotify"];
|
||||||
|
} else if (player === "elisa") {
|
||||||
|
this.command = ["lrcfetch", "fetch", "--player", "elisa"];
|
||||||
|
} else {
|
||||||
|
root.isFetchingLyrics = false;
|
||||||
|
root.parseLRC("");
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
this.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
running: false
|
running: false
|
||||||
|
onExited: (exitCode, exitStatus) => {
|
||||||
|
if (this.queuedPlayer)
|
||||||
|
Qt.callLater(handleQueue);
|
||||||
|
else
|
||||||
|
root.isFetchingLyrics = false;
|
||||||
|
}
|
||||||
|
|
||||||
stdout: SplitParser {
|
stdout: StdioCollector {
|
||||||
splitMarker: ""
|
onStreamFinished: {
|
||||||
onRead: (data) => {
|
root.parseLRC(this.text);
|
||||||
lyrics = data.split("\n").slice(0, linesCount);
|
|
||||||
if (lyrics.length < linesCount) {
|
|
||||||
// fill with empty lines if not enough
|
|
||||||
for (let i = lyrics.length; i < linesCount; i++) {
|
|
||||||
lyrics[i] = " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Timer {
|
||||||
id: action
|
id: updateInternalTimer
|
||||||
|
|
||||||
running: false
|
interval: 50
|
||||||
}
|
repeat: true
|
||||||
|
running: shouldRun && MediaService.isPlaying
|
||||||
FileView {
|
onTriggered: {
|
||||||
id: offsetFileView
|
root.internalPosition += this.interval;
|
||||||
|
|
||||||
path: offsetFile
|
|
||||||
watchChanges: false
|
|
||||||
onLoaded: {
|
|
||||||
try {
|
|
||||||
const fileContents = text();
|
|
||||||
if (fileContents.length > 0) {
|
|
||||||
const val = parseInt(fileContents);
|
|
||||||
if (!isNaN(val)) {
|
|
||||||
offset = val;
|
|
||||||
Logger.d("Lyrics", "Loaded offset:", offset);
|
|
||||||
} else {
|
|
||||||
offset = 0;
|
|
||||||
writeOffset();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
offset = 0;
|
|
||||||
writeOffset();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Logger.e("Lyrics", "Error reading offset file:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
Logger.e("Lyrics", "Error loading offset file.");
|
|
||||||
}
|
|
||||||
onSaveFailed: {
|
|
||||||
Logger.e("Lyrics", "Error saving offset file.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lyrics: ListModel {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
url=$(playerctl --player=elisa metadata xesam:url)
|
||||||
|
|
||||||
|
if [[ "$url" != file://* ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
path="${url#file://}"
|
||||||
|
lyrics_path="${path%.*}.lrc"
|
||||||
|
|
||||||
|
if [[ ! -e "$lyrics_path" ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$lyrics_path"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
lockfile=/tmp/fzfclip.lock
|
lockfile=/tmp/fzfclip.lock
|
||||||
exec {LOCK_FD}>"$lockfile"
|
exec {LOCK_FD}>"$lockfile"
|
||||||
@@ -7,4 +7,4 @@ flock -n "$LOCK_FD" || {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fzfclip "$@"
|
fzfclip "$@" {LOCK_FD}>&-
|
||||||
|
|||||||
Reference in New Issue
Block a user