Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/x-eon-max/LiveLyrics/llms.txt

Use this file to discover all available pages before exploring further.

When a new track is detected, LiveLyrics kicks off a three-stage lyrics pipeline: it fetches timestamped lyrics from the LRCLIB public API, parses the .lrc format into a structured list of (timestamp, text) pairs, and then — on every loop tick — picks the single lyric line whose timestamp best matches the current playback position.

LRCLIB API

LiveLyrics calls the LRCLIB search endpoint with the track title and artist name as query parameters:
res = requests.get(
    "https://lrclib.net/api/search",
    params={"track_name": title, "artist_name": artist},
    timeout=10,
)
results = res.json()
synced = results[0].get("syncedLyrics")
The API returns a JSON array of candidate tracks sorted by relevance. LiveLyrics always takes the first result (results[0]) and reads only the syncedLyrics field — the millisecond-timestamped .lrc string. Plain (unsynced) lyrics are not used, because without timestamps there is no way to match a line to a position in the track.

Caching

Network round-trips to LRCLIB happen only once per unique track. LiveLyrics maintains a module-level LYRICS_CACHE dictionary keyed by (title, artist) tuples. Before making any HTTP request, fetch_synced_lyrics checks the cache:
LYRICS_CACHE = {}

def fetch_synced_lyrics(title, artist):
    key = (title, artist)
    if key in LYRICS_CACHE:
        return LYRICS_CACHE[key]
    # ... fetch and cache ...
    LYRICS_CACHE[key] = parsed
    return parsed
If the key already exists, the function returns immediately with the previously parsed list. This also means that tracks with no available lyrics are cached as empty lists ([]), so LiveLyrics will not re-query LRCLIB for them on subsequent track-change events within the same session.
LYRICS_CACHE is an in-memory dictionary and is not persisted to disk. It resets to empty each time the script is restarted, so the first play of any track in a new session will always trigger a fresh LRCLIB request.

LRC Parsing

LRCLIB’s syncedLyrics field contains a multi-line string in the standard .lrc format, where every line begins with a bracketed timestamp followed by the lyric text:
[00:17.520] Last night I dreamt of some bagels
[00:22.110] I was eating them in a field
LiveLyrics parses this format with a compiled regular expression and converts each timestamp to a plain floating-point number of seconds:
pattern = re.compile(r'^\[(\d{2}):(\d{2})\.(\d{2,3})\]\s*(.*)$')

def parse_synced_lyrics(synced):
    lines = []
    for raw_line in synced.split('\n'):
        m = pattern.match(raw_line)
        if not m:
            continue
        minutes, seconds, frac, text = m.groups()
        frac = frac.ljust(3, '0')
        total_sec = int(minutes) * 60 + int(seconds) + int(frac) / 1000
        if text.strip():
            lines.append((total_sec, text.strip()))
    lines.sort(key=lambda x: x[0])
    return lines
The function returns a sorted list of (timestamp_seconds, lyric_text) tuples. Lines with empty text (common for instrumental breaks in .lrc files) are silently dropped. The frac.ljust(3, '0') call is a normalization step: some .lrc sources use two-digit centisecond fractions ([01:23.45]) while others use three-digit millisecond fractions ([01:23.456]). Left-padding the shorter variant to three digits ensures both are interpreted correctly as thousandths of a second.

Line Matching

get_current_line() takes the sorted lyrics list and the current playback position and returns whichever lyric line should be displayed right now:
def get_current_line(lyrics_lines, position_sec):
    current = None
    for ts, text in lyrics_lines:
        if ts <= position_sec:
            current = text
        else:
            break
    return current
The function iterates forward through the sorted list, overwriting current with each line whose timestamp is at or before the current position. The first line whose timestamp exceeds the position halts the search. The result is always the most recent line that has already been reached — which is exactly what should be displayed. current starts as None, so if the playback position is before the very first lyric, the function returns None and no status update is triggered. In main(), the returned line is compared against last_line; a Discord status update is only sent when line != last_line, ensuring the API is called only on an actual lyric transition.

Fallback

If fetch_synced_lyrics returns an empty list — either because LRCLIB returned no results, the first result had no syncedLyrics field, or a network error occurred — main() falls back to a simple artist–title status string:
update_status(f"{track['artist']} - {track['title']}"[:128])
This ensures Discord always shows something meaningful for the currently playing track, even when timestamped lyrics are unavailable.

Build docs developers (and LLMs) love