# QRC format

The QQ provider (`/qq/getLyrics`) returns lyrics in QRC, a format with word-level timing. This page describes the response and how to parse it.

## Response structure

```json
{
  "lyrics": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<QrcInfos>...</QrcInfos>",
  "provider": "qq"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `lyrics` | string | An XML document that wraps the QRC body |
| `provider` | string | Always `qq` |

## The XML envelope

The QRC body is not an element of its own. It is the value of the `LyricContent` attribute on `<Lyric_1>`, inside `<QrcInfos><LyricInfo>`. The attribute value spans several lines.

```xml
<?xml version="1.0" encoding="utf-8"?>
<QrcInfos>
<QrcHeadInfo SaveTime="306" Version="100"/>
<LyricInfo LyricCount="1">
<Lyric_1 LyricType="1" LyricContent="[ti:Wanderlust]
[ar:The Weeknd]
[al:Kiss Land]
[by:]
[offset:0]
[5767,324]Wanderlust - (5767,109)The (5876,106)Weeknd(5982,109)
...
[292333,1910]Let (292333,136)it (292469,97)come (292566,202)to (293301,377)you(293678,565)
"/>
</LyricInfo>
</QrcInfos>
```

Because the body is an attribute value, a double quote or ampersand inside it arrives as `&quot;` or `&amp;`. Decode these after you extract the body.

## Header tags

The body starts with LRC-style tags. Any of them can be empty.

| Tag | Meaning |
| --- | --- |
| `[ti:]` | Title |
| `[ar:]` | Artist |
| `[al:]` | Album |
| `[by:]` | Author of the lyrics file |
| `[offset:]` | Timing offset in milliseconds |

Header tags carry no lyrics. The braccato parser skips every tag of the form `[letters:...]`, including `[offset:]`.

## Line structure

Each lyric line starts with `[startMs,durationMs]`. Then come the words, and each word is followed by its own timing token `(startMs,durationMs)`:

```
[292333,1910]Let (292333,136)it (292469,97)come (292566,202)to (293301,377)you(293678,565)
```

This line breaks down as:

| Text | Start (ms) | Duration (ms) |
| --- | --- | --- |
| line | 292333 | 1910 |
| `"Let "` | 292333 | 136 |
| `"it "` | 292469 | 97 |
| `"come "` | 292566 | 202 |
| `"to "` | 293301 | 377 |
| `"you"` | 293678 | 565 |

**The text comes before its timing token.** A word is all the text between the end of the previous token (or the line header) and the start of its own token. The space between words belongs to the word before it, so join the words as they are to rebuild the line.

## Time format

-   All times are integers in milliseconds.
-   Word start times are absolute, measured from the start of the song, not from the start of the line.
-   The line header gives the start and length of the whole line.
-   There can be gaps between words. A word ends at `start + duration`, not where the next word starts.
-   A duration of `0` can occur. The braccato parser then uses the start of the next line, or the song duration for the last line.

## Credit and title lines

The first lines of a QRC body often repeat the title and artist, or list credits. These are timed like lyrics, so a naive parser shows them as sung lines:

```
[5767,324]Wanderlust - (5767,109)The (5876,106)Weeknd(5982,109)
[6091,2858]Lyrics (6091,125)by：(6216,117)QUENNEVILLE/(6333,120)...
[8949,3816]Composed (8949,126)by：(9075,162)QUENNEVILLE/(9237,144)...
```

Credit lines start with a label and a colon, either `:` or the full-width `：`. The labels are English (`Lyrics by`, `Composed by`, `Arranged by`, `Producer`) or Chinese (`作词`, `作曲`, `编曲`, `制作人`, and others).

### Singer prefixes

A line can also start with a singer name and a colon:

```
[12765,752]The (12765,487)Weeknd：(13252,265)
```

The prefix names the singer for this line and the lines after it, until a new prefix appears. It is not part of the lyrics.

## Parsing

### With @braccato/parsers (recommended)

[`@braccato/parsers`](https://docs.betterlyrics.org/braccato/parsers) reads the response as-is, envelope included, and handles the cases above:

```javascript
import { parseQRC } from "@braccato/parsers";

const response = await fetch(
  "https://api.betterlyrics.org/qq/getLyrics?s=Wanderlust&a=The+Weeknd&al=Kiss+Land+(Deluxe)&d=307"
);
const { lyrics: qrc } = await response.json();

const lyrics = parseQRC(qrc, 307 * 1000, { title: "Wanderlust", artist: "The Weeknd" });
```

-   Skips the header tags
-   Drops credit lines
-   With the `title` and `artist` metadata, drops opening lines that only repeat them
-   Turns singer prefixes into an `agent` (`v1`, `v2`, and `v1000` for a group) and removes them from the text
-   Adds instrumental lines for gaps longer than 5 seconds

Each line has `startTimeMs`, `durationMs`, `words` and a `parts` array with one entry per word. `QRCParser.parse(qrc, durationMs)` is the same without metadata, and `detectParser` selects `QRCParser` for a QQ response.

### Parsing by hand (JavaScript)

If you cannot add a dependency, this function extracts the lines and words. It does not remove credit lines or singer prefixes.

```javascript
function parseQRC(xml) {
  const match = xml.match(/LyricContent="([\s\S]*?)"\s*(?:\/?>|[a-zA-Z]+=)/);
  const body = (match ? match[1] : xml)
    .replace(/&quot;/g, '"')
    .replace(/&amp;/g, "&");

  const lines = [];

  for (const raw of body.split("\n")) {
    const line = raw.trim();
    const header = line.match(/^\[(\d+),(\d+)\]/);
    if (!header) continue;

    const rest = line.slice(header[0].length);
    const words = [];
    let textStart = 0;

    for (const token of rest.matchAll(/\((\d+),(\d+)\)/g)) {
      words.push({
        text: rest.slice(textStart, token.index),
        startMs: Number(token[1]),
        durationMs: Number(token[2]),
      });
      textStart = token.index + token[0].length;
    }

    lines.push({
      startMs: Number(header[1]),
      durationMs: Number(header[2]),
      text: words.map((word) => word.text).join(""),
      words,
    });
  }

  return lines;
}
```

The header check `^\[(\d+),(\d+)\]` only matches timed lines, so tags like `[ti:Wanderlust]` are skipped. The word text is read as the span between tokens rather than with a capture group, so a word that contains a bracket still parses.

Source: https://docs.betterlyrics.org/docs/qrc-format
