Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions spotify-lyrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Noctalia Synced Lyrics Plugin

A seamless, time-synced scrolling lyrics panel for the Noctalia desktop shell. It integrates directly into your Noctalia bar and displays a beautifully formatted, auto-scrolling lyrics card when you click the `♫` icon.

## Why it's great
* **Zero Configuration:** No Spotify `sp_dc` cookies, API keys, or web scraping required! It pulls lyrics from public databases like LRCLIB and NetEase automatically.
* **Blazing Fast:** Uses a lightweight Python background daemon that caches lyrics to your disk so subsequent plays load in 0ms.
* **Native Shell Integration:** Doesn't feel like a clunky third-party app. It uses Noctalia's native declarative UI framework for buttery smooth, theme-aware rendering.

## Plugin
- **Id:** `noctalia/spotify-lyrics`
- **Widgets:**
- `lyrics`: The bar icon that toggles the lyrics panel.
- **Panels:**
- `lyrics-panel`: The scrolling lyrics panel.
- To toggle it manually via IPC, run: `noctalia msg panel-toggle noctalia/spotify-lyrics:lyrics-panel`

## Usage

### 1. Install Dependencies
You need `playerctl` and the `syncedlyrics` python package.
```bash
# Arch Linux
sudo pacman -S playerctl
pip install syncedlyrics
```

### 2. Set up the Background Daemon
The daemon listens to your media player (Spotify, MPD, etc.) and fetches the lyrics.

1. Copy the `spotify_lyrics_daemon.py` file to your preferred location (e.g., `~/.local/bin/`).
2. Set it up to run in the background. The recommended way is using a systemd user service:

```ini
# ~/.config/systemd/user/noctalia-lyrics.service
[Unit]
Description=Noctalia Lyrics Daemon
After=graphical-session.target

[Service]
ExecStart=/usr/bin/python3 /path/to/spotify_lyrics_daemon.py
Restart=always

[Install]
WantedBy=default.target
```
Start and enable the daemon:
```bash
systemctl --user daemon-reload
systemctl --user enable --now noctalia-lyrics.service
```

### 3. Enable the Noctalia Plugin
1. Install this plugin from the plugin manager or download the folder to `~/.local/share/noctalia/plugins/spotify-lyrics/`.
2. Enable the plugin via CLI:
```bash
noctalia msg plugins enable noctalia/spotify-lyrics
```
3. Add the `lyrics` widget to your bar's layout in your `~/.local/state/noctalia/settings.toml` (next to the `media` widget).

```toml
start = [ "launcher", "workspaces", "media", "lyrics" ]
```

That's it! Play a song on Spotify and a `♫` icon will appear in your bar. Click it to view the synced lyrics.
36 changes: 36 additions & 0 deletions spotify-lyrics/bar.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--!nonstrict
-- Minimal bar trigger: shows a small lyrics glyph next to the media widget.
-- Auto-hides when no music is playing. Click to toggle the lyrics panel.

local function readState()
local content = noctalia.readFile("~/.cache/noctalia/lyrics/current.json")
if not content then return nil end
local state, _ = noctalia.json.decode(content)
return state
end

local tickCount = 0

function update()
noctalia.setUpdateInterval(100)
tickCount = tickCount + 1
noctalia.state.set("lyricsTick", tickCount)

local state = readState()

if not state or state.status == "Stopped" or state.status == "" then
barWidget.setVisible(false)
return
end

barWidget.setVisible(true)
barWidget.setGlyph("music")
barWidget.setText("")
barWidget.setGlyphColor("primary")

barWidget.setTooltip(state.current or "...")
end

function onClick()
noctalia.togglePanel("noctalia/spotify-lyrics:lyrics-panel")
end
119 changes: 119 additions & 0 deletions spotify-lyrics/panel.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
--!nonstrict
-- Lyrics panel: 3-line synced view (prev / current / next).
-- Anchored to the bar widget so it feels like part of the media controls.

local function readState()
local content = noctalia.readFile("~/.cache/noctalia/lyrics/current.json")
if not content then return nil end
local state, _ = noctalia.json.decode(content)
return state
end

local function render()
local state = readState()

-- No music
if not state or state.status == "Stopped" or state.status == "" then
panel.render(ui.column({ flexGrow = 1, gap = 8, align = "stretch", justify = "center", padding = 16 }, {
ui.row({ justify = "center" }, { ui.glyph({ name = "music", size = 28, color = "on_surface/0.2" }) }),
ui.label({ text = "No music playing", fontSize = 14, fontWeight = "medium", color = "on_surface/0.25", textAlign = "center" }),
}))
return
end

-- Paused
if state.status == "Paused" then
panel.render(ui.column({ flexGrow = 1, gap = 6, align = "stretch", justify = "center", padding = 16 }, {
ui.row({ gap = 6, justify = "center", align = "center" }, {
ui.glyph({ name = "player-pause", size = 16, color = "on_surface/0.3" }),
ui.label({
text = `{state.title or "Paused"} — {state.artist or ""}`,
fontSize = 12,
fontWeight = "medium",
color = "on_surface/0.4",
textAlign = "center",
wrap = true,
}),
}),
ui.label({
text = state.current or "Paused",
fontSize = 18,
fontWeight = "bold",
color = "on_surface/0.6",
textAlign = "center",
wrap = true,
}),
}))
return
end

-- ── Playing ──

local rows = {}

-- Track header
if state.title and state.artist then
table.insert(rows, ui.column({ gap = 4, align = "stretch" }, {
ui.row({ justify = "center" }, { ui.glyph({ name = "music", size = 14, color = "primary/0.7" }) }),
ui.label({
text = `{state.title} • {state.artist}`,
fontSize = 12,
fontWeight = "medium",
color = "primary/0.7",
textAlign = "center",
wrap = true,
}),
}))
table.insert(rows, ui.box({ height = 1, fill = "on_surface/0.06" }))
end

-- Previous line (faded)
local prevText = state.prev or ""
if prevText ~= "" then
table.insert(rows, ui.label({
text = prevText,
fontSize = 14,
fontWeight = "medium",
color = "on_surface/0.4",
wrap = true,
textAlign = "center",
}))
else
table.insert(rows, ui.box({ height = 16 }))
end

-- Current line (bold, prominent)
table.insert(rows, ui.label({
text = state.current or "...",
fontSize = 18,
fontWeight = "bold",
color = "on_surface/0.95",
wrap = true,
textAlign = "center",
}))

-- Next line (faded)
local nextText = state.next or ""
if nextText ~= "" then
table.insert(rows, ui.label({
text = nextText,
fontSize = 14,
fontWeight = "medium",
color = "on_surface/0.4",
wrap = true,
textAlign = "center",
}))
else
table.insert(rows, ui.box({ height = 16 }))
end

panel.render(ui.column({ flexGrow = 1, gap = 8, align = "stretch", justify = "center", padding = 16 }, rows))
end

noctalia.state.watch("lyricsTick", function(_value)
render()
end)

function onOpen(_context)
render()
end
23 changes: 23 additions & 0 deletions spotify-lyrics/plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
id = "noctalia/spotify-lyrics"
name = "Spotify Lyrics"
version = "1.2.1"
plugin_api = 3
author = "goatnath"
license = "MIT"
dependencies = []
tags = ["music"]
icon = "music"
description = "Time-synced lyrics panel linked to the media widget."

# Minimal bar trigger: tiny glyph icon next to the media widget.
# Auto-hides when nothing is playing. Click to open the lyrics panel.
[[widget]]
id = "lyrics"
entry = "bar.luau"

# Lyrics panel: 3-line synced view anchored near the media area.
[[panel]]
id = "lyrics-panel"
entry = "panel.luau"
width = 460
height = 200
Loading