ttmldb
API REFERENCE
← back to library

A small self-hosted lyric store, partially inspired by LRCLIB, except this one stores full TTML lyrics, allowing for word-level timing, multiple singers, custom metadata, etc.

Base URL: https://ttmldb.sowur.net

Authentication

Reads (/api/search, /api/get*) are always open. POST /api/upload only requires an X-API-Key header if the server has one configured - check with whoever runs this instance whether that's the case for you.

There is no deletion or bulk-edit capability on this API, and that's by design. That capability lives on a separate, network-isolated admin surface that the public can't reach.

Endpoints

GET /health

Are we alive?

curl https://ttmldb.sowur.net/health
{"status": "ok"}

GET /api/search

There are two types of searching, with the difference being "Quoting":

paramtyperequirednotes
qstringrequiredsee quoting rules above
limitintoptionaldefault 20, max 100
# metadata search
curl "https://ttmldb.sowur.net/api/search?q=Moment"

# lyrics-content search
curl "https://ttmldb.sowur.net/api/search?q=%22moment%22"
[
  {
    "id": 7,
    "track_id": 7,
    "track_name": "Moment",
    "artist_name": "Athena, smle",
    "album_name": "Moment",
    "duration": 162.525,
    "source": "web-upload",
    "timed_by": "Sowren",
    "snippet": "All it takes is a [moment]\nIs..."
  }
]

GET /api/get

Best-effort match by track/artist(/album). Tries an exact normalized signature first, then falls back to a looser prefix match. If a duration is supplied and a candidate falls within ~2 seconds of it, that one wins.

paramtyperequired
track_namestringrequired
artist_namestringrequired
album_namestringoptional
durationfloat (seconds)optional
curl "https://ttmldb.sowur.net/api/get?track_name=Moment&artist_name=Athena&duration=162"

Returns the full record, including the raw ttml string. Returns 404 if nothing matches.

GET /api/get/{lyrics_id}

Same shape as above, just directly by id.

curl https://ttmldb.sowur.net/api/get/7

GET /api/get/{lyrics_id}/ttml

This endpoint is purely for convenience. It supplies the raw .ttml body, correct application/ttml+xml content type, no JSON wrapper.

curl https://ttmldb.sowur.net/api/get/7/ttml

POST /api/upload

Adds a lyrics entry. If a track with the same normalized track+artist+album signature already exists, this attaches a new lyrics entry to it rather than duplicating the track. Useful for multiple takes/re-syncs of the same song.

fieldtyperequirednotes
track_namestringrequired
artist_namestringrequired
album_namestringoptional
durationfloatoptionalauto-extracted from <body dur> if omitted
ttmlstringrequiredthe full TTML document
plain_lyricsstringoptionalauto-extracted from the TTML if omitted
timed_bystringoptionalauto-extracted from a timedby:person metadata field if omitted
sourcestringoptionaldefaults to "manual" free text, shown as a tag on search results
# PowerShell
$ttml = [System.IO.File]::ReadAllText("C:\path\to\file.ttml")
$body = @{
    track_name  = "Moment"
    artist_name = "Athena"
    ttml        = $ttml
    source      = "api-upload"
} | ConvertTo-Json
Invoke-RestMethod -Uri "https://ttmldb.sowur.net/api/upload" -Method Post `
    -ContentType "application/json" -Body $body

Returns 201 with the full stored record on success.

Response shapes

Search result, one entry per match:

{
  "id": int,            // lyrics entry id
  "track_id": int,
  "track_name": string,
  "artist_name": string,
  "album_name": string | null,
  "duration": float | null,
  "source": string,
  "timed_by": string | null,
  "snippet": string      // [bracketed] on a lyrics-content match
}

Full lyrics record. from /api/get and /api/upload:

{
  "id": int,
  "track_id": int,
  "track_name": string,
  "artist_name": string,
  "album_name": string | null,
  "duration": float | null,
  "ttml": string,          // full TTML document
  "plain_lyrics": string | null,
  "timed_by": string | null,
  "source": string,
  "is_synced": bool
}

A note on TTML conventions

Beyond standard TTML, this database recognises a couple of custom namespaced fields if they're present in <head><metadata> — harmless to omit, but picked up automatically if you have them:

This is a small self-hosted library, not a public-scale service. Please, be reasonable with request volume. No hard rate limit is enforced today, but that's a courtesy, not a guarantee.