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
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.
Are we alive?
curl https://ttmldb.sowur.net/health
{"status": "ok"}
There are two types of searching, with the difference being "Quoting":
?q=Moment — bare, unquoted → searches metadata only (track/artist/album name), every word ANDed regardless of order.?q="Moment" — quoted → searches lyric content only, full-text, and returns a highlighted snippet.| param | type | required | notes |
|---|---|---|---|
q | string | required | see quoting rules above |
limit | int | optional | default 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..."
}
]
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.
| param | type | required |
|---|---|---|
track_name | string | required |
artist_name | string | required |
album_name | string | optional |
duration | float (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.
Same shape as above, just directly by id.
curl https://ttmldb.sowur.net/api/get/7
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
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.
| field | type | required | notes |
|---|---|---|---|
track_name | string | required | |
artist_name | string | required | |
album_name | string | optional | |
duration | float | optional | auto-extracted from <body dur> if omitted |
ttml | string | required | the full TTML document |
plain_lyrics | string | optional | auto-extracted from the TTML if omitted |
timed_by | string | optional | auto-extracted from a timedby:person metadata field if omitted |
source | string | optional | defaults 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.
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
}
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:
<lemn:artist> / <lemn:album> — redundant with the upload fields, mostly there so the file is self-describing on its own.<timedby:person> — credits whoever did the timing/sync work; surfaces as timed_by in every response.