Skip to content

feat: add media-lyrics plugin (synced lyrics media panel) - #592

Merged
ItsLemmy merged 6 commits into
noctalia-dev:mainfrom
TraNzeM:feat/media-lyrics
Sep 2, 2026
Merged

feat: add media-lyrics plugin (synced lyrics media panel)#592
ItsLemmy merged 6 commits into
noctalia-dev:mainfrom
TraNzeM:feat/media-lyrics

Conversation

@TraNzeM

@TraNzeM TraNzeM commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Plugin

  • Id: tranzem/media-lyrics
  • New plugin
  • Update to an existing plugin (version bumped in plugin.toml)

What it does

Karaoke-style time-synced lyrics media panel for the Noctalia shell. Shows the
currently playing track (album cover, marquee title/artist, transport controls,
progress bar) and a synced-lyrics carousel (10/14/16 lines per size preset)
where the active line is
highlighted and neighbours fade by distance.

  • Pure Luau implementation — no playerctl, no python daemons, no GTK
    overlays. Reads MPRIS via Noctalia's own D-Bus aggregator; the only runtime
    binaries are busctl (MPRIS poll) and curl (LRCLIB HTTPS fetch, see below).
  • Long lyric lines wrap instead of clipping — lines wider than the panel
    are soft-wrapped into balanced sub-lines; the karaoke window fits a vertical
    sub-line budget so the active line is never pushed below the panel edge.
  • LRCLIB integration — exact /api/get lookup first, /api/search fallback,
    LRC parsed in pure Luau. Fetched with curl -sSf -m 8 -4 <url> (argv-only,
    no shell; IPv4 forced because the built-in HTTP client times out against
    lrclib.net on some setups).
  • Clickable lyric lines — click a synced line (or press Return/Space on the
    keyboard cursor) to seek the player to that line's timestamp; a chevron
    marker highlights the selected line. Up/Down scroll the lyrics manually.
  • Local .lrc files — take priority over the network (offline-friendly);
    LRCLIB responses are cached on disk (opt-in).
  • Translatable UI — all strings route through noctalia.tr() (English
    ships in translations/en.json; other locales arrive via Noctalia
    Translate), with lyric timing offset, cache and lyrics-folder settings.
  • Entries: bar widget now-playing (click → panel, middle-click → play/pause,
    scroll → next/prev), panel panel (520×520, floating), service service,
    shortcut toggle.

External dependencies

  • busctl — polls the MPRIS D-Bus aggregator (dev.noctalia.Mpris) at a
    150 ms cadence. Declared in dependencies = ["busctl", "curl"] in plugin.toml.
  • curl — LRCLIB HTTPS fetch (curl -sSf -m 8 -4 <url>, argv-form, no
    shell, no stored config). Declared in dependencies and README Requirements.
  • sleep (coreutils) — 0.35 s refresh delay after transport commands
    (argv-form, no shell); documented in README Notes.
  • https://lrclib.net — HTTPS network calls only: /api/get (exact match),
    then /api/search (fallback). Nothing is uploaded; no analytics, no tracking.
  • Filesystem: on-disk lyric cache in the plugin data directory (opt-in
    use_cache setting); local .lrc files read from local_lyrics_dir
    (default ~/.local/share/media-lyrics).
  • No remote code execution; the complete spawned-process list is: busctl,
    curl, sleep — all argv-form, no shell.

Testing

  • Opened the panel (noctalia msg panel-toggle tranzem/media-lyrics:panel) on
    tracks with synced and plain lyrics — both render correctly.

  • Clickable lines: seek verified live via MPRIS position — 3s → 136s jump to
    the target line's timestamp on a synced track.

  • Keyboard: Up/Down move the lyric cursor (chevron marker), Return/Space seek;
    plain lyrics scroll but don't seek (no timestamps).

  • Bar widget now-playing: click opens panel, middle click toggles play/pause,
    scroll switches tracks ([widget.actions] middle = "none" declared).

  • Settings toggled: offset_ms, use_cache, local_lyrics_dir.

  • luac -p passes on every entry script.

  • Community review round 1 (2026-09-02): curl declared in dependencies
    and README; plugin description no longer claims "no external dependencies";
    panel strings wrapped in noctalia.tr() (keys added to en.json);
    thumbnail regenerated with the official thumbnail generator (960×540 WebP).

  • Long-line regression test (v0.8.11): 47/56/64/72-char lyric fixtures on the
    compact preset wrap into balanced sub-lines with no right-edge clipping;
    pixel-verified that the active line stays fully above the panel edge.

  • Review-hardening round 2 (v0.8.13): widget/provider/error strings via
    noctalia.tr(); transport failures surface a real error state (verified by
    code path — curl exit ≠ 0/22); UTF-8 Cyrillic fixture renders with no
    mojibake and no "ui tree node is not a table" warnings (WRN delta 0 on
    synced and plain tracks).

  • Tested on Niri

  • Tested on Hyprland

  • Tested on Sway

  • Tested on another compositor: Umbriel (Wayland)

  • Noctalia version tested against: v5 beta (5.0.0_beta.10)

  • Plugin API level: 24

Screenshots / Videos

Light theme Dark theme
light dark
Settings
settings

Checklist

  • The directory name matches the part of id after the / in plugin.toml exactly.
  • It ships plugin.toml, README.md, thumbnail.webp, and translations/en.json.
  • README.md follows the
    README template, documents
    every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
  • I created thumbnail.webp with the thumbnail generator.
  • version follows semver and is bumped in this PR; plugin_api is the oldest API level this plugin requires.
  • Every non-English translation in this PR uses a locale supported by Noctalia core, and I can read, write, and
    understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
  • I did not edit catalog.toml; CI generates it.
  • This PR touches exactly one plugin directory.

Code review attestation

  • The code is readable and not obfuscated, minified, or generated.
  • It does not download and execute remote code.
  • Every network call, filesystem write, and spawned process is something the description above accounts for.
  • I have the right to publish this code under the license declared in plugin.toml.

Karaoke-style time-synced lyrics panel for Noctalia: 14-line lyric
carousel, album cover, transport controls, progress bar, marquee
titles. Pure Luau, no playerctl/python/GTK — MPRIS via busctl,
lyrics from LRCLIB with local .lrc support and on-disk cache.
…roll, absolute seek fix)

- Clickable lyric lines: click or Return/Space seeks to the line timestamp
- Manual lyric scroll: Up/Down cursor with chevron marker (capture_keys +
  onKey, keyboard_focus = exclusive)
- FIX: busctlCall dropped D-Bus signature — seek/shuffle/loop silently
  failed; types (b/s/x) now passed, seek uses SetPositionActive (absolute)
- FIX: bar widget empty state renders a disc tree instead of setGlyph/setText
  (dead after first render())
- version 0.8.5
@ItsLemmy

ItsLemmy commented Sep 2, 2026

Copy link
Copy Markdown
Contributor
  1. blocking.
    The thumbnail was not generated by the thumbnail generator (see checklist)

  2. non-blocking - media-lyrics/service.luau:304
    The service spawns the curl CLI for every LRCLIB fetch ("-sSf -m 8 -4" argv form, no
    shell), but curl is not declared: dependencies = ["busctl"] only (media-lyrics/plugin.toml:20),
    the README Requirements section lists only busctl and HTTPS access, and the PR description
    claims "No other commands, no remote code execution" while the Code review attestation
    states every spawned process is accounted for. The changelog's own 0.8.3 entry states the
    rule "every shelled-out command must be declared" (media-lyrics/CHANGELOG.md). sleep
    (coreutils) is also spawned (media-lyrics/service.luau:503). Impact: on a system without
    curl the network lyric pipeline silently fails and only cache/local lyrics work. Declare
    curl in dependencies and the README, and correct the description/attestation wording.

  3. non-blocking - media-lyrics/panel.luau:121
    Several user-visible strings are hardcoded English instead of noctalia.tr calls:
    "Loading lyrics…" (panel.luau:121), "Lyrics error: " (panel.luau:125), "No lyrics found"
    (panel.luau:129), "(no title)" (panel.luau:450), "NOW PLAYING" (panel.luau:490),
    "Reload lyrics" (panel.luau:495). The README advertises a "Bilingual UI (en/ru)" feature,
    but only translations/en.json ships and these strings bypass translation entirely, so
    they render in English for every locale. Minor localization gap in a claimed feature.

@ItsLemmy
ItsLemmy marked this pull request as draft September 2, 2026 03:06
@TraNzeM

TraNzeM commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all three points are addressed. Summary of what changed since your pass (media-lyrics is now at v0.8.13 in this branch):

0 (blocking) — thumbnail
Regenerated with the official thumbnail generator (assets.noctalia.dev/plugins/thumbnail-generator.html) — 960×540 WebP, replaced in media-lyrics/thumbnail.webp.

2 (non-blocking) — curl/sleep accounting

  • plugin.toml: dependencies = ["busctl", "curl"]; description no longer claims "no external dependencies" — it states the real runtime needs.
  • README Requirements now lists curl (spawn form, argv-only, no shell) and mentions sleep (coreutils); README Notes has the complete spawned-process list: busctl, curl, sleep — all argv-form.
  • PR description + attestation reworded accordingly ("No remote code execution; the complete spawned-process list is: busctl, curl, sleep").

3 (non-blocking) — i18n

  • All the listed panel strings now go through noctalia.tr() with keys in translations/en.json: "Loading lyrics…", "Lyrics error: ", "No lyrics found", "(no title)", "NOW PLAYING", "Reload lyrics", plus "No media player" and the "synced/unsynced" footer suffixes.
  • Extended beyond the list while at it (same class): widget title fallback, the panel "Unknown artist" fallback, and the footer provider labels ("Local"/"Cache") also route through noctalia.tr().
  • README claim softened from "Bilingual UI (en/ru)" to "translatable via Noctalia i18n" (only en.json ships per the community rule).

Extra hardening in this branch (0.8.11–0.8.13)

  • Long lyric lines (47+ chars) no longer clip at the panel edge — they soft-wrap into balanced sub-lines; the karaoke window fits a vertical sub-line budget so the active line never drops below the panel edge (pixel-verified on the compact preset with 47/56/64/72-char fixtures).
  • Network failures are surfaced as a real error state ("LRCLIB unreachable") instead of the misleading "No lyrics found" (curl exit ≠ 0 and ≠ 22).
  • UTF-8-safe cuts (Cyrillic titles/lines verified live), nil-children removed from the UI tree (the host's "ui tree node is not a table" warning is gone — WRN delta 0), docs/comments reconciled with the code (10/14/16 lines per preset, 150 ms poll, no stale "ring"/"zero external tools" wording).

Both CI checks (enforce, validate) are green on the current head (3b387c8). Would appreciate a re-review when you get a chance — happy to adjust anything else.

@TraNzeM
TraNzeM marked this pull request as ready for review September 2, 2026 20:08
@ItsLemmy
ItsLemmy merged commit 7fc92c3 into noctalia-dev:main Sep 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants