# aimusiclist Rulebook v1.3 — connect your own music site to aimusiclist.com

- Version: **Rulebook v1.3** (matches Integration Spec v1.3 = catalog 1.3, and Ranking v2.0) · Date: **2026-10-29**
- Permanent address of this file: `https://aimusiclist.com/developers/aimusiclist-rulebook.md`
  (Korean: `https://aimusiclist.com/developers/aimusiclist-rulebook.ko.md`)
- Checker: `https://aimusiclist.com/developers/check`

## 0. How to use this file

**Paste this whole file into your AI (ChatGPT, Claude, Gemini… free versions are fine), then send one of the requests below.**
The AI should follow every MUST in this file exactly. Words in `code` are exact names — do not translate or rename them.

### Request A — PHP shared hosting (cPanel / Apache)
```text
Above is the aimusiclist Rulebook. Build my music website in plain PHP 8 for shared hosting
(Apache + .htaccess, no framework, no database: store data in JSON files in a folder that is not
reachable from the web). Features: one admin password; upload MP3/M4A songs (max 15 MB);
edit title, artist and genres (keys from genres.json); a checkbox "Also publish on aimusiclist.com"
(checked by default); albums. Implement every address in section 2 exactly (catalog.json with
ETag, verify, /a/{id} with Range and CORS, /i, /l, /p), automatic registration (section 5) and
ping after every change (section 7). Give me every file with full code and step-by-step install
instructions for a beginner, then tell me how to test it with the aimusiclist checker.
```

### Request B — Cloudflare Workers + R2 (free plan)
```text
Above is the aimusiclist Rulebook. Build my music website as ONE Cloudflare Worker with an R2
bucket binding (free plan; keep requests and CPU low). Features: admin login; upload MP3/M4A
songs (max 15 MB) to R2; edit title, artist and genres (keys from genres.json); a checkbox
"Also publish on aimusiclist.com" (checked by default); albums. Generate site_secret on first run
and keep it in R2 (never in HTML/JS). Implement every address in section 2 exactly, automatic
registration (section 5), ping after every change (section 7) and a cron retry. Give me the full
code, wrangler.toml and beginner-friendly deploy steps, then how to test with the checker.
```

### Request C — static site + small server
```text
Above is the aimusiclist Rulebook. My pages are a static site (for example GitHub Pages or
Netlify). Keep my pages static, and add the smallest possible server part (one free Cloudflare
Worker on the SAME domain, or serverless functions with internal rewrites, never redirects) that
serves all addresses in section 2, keeps site_secret secret, registers automatically (section 5)
and sends ping (section 7). Songs are stored in [R2 / my storage]. Explain exactly which
addresses go to the server part, give full code and beginner steps, then how to test with the checker.
```

Tip: after the AI finishes, open `https://aimusiclist.com/developers/check`, enter your site address, and paste any red/yellow lines back to the AI.

---

## 1. How it works (big picture)

aimusiclist.com **never receives files**. It **pulls** a song list from your site. Your site only says "please pull again" (ping).

```text
 YOUR SITE (https://your.site)                         aimusiclist.com
 ─────────────────────────────                         ─────────────────────────────
 1. first run: make site_secret (64 hex), keep it secret
 2. POST /api/v1/sites/auto {url, name, …, site_secret} ──►
                                    ◄── GET /.well-known/aimusiclist/verify?c=CHALLENGE
 3. answer {"site_id":…, "proof": HMAC(site_secret, "verify:"+CHALLENGE)} ──►
                                    ◄── 201 {"ok":true,"site_id":"s_xxxxxxxx"}
 4. save site_id, put it in catalog.json
                                    ◄── GET /.well-known/aimusiclist/catalog.json (≈3 s later)
 5. on every change: POST /api/v1/ping (Bearer site_secret) ──►
                                    ◄── GET catalog.json (queued; staggered within every 6 hours)
 6. listeners on aimusiclist.com play  ◄── browser GET /a/{id} (Range, CORS)
                                           browser GET /i/{id} image, /l/{id} lyrics
```

- A song is public on aimusiclist **only while it is in your catalog.json**. Remove it from the list = private.
- Your own site can show every song you have. Only songs with "Also publish on aimusiclist.com" go into catalog.json.
- Owner check = "can you run code on that address?" No account, no manual approval.

---

## 2. Addresses your site MUST serve

All on your **site origin** (e.g. `https://music.example.com`, no path). HTTPS only. **No redirects** (aimusiclist does not follow 301/302). No login, no bot challenge on these paths.

| Address | Method | Required | What to return |
|---|---|---|---|
| `/.well-known/aimusiclist/catalog.json` | GET | MUST | catalog JSON (section 3). `Content-Type: application/json`. ETag + `304` on `If-None-Match` SHOULD |
| `/.well-known/aimusiclist/verify?c={challenge}` | GET | MUST | `{"site_id": "s_…" or null, "proof": "64 lowercase hex"}` (section 6) |
| `/a/{track_id}` | GET, HEAD, OPTIONS | MUST | the audio file. Range support (`206`), `Accept-Ranges: bytes`, `Cache-Control: no-cache`, CORS (below) |
| `/i/{track_id}` | GET | optional | song image, 100×100 WebP (only if you use your own image) |
| `/l/{track_id}` | GET | optional | lyrics, `.srt` text, `text/plain; charset=utf-8`, CORS (below) |
| `/c/{album_id}` | GET | optional | album cover, 200×200 WebP |
| `/p` | GET | optional | profile image, 200×200 WebP, ≤ 100 KB (aimusiclist downloads it as `/p?v={hash}`) |

**Timing**: catalog must answer within **3 s** (ping) / **10 s** (scheduled); verify within **5 s**. Max response **1 MB** (catalog itself ≤ 512 KB).

### CORS (MUST for `/a/` and `/l/`)
Listeners' browsers on aimusiclist.com load `/a/` with `crossOrigin` (for the visualizer) and `/l/` with `fetch()`. When the request header `Origin` is `https://aimusiclist.com` or `https://www.aimusiclist.com` **and the song is in your catalog**, send:

```text
Access-Control-Allow-Origin: https://aimusiclist.com        (copy the request Origin)
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Allow-Headers: Range, If-None-Match
Access-Control-Expose-Headers: Content-Length, Content-Range, Accept-Ranges
Vary: Origin
```
Answer `OPTIONS` with `204` + the same headers. For songs **not** in the catalog, send no CORS headers (your own site still plays them).
Without CORS on `/a/`, aimusiclist still plays the song but without the real visualizer; without CORS on `/l/` lyrics do not open.

---

## 3. catalog.json (schema 1.3)

Top level — all MUST:

| Field | Type | Rule |
|---|---|---|
| `schema` | string | `"1.3"` (aimusiclist accepts `1.0`, `1.1`, `1.2`, `1.3` and any later `1.x`; `2.0` is refused) |
| `generated_at` | string | ISO 8601 UTC, e.g. `"2026-10-28T04:23:21Z"` |
| `site` | object | see below |
| `albums` | array | `[]` if none |
| `tracks` | array | `[]` if none (an empty valid list = "everything private") |

**site**

| Field | Type | Req. | Rule |
|---|---|---|---|
| `site_id` | string | MUST | the id you got from registration (`s_` + 8 chars). Wrong → whole list refused |
| `url` | string | MUST | exactly your registered origin, e.g. `https://music.example.com`. Wrong → whole list refused |
| `name` | string | MUST | site name, 1–40 chars |
| `artist_name` | string | MUST | main artist, 1–40 chars |
| `default_lang` | string | MUST | `en`, `ko`, `ja`, `pt-BR` … |
| `description` | string | optional | ≤ 200 chars |
| `profile_url` | string | optional | only `{url}/p?v={8 lowercase hex}` (v = hash of the image; change it when the image changes) |
| `socials` | string[] | optional (1.2) | the artist's social network **keys** only, 0–10, e.g. `["youtube","instagram","soundcloud"]` — **never addresses** (section 3b) |
| `ext` | object | optional (1.3) | site extension fields from fields.json with `scope: "site"` (section 3c), ≤ 2 KB |

**tracks[]**

| Field | Type | Req. | Rule |
|---|---|---|---|
| `id` | string | MUST | `[a-z0-9_-]`, 1–64 chars, unique in your site, **never changes** |
| `title` | string | MUST | 1–100 chars |
| `artist` | string | MUST | 1–60 chars (usually the site name, or a virtual artist name) |
| `genres` | string[] | MUST | 1–3 genre **keys**; first = main genre (section 4) |
| `duration` | integer | MUST | seconds, **integer** 30–900 (not `221.4`, not `"221"`) |
| `audio_url` | string | MUST | exactly `{site.url}/a/{id}` |
| `published_at` | string | MUST | ISO 8601, e.g. `"2026-10-28T02:13:57Z"` (display only) |
| `thumb_url` | string | optional | only `{site.url}/i/{id}` or `https://aimusiclist.com/img/default/{5 digits}.webp` (section 10) |
| `lyrics_url` | string | optional | only `{site.url}/l/{id}`, only if lyrics exist |
| `langs` | string[] | optional (1.2) | lyric language(s) of the song, 1–2 codes, first = main (section 3a). Instrumental = `["zxx"]` alone |
| `ext` | object | optional (1.3) | extension fields with `scope: "track"`, e.g. `{"mood":["calm","nostalgic"]}` (section 3c), ≤ 1 KB |

**albums[]** (section 9)

| Field | Type | Req. | Rule |
|---|---|---|---|
| `id` | string | MUST | same rule as track id |
| `title` | string | MUST | 1–100 chars |
| `artist` | string | MUST | 1–60 chars |
| `cover_url` | string/null | MUST | `{site.url}/c/{id}` or `null` |
| `track_ids` | string[] | MUST | 10–30 different track ids, all present in `tracks` |
| `published_at` | string | MUST | ISO 8601 |

**Limits**: file ≤ 512 KB, ≤ 1,000 tracks, ≤ 100 albums. Unknown fields are ignored (safe to add your own — e.g. a `local` object for your own site-only fields is ignored; do not put those in `ext`).

**What aimusiclist does with mistakes**

| Mistake | Result |
|---|---|
| broken JSON, `schema` not 1.x, `site.site_id`/`site.url` wrong, too large | whole list refused, previous state kept, counts as a failed fetch |
| one track wrong (id, title, artist, main genre, duration, audio_url, published_at, duplicate id) | only that track refused |
| wrong `thumb_url` / `lyrics_url` / `profile_url` | only that field ignored |
| (1.2) unknown code in `langs`, unknown key in `socials`, more than 10 socials | only that value ignored; no valid `langs` → the song language is `site.default_lang` (part before `-`, e.g. `pt-BR` → `pt`) |
| (1.3) `ext` key not in fields.json, wrong type, unknown option, too many options, over 1 KB | only that value (over 1 KB: the whole `ext`) ignored — the song is kept |
| unknown or duplicate secondary genre | only that genre ignored |
| album wrong (count, missing track, fields) | only that album refused |
| wrong album `cover_url` | only the cover ignored |
| bad `site.name` / `artist_name` / `default_lang` | previous value kept |

### Minimal example (1 song)
```json
{
  "schema": "1.3",
  "generated_at": "2026-10-28T04:23:21Z",
  "site": { "site_id": "s_ez57qni8", "url": "https://music.example.com",
            "name": "Hip2pm", "artist_name": "Hip2pm", "default_lang": "en" },
  "albums": [],
  "tracks": [
    { "id": "t_ynyd78co", "title": "Where Are You Going", "artist": "Hip2pm",
      "genres": ["rnb"], "duration": 221,
      "audio_url": "https://music.example.com/a/t_ynyd78co",
      "published_at": "2026-10-28T02:13:57Z" }
  ]
}
```

### Full example (all optional fields, album shortened)
```json
{
  "schema": "1.3",
  "generated_at": "2026-10-28T04:23:21Z",
  "site": {
    "site_id": "s_ez57qni8",
    "url": "https://music.example.com",
    "name": "Hip2pm",
    "artist_name": "Hip2pm",
    "default_lang": "en",
    "description": "Late-night R&B made with AI.",
    "profile_url": "https://music.example.com/p?v=9f2c01ab",
    "socials": ["youtube", "instagram", "soundcloud"]
  },
  "albums": [
    { "id": "a_night", "title": "Night Drive", "artist": "Hip2pm",
      "cover_url": "https://music.example.com/c/a_night",
      "track_ids": ["t_01","t_02","t_03","t_04","t_05","t_06","t_07","t_08","t_09","t_10"],
      "published_at": "2026-10-20T00:00:00Z" }
  ],
  "tracks": [
    { "id": "t_01", "title": "Where Are You Going", "artist": "Hip2pm",
      "genres": ["rnb", "hiphop", "dance"], "duration": 221,
      "audio_url": "https://music.example.com/a/t_01",
      "published_at": "2026-10-28T02:13:57Z",
      "thumb_url": "https://music.example.com/i/t_01",
      "lyrics_url": "https://music.example.com/l/t_01",
      "langs": ["ko", "en"],
      "ext": { "mood": ["calm", "nostalgic"] } },
    { "id": "t_02", "title": "Blue Hour", "artist": "Neon Ghost",
      "genres": ["rnb", "neo_soul"], "duration": 198,
      "audio_url": "https://music.example.com/a/t_02",
      "published_at": "2026-10-27T11:00:00Z",
      "thumb_url": "https://aimusiclist.com/img/default/04213.webp",
      "langs": ["zxx"] }
  ]
}
```
(The album above needs all 10 tracks `t_01`…`t_10` in `tracks`; only 2 are shown here.)
Display on aimusiclist: under the title it shows `Hip2pm` (artist = site name, case-insensitive) or `Hip2pm > Neon Ghost` (different artist). Clicking the site name opens your site.
On the right of each song row aimusiclist shows: [social icons] → [SRT, only with lyrics] → [♥ likes] → [+ add]. With the socials above: YouTube, Instagram and one 🔗 (for SoundCloud).

### 3a. Song languages `langs` (catalog 1.2)

- The **lyric language** of the song, not your site's interface language. 1–2 codes; the first is the main language. Language charts and the language filter use the main language.
- Codes: `GET https://aimusiclist.com/api/v1/song-langs.json` (public, CORS `*`, ETag, cache 1 day). Today:
  `en ko ja zh yue es pt id hi th de ru fr vi tr it ar fil ms nl pl sv uk he fa bn ur ta te pa mr sw el cs ro hu fi da no km my mn kk zxx mis`
  (`zxx` = instrumental / no lyrics — only alone; `mis` = another language). Codes have **no region**: `pt`, not `pt-BR`; `zh`, not `zh-TW`.
- Show language names as written in the language itself (the list gives them: `한국어`, `日本語`, `ไทย`…). Only `zxx` and `mis` have no name (`"key":"songlang.zxx"`): translate them in your own language pack.
- Default when the artist chooses nothing: your site's default language (part before `-`). If you don't send `langs`, aimusiclist does the same.
- Listeners can report "wrong language". An aimusiclist admin may correct a song's language; the corrected value wins until the admin unlocks it. Sites that keep mislabeling may be excluded from language charts.

### 3b. Social links `site.socials` (catalog 1.2)

- aimusiclist receives **only the kind (key)** of each social network, never the address. Icons on aimusiclist open **your site's** links page: `{site.url}/#/links/{key}` (and `{site.url}/#/links/more` for the 🔗), so your site MUST show those links there.
- Keys — the big five (shown as icons on aimusiclist, always in this order): `youtube instagram tiktok facebook x`.
  Others (shown together as one 🔗): `threads soundcloud spotify applemusic bandcamp suno udio discord twitch line whatsapp telegram kakao bilibili patreon bluesky vk weibo`.
- Order in `socials`: the big five you use first, then others. Max 10. Unknown keys are ignored.
- Your own admin screen should only accept `https://` addresses on the official domains of each network (e.g. `youtube.com`, `youtu.be` for YouTube).

### 3c. Extension fields `ext` and `fields.json` (catalog 1.3)

New optional song fields (for example **mood**) are announced in one file, so sites don't need new code for each one:

- `GET https://aimusiclist.com/api/v1/fields.json` (public, CORS `*`, ETag, `Cache-Control: public, max-age=3600`, ≤ 64 KB, data only):
  ```json
  {"schema":"fields-1","version":1,"updated_at":"2026-10-28T00:00:00Z","fields":[
    {"key":"mood","scope":"track","type":"multiselect","max_select":2,"required":false,
     "label":{"en":"Mood","ko":"분위기"},"help":{"en":"Up to 2","ko":"최대 2개"},
     "options":[{"key":"happy","label":{"en":"Happy","ko":"신나는"}},{"key":"calm","label":{"en":"Calm","ko":"잔잔한"}}],
     "display":{"track_row":true,"detail":true,"filter":true,"order":10},"since":1,"status":"active"}]}
  ```
- Types: `text` (string, ≤ `max_length` ≤ 200) · `number` (`min`/`max`) · `bool` · `select` (one option key) · `multiselect` (array of option keys, ≤ `max_select` ≤ 5). `scope` `track` → `tracks[].ext`, `site` → `site.ext`.
- Labels: use your interface language; missing language → `en`. Always escape the text (it is data, never HTML).
- Read it **once a day** at a time spread by your site (e.g. minute = hash(site_id) mod 1440), with `If-None-Match`. On failure, invalid JSON, > 64 KB or an unknown `schema` (e.g. `fields-2`), keep using your last good copy. Unknown `type` → skip only that field and show "This field needs a program update".
- New fields are always optional. `status: "deprecated"` = hide the input but keep sending existing values. Keys and option keys are never renamed or reused.
- Your site's own private fields go to a separate `local` object that is **not** sent in catalog.json.
- Values: send only what the artist filled in. aimusiclist validates every value against fields.json and drops only wrong values.
- aimusiclist shows `display.track_row` fields as gray text next to the genre, and `display.filter` fields as filter chips. **Extension fields never change chart scores.**
- **Boundary**: sites using the common input engine (the aimusiclist personal-site code: Workers, PHP, MeshMelo hosting) get new fields automatically. A **hand-made upload screen** (your own PHP, WordPress…) keeps working with aimusiclist, but new fields appear only when its developer adds code that reads fields.json. We recommend the common input engine.

### 3d. New versions of the personal-site program `versions.json`

- `GET https://aimusiclist.com/api/v1/versions.json` (public, CORS `*`, cache 1 day):
  `{"schema":"versions-1","personal_site":{"latest":"3.8.0","min_supported":"3.5.0","update_url":"https://…","notes_url":null}}`
- If your installed version is lower than `latest`, show one line "A new version is available" in your admin screen (never forced).

---

## 4. Genres

- List: `GET https://aimusiclist.com/api/v1/genres.json` (public, CORS `*`, cache 1 day).
  ```json
  {"schema":"1.0","rules":{"max_genres":3,"first_must_be_main":true},
   "genres":[{"key":"rnb","names":{"en":"R&B","ko":"알앤비"},
              "subs":[{"key":"neo_soul","names":{"en":"Neo soul","ko":"네오 소울"}}]}]}
  ```
- Put **keys** in `genres` (`"rnb"`), never display names (`"R&B"`).
- `genres[0]` MUST be a **main** key (a top-level `key`). A sub key there (e.g. `"kpop"`) → song refused.
- `genres[1]`, `genres[2]` (optional) = any main or sub key. No duplicates. Max 3 in total.
- Main keys today: `pop rnb hiphop rock electronic dance ballad jazz classical folk latin soundtrack ambient world`. Always read genres.json for the current list (keys are never renamed or removed).
- Config files you read from aimusiclist (`genres.json`, `song-langs.json`, `fields.json`, `default-images.json`, `versions.json`): read at most once a day, use `If-None-Match`, and keep your last copy if aimusiclist is unreachable.

---

## 5. Automatic registration — `POST https://aimusiclist.com/api/v1/sites/auto`

No account, no login. Send it the first time a song is published (or right after setup). Wait up to 20 s for the answer: during your request aimusiclist calls your verify address, so **your server must be able to answer another request at the same time**.

Request (`Content-Type: application/json`):
```json
{ "url": "https://music.example.com", "name": "Hip2pm", "artist_name": "Hip2pm",
  "contact_email": "owner@example.com", "default_lang": "en",
  "site_secret": "64 lowercase hex characters" }
```

| Field | Rule |
|---|---|
| `url` | `https://` + host only. No path, no query, no port, no IP address, ≤ 200 chars. One registration per address |
| `name`, `artist_name` | 1–40 chars |
| `contact_email` | optional but recommended. Never shown publicly; used only for block/offline notices |
| `default_lang` | e.g. `en` (default `en`) |
| `site_secret` | 64 hex chars, made once with a secure random generator (PHP `bin2hex(random_bytes(32))`, JS `crypto.getRandomValues`). Keep it on the server only |

Success: `201 {"ok":true,"site_id":"s_ez57qni8","renewed":false,"status":"active"}` → **save `site_id` immediately** and use it in catalog.json. aimusiclist pulls your catalog about 3 s later (at the latest within ~1–2 min).

**Re-register** (reinstall, lost secret): just make a new secret and call again with the same `url`. If verify passes, aimusiclist keeps the same `site_id`, replaces the secret and answers `201` with `"renewed": true`.

Errors (body: `{"ok":false,"error":{"code":"…","params":{…}}}`):

| HTTP | code | Meaning / what to do |
|---|---|---|
| 400 | `err.site.url_invalid` | url format wrong (http, path, port, IP…) |
| 400 | `err.validation` | a field is wrong; `params.field` tells which |
| 429 | `err.rate_limited` | same url within 10 min (failed tries count too), or too many new sites this hour. Wait `params.retry_after` s (`Retry-After` header) |
| 403 | `err.site.suspended` | blocked by an admin; re-registering does not unblock |
| 422 | `err.site.verify_failed` | proof did not match (section 6) |
| 422 | `err.catalog.too_large` | verify answer larger than 1 MB |
| 503 | `err.site.offline` | could not reach verify (timeout, DNS, TLS, redirect, HTTP error) |
| 503 | your own code | if verify answered `{"ok":false,"error":{"code":"err.site.not_configured"}}` we pass that code back |

Retry policy for your site: on failure try again every 10 minutes, at most 20 times a day.

---

## 6. Verify (owner check) — `GET /.well-known/aimusiclist/verify?c={challenge}`

- `c` = random challenge from aimusiclist (today 32 hex chars; accept `[A-Za-z0-9_-]{8,128}`).
- Answer `200`, `Content-Type: application/json`, `Cache-Control: no-store`:
  `{"site_id": "<your site_id or null before the first registration>", "proof": "<hex>"}`
- **proof = HMAC-SHA256(key = site_secret, message = "verify:" + c)**, lowercase hex (64 chars).
- The key is the 64-character secret **as text** (UTF-8 bytes of the string). Do **not** hex-decode it.
- If the secret does not exist yet: answer `503 {"ok":false,"error":{"code":"err.site.not_configured"}}`.

PHP:
```php
<?php
$c = $_GET['c'] ?? '';
if (!preg_match('/^[A-Za-z0-9_-]{8,128}$/', $c)) { http_response_code(400); exit; }
header('Content-Type: application/json');
header('Cache-Control: no-store');
echo json_encode(['site_id' => $siteId ?: null,                 // from your saved data
                  'proof'   => hash_hmac('sha256', 'verify:' . $c, $siteSecret)]);
```

JavaScript (Cloudflare Workers, Deno, Node 18+):
```js
async function hmacHex(secret, message) {
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey('raw', enc.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
  const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message));
  return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, '0')).join('');
}
// const proof = await hmacHex(siteSecret, 'verify:' + c);
// return Response.json({ site_id: siteId || null, proof }, { headers: { 'cache-control': 'no-store' } });
```
Test vector: secret = `"0123456789abcdef"` repeated 4 times (64 chars), c = `"abc123xyz0000000"` →
proof = `a7ebecd185ed00ae76912b177af4ba1dc0f3a67f57d546bbd473831840162f6c`

---

## 7. Ping — `POST https://aimusiclist.com/api/v1/ping`

Send after **every** change that changes catalog.json (upload, edit, publish/unpublish, delete). Uploading many files: send **one** ping at the end.

```text
POST /api/v1/ping
Authorization: Bearer {site_secret}
Content-Type: application/json

{"site_id":"s_ez57qni8"}
```

| HTTP | Body | Meaning |
|---|---|---|
| 200 | `{"ok":true,"status":"updated","tracks":24}` / `"not_modified"` | pulled right away |
| 202 | `{"ok":true,"status":"queued"}` (+ `error`) | will retry within minutes |
| 401 | `err.auth.invalid_secret` | wrong secret → re-register (section 5) |
| 404 | `err.site.not_found` | unknown site_id → re-register |
| 403 | `err.site.suspended` | blocked |
| 429 | `err.rate_limited` | more than 6/min or 60/hour for your site |

If a ping fails, try again after 10 s, then once more about a minute later. Ping queues a fetch and returns HTTP 202 without contacting your server in the ping request. Regular pulls use a deterministic per-site slot distributed across each six-hour window, rather than four fixed clock times, with `If-None-Match`.

---

## 8. Changing your address (own domain) — `POST https://aimusiclist.com/api/v1/sites/{site_id}/url`

```text
Authorization: Bearer {site_secret}
{"url":"https://mymusic.com"}
```
1. The new domain must already serve verify (section 6) with the **same secret**.
2. aimusiclist checks verify on the new address. OK → `200 {"ok":true,"site_id":"…","url":"https://mymusic.com","changed":true}` and pulls the catalog from the new address.
3. From then on, catalog `site.url` and every `*_url` MUST use the new address.
4. Keep the old address answering `/.well-known/`, `/a/`, `/c/`, `/i/`, `/l/` (saved playlists still point there). You may 301-redirect only visitor pages.

Errors: `401`, `404`, `400 err.site.url_invalid`, `409 err.site.url_taken` (another site uses it), `422 err.site.verify_failed`, `503`, `403 err.site.suspended`, `429` (5 changes/hour).

---

## 9. Albums

- Your site may have albums of any size, but an album goes into catalog.json **only with 10–30 tracks**.
- Every id in `track_ids` MUST also be in `tracks` (= publishing an album publishes its songs).
- If aimusiclist refuses some of its songs and fewer than 10 remain, the album is hidden.
- The aimusiclist front page shows new albums with 10+ songs; playlists also need 10+ songs to be featured.

## 10. Default song images

- aimusiclist hosts 9,963 free song images (100×100 WebP): `https://aimusiclist.com/img/default/00001.webp` … `09963.webp`.
- `GET https://aimusiclist.com/api/v1/default-images.json` → `{"count":9963,"base":"https://aimusiclist.com/img/default/","ext":".webp"}`
- When a song is uploaded, pick a random number 1…count, zero-pad to 5 digits and store `thumb_url` = base + number + ext. If the request fails, use count 9963. Keep the same number forever for that song.
- No `thumb_url` → aimusiclist picks a default image by its own song number.
- Your own image: serve it at `/i/{id}` (100×100 WebP, ≤ 50 KB) and set `thumb_url` to that.

## 11. Audio and files

| Item | Rule |
|---|---|
| Audio | MP3 (`audio/mpeg`) or M4A/AAC (`audio/mp4`), ≤ 15 MB, 30 s – 15 min |
| `duration` | must match the real length (integer seconds) |
| Song image | 100×100 WebP, ≤ 50 KB |
| Album cover | 200×200 WebP, ≤ 100 KB |
| Profile | 200×200 WebP, ≤ 100 KB; must be real WebP or it is ignored |
| Lyrics | `.srt`, UTF-8, ≤ 100 KB |
| Public songs | ≤ 1,000 per site |

## 12. Limits and safety

| What | Limit | Over the limit |
|---|---|---|
| `POST /sites/auto` | same url once per 10 min; all of aimusiclist ≤ 200 new sites/hour | `429` |
| `POST /ping` | per site 6/min, 60/hour | `429` |
| `POST /sites/{id}/url` | per site 5/hour | `429` |
| aimusiclist → your site | at most once per 10 s per site | waits in a queue |
| public GET API | 60/min per IP | `429` |
| checker | 10 per 10 min per IP | `429` |

How aimusiclist fetches your site: HTTPS only, valid certificate, no redirects, public IP only, ≤ 1 MB, timeouts 3/5/10 s, `User-Agent: aimusiclist-fetch/1.0 (+https://aimusiclist.com)`. Do not block that agent (turn off bot challenges for `/.well-known/aimusiclist/*`).

**Offline**: each scheduled pull that fails is retried after 5 and 30 minutes. After 3 failed rounds in a row (~12 h) your site is `hidden` (songs disappear); the next success shows it again. After 30 days of failures we email `contact_email`.

## 13. Rights, reports and blocking

- Publish **only songs you have the right to distribute** (you made them, and your AI tool's terms allow public distribution). No covers or uploads of other people's songs.
- Signed-in aimusiclist members can report a song. 3 different reporters (accounts older than 24 h) hide it until an admin decides (within 3 days).
- Blocked songs are ignored in all future pulls. 3 confirmed stolen songs → the whole site is `suspended`.
- Junk sites (broken files, silence, noise, not music, spam, misleading metadata) can be `suspended` immediately.
- A suspended site cannot unblock itself by re-registering. Appeal by replying to the email sent to `contact_email`.

## 14. How the chart works (summary of Ranking v2.0)

- Only plays **on aimusiclist.com** count. Plays on your own site never count. Likes (♥) and extension fields **never** count.
- One valid play = **30 s of real listening** (seeking does not count), checked with a server-signed play token.
- We count **different listeners**, not repeat plays: the same listener (member or device) counts **once per song per 24 h**, up to 50 songs a day; in a weekly chart one listener adds at most **3 days** to one song. The artist is a listener like anyone else.
- Weights: verified-email members 1.0, other members and guests 0.5, accounts younger than 24 h ×0.5, plays from unknown locations ×0.5; member playlist adds 2.0 (once per member per song, ever).
- **Countries are balanced** (Penrose square-root method): each listener belongs to one home country per period; the global score adds each country's score divided by the square root of that country's active listeners, so a country with 4× more listeners has 2× the influence. Countries with very few listeners are grouped together.
- Charts: **Global Top 100**, **country Top 100** (when that country has enough listeners), **language Top 100** (main song language), **genre Top 100** (shown on each genre page, "Popular" is the default sort), **Rising 50** (songs first seen within 28 days).
- Periods (UTC): **weekly = Friday 00:00 – Thursday 24:00** (main chart, final every Friday 00:30 UTC = 09:30 KST), daily = one UTC day, computed every day at 00:30 UTC. No real-time chart.
- A song needs a minimum number of different listeners to enter (number not published). Long runs step down: after 20 weeks below No. 50, 26 weeks below 25, 52 weeks below 10, 78 weeks below 5 (weekly Global, country and genre charts).
- Movement next to each rank: ▲n / ▼n / – / `NEW` (first time in this chart) / `RE` (back after leaving). Every chart song shows how many listeners and countries it had.
- Suspicious songs are **held** and reviewed: plays concentrated on very few networks, many brand-new accounts, listeners who play nothing else, or a sudden surge from one unrelated country.
- Only songs still in your catalog from `active` sites are shown (removing a song from the catalog removes it from the charts right away).
- **Do not manipulate** (bots, play farms, many fake accounts, paid plays, autoplay loops, VPN country hopping). Manipulated songs are held, and sites can be suspended.
- Full method: `https://aimusiclist.com/charts/methodology` · rules (Korean original): `https://aimusiclist.com/developers/aimusiclist-ranking.ko.md`

## 15. Common mistakes

1. HMAC key hex-decoded, or message without `verify:` → `err.site.verify_failed`. Use the secret string as is.
2. Redirects anywhere in the chain (http→https, `www`↔no-`www`, trailing slash, `/catalog.json` → `/catalog.json/`) → offline. aimusiclist never follows redirects.
3. `site.url` not exactly the registered origin (`www`, path, `http`, port) → whole list refused.
4. `site_id` still `null` or old after registration → whole list refused. Save the id from the `201` answer first.
5. `audio_url` pointing to R2/CDN/S3 or with `?query` → song refused. It must be `{site.url}/a/{id}`.
6. `duration` as decimal or text → song refused. Round to an integer.
7. Display name (`"R&B"`) or sub-genre (`"kpop"`) as first genre → song refused.
8. Albums with fewer than 10 or more than 30 songs, or ids that are not in `tracks` → album refused.
9. No CORS on `/a/` (no visualizer) or `/l/` (lyrics do not open). Remember `OPTIONS`.
10. catalog.json served as an HTML page (SPA fallback, 404 page, login page, bot challenge) → refused.
11. Secret visible in HTML/JS, in catalog.json or in a public Git repo. Anyone with it can ping and change your address.
12. Ping for every single file during a batch upload, or in a loop → `429`.
13. Changing song ids (e.g. from the file name) → treated as new songs; chart history is lost.
14. Private songs left in catalog.json — the catalog decides what is public.
15. A server that cannot answer verify while it waits for `/sites/auto` (single-thread dev server) → timeout.
16. (1.2) Social **addresses** in `site.socials` (`"https://youtube.com/@me"`) → ignored. Send keys (`"youtube"`) and show the addresses on your `#/links` page.
17. (1.2) Interface language codes in `langs` (`"pt-BR"`, `"zh-TW"`) → ignored. Use `pt`, `zh`. `zxx` together with another code → `zxx` dropped.
18. (1.3) Made-up `ext` keys or option keys, or your private fields inside `ext` → ignored. Private fields go to `local` (not sent).
19. (1.3) Reading fields.json on every page view or every minute — read once a day and cache it.

## 16. Check it yourself

- Web: **`https://aimusiclist.com/developers/check`** — enter your site address. It pulls your catalog with the same checks as the real import, tests verify (format only, no secret needed) and one audio file (HEAD + CORS). Nothing is saved and nothing is registered.
- API (same result as JSON):
  ```bash
  curl -s -X POST https://aimusiclist.com/api/v1/tools/check-catalog \
    -H 'Content-Type: application/json' \
    -d '{"url":"https://music.example.com","verify":true}'
  ```
  Answer: `{"ok":true,"result":"pass|warn|error","summary":{"pass":n,"warn":n,"error":n},"checks":[{"status":"error","code":"chk.track.duration","where":{"kind":"track","id":"t_01","field":"duration"},"params":{"value":"221.4"}}, …]}`. Each `code` is explained (with how to fix it) on the web page.
- Test your HMAC with the test vector in section 6.
- Your own catalog: `curl -i https://your.site/.well-known/aimusiclist/catalog.json` must start with `HTTP/… 200` and `content-type: application/json`.

## 17. Changelog

| Version | Date | Changes |
|---|---|---|
| v1.1 | 2026-10-28 | First public Rulebook. Based on Integration Spec v1.1 as implemented on aimusiclist.com (automatic registration, verify `?c=`, catalog 1.1 with `thumb_url`/`lyrics_url`/`profile_url`, address change, default images) and Ranking v1.2 |
| v1.2 | 2026-10-29 | catalog 1.2: `tracks[].langs` (song languages, `song-langs.json`) and `site.socials` (social network keys only, links page `#/links/{key}`). Ranking v2.0 summary (country balance, country/language/genre/Rising charts, UTC Friday–Thursday weeks, NEW/RE) |
| v1.3 | 2026-10-29 | catalog 1.3: `tracks[].ext` / `site.ext` with `fields.json` (first field: `mood`), `local` is never sent, `versions.json`, boundary for hand-made upload screens. Same permanent address |
