Ctrl K
ComunidadGuías…

proposals/2026-09-21-lyrics.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Lyrics

**Goal** - tap the song title in the player and read the words, scrolling in time with
the music when the library has timings for it.

**Tier** - T2. One new method on `peartune/media/1` (`lyrics.get`) and one new `ping`
capability (`caps.lyrics`). Both are additive: an old host never advertises the cap and
the app never asks, an old phone never asks a new host. Nothing stored, nothing
replicated, no grant semantics touched - lyrics are read-only library data, so a
readonly grant may fetch them.

## What is wrong

There are no lyrics anywhere in PearTune. Every source we support can supply them and
we ask none of them. It is the most-expected music-player feature we do not have, and
for a lot of libraries the words are already sitting in the files.

## Where the words come from

One optional adapter method, `lyrics(trackId)`, resolving to `null` (nothing known) or
`{ synced: boolean, lines: [{ t, text }] }` - `t` is milliseconds from the start of the
track, and is `null` on every line when `synced` is false.

| Adapter | Where it reads |
| --- | --- |
| Subsonic | `getLyricsBySongId` (the OpenSubsonic extension) for synced lines; falls back to classic `getLyrics` by artist+title, which is plain text only. Navidrome serves the first. |
| Jellyfin / Emby | `GET /Audio/{id}/Lyrics`, whose `Lyrics[]` carries `Start` in ticks (10,000 per ms) and `Text`. |
| Folder | A `.lrc` beside the file wins (it is what people curate); otherwise `common.lyrics` from the `music-metadata` scan, which is unsynced. |
| Audiobookshelf | None. The method is absent, the cap is off for a books-only host. |
| Combined | Delegates to whichever adapter owns the track, like `stream`. |

An adapter without the method, or with it answering `null`, is not a failure: the app
gets `{ lines: [] }` and says the library has no words for this one.

## The shape

- **Host** - `lyrics.get { trackId }` -> `{ synced, lines }`, added to `METHODS` and NOT
  to `MUTATING`. `ping` gains `caps.lyrics: 1` when the live adapter implements the
  method. Answers are small (a few KB at most) and parsed on the host, so the phone
  never sees an `.lrc` file or a ticks-vs-ms difference.
- **Phone** - a `lyrics` RPC in `src/bare.js`, gated on `capsFor(libraryId).lyrics` the
  way seek-offset already gates on `caps.timeOffset`. One fetch per track, cached in
  memory for the current track only.
- **UI** - a Lyrics panel in the expanded player. With timings, the current line is
  highlighted and the panel follows playback (and a tap on a line seeks to it); without,
  it is plain scrolling text. No button at all when the cap is off, the same as playlists
  on a host too old for them.

## Backwards compatibility

- An old host: no `caps.lyrics`, so the app never calls `lyrics.get` and shows no
  button. If it is called anyway the host answers `ENOMETHOD`, which the client already
  treats as "the host is too old" everywhere else (`worklet/outbox.js`, `client/index.js`).
- An old phone against a new host: it never asks. The extra `caps` key is ignored -
  `caps` is a JSON object on the wire and has taken new keys before (`books`).
- No migration: nothing is persisted.

## What could go wrong

- **A huge or malicious lyrics payload.** The host caps the answer (256 KB read, 2,000
  lines) and truncates rather than failing, so a bad `.lrc` cannot be used to push
  megabytes at a phone on cellular.
- **Timings that lie.** Out-of-order or negative `t` values sort and clamp on the host.
  A line with no parseable time in a mostly-synced file drops to unsynced for the
  whole track rather than jumping the highlight around.
- **A source that is slow to answer.** The fetch is off the playback path entirely:
  lyrics are asked for when the panel opens, never as part of starting a track.

## Verify

`npm run verify` green, plus: unit tests for the `.lrc` and ticks parsers and for the
cap gate; the Subsonic and Jellyfin adapters against their fixtures; on the emulator,
a track with a `.lrc` scrolls in time and a track without one says so; and an OLD host
(the Umbrel, until it is updated) shows no Lyrics button at all.