# 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-31**
- Permanent address of this file: `https://aimusiclist.com/developers/aimusiclist-rulebook.md`
- Checker: `https://aimusiclist.com/developers/check`

## 0. How to use this file

**For the person:** download this file, drop it into any AI chat (ChatGPT, Gemini, Claude… free versions are fine) and type **Start**. The AI asks you a few questions, then gives you every file and walks you through installing it, one step at a time. You do not need to know any coding.

**For the AI — read this whole section first and follow it exactly.** Words in `code` are exact names — never translate or rename them. Every MUST in this file is required.

### 0a. Talk to the person like this
- Answer in **the person's language**. Assume they have **never written code** and do not know what a terminal, a file path or JSON is.
- Explain only **what to click and what they will see**. Name buttons and menus with their **exact English screen names in bold** (for example **Create bucket**), add a few plain words around them, and say what appears next ("a green **Success** box appears"). Never ask the person to edit code; anything they must type (a password, their site name) goes into a clearly marked place you show them.
- Be warm and encouraging, short sentences, one action per step, numbered steps.

### 0b. First reply (when the person only sent this file or said "Start")
1. Greet them in one line and say you will build their own music site that shares songs with aimusiclist.com.
2. Ask which way they want, in plain words — and recommend **B**:
   - **A. Web hosting I already pay for** (cPanel with PHP) — songs stay on my hosting.
   - **B. Free Cloudflare account** (recommended) — free for about 1,000 songs and thousands of visitors a day, nothing to install on my computer.
   - **C. I already have a site** (static pages) and only want to add the music part.
3. Ask for: site name, artist name, the language their visitors mostly speak. (Optional: their email for notices.)
4. Stop and wait for the answer. Do not write code yet.

### 0c. Then build it (follow the matching request)
- **A** → Request A below. **B** → Request B below. **C** → Request C below. Use the names they gave you.
- **Simplest install path, no terminal:**
  - **A:** everything through the hosting control panel: **cPanel → File Manager** (**+ Folder**, **+ File**, right-click **Edit**, paste, **Save Changes**). Settings the person must fill in go in ONE small file you show them.
  - **B:** everything in the Cloudflare dashboard, in the browser: **R2 Object Storage → Create bucket**; **Workers & Pages → Create → Create Worker → Deploy → Edit code** (paste, **Deploy**); then the Worker's **Settings**: **Bindings → Add → R2 bucket** (variable name `BUCKET`), **Variables and Secrets → Add** (type **Secret** for the admin password), cron under **Trigger events**/**Triggers**. Keep the code in **one file `worker.js`** when you can; if it must be split, at most 3 files, and tell them exactly how to add each one in the editor. (Screen names can differ slightly; tell them what to look for.)
  - **C:** say which addresses go to the server part and give the same dashboard steps as B for the Worker.
- **Song length:** the admin page MUST read the length of each song by itself (in the browser, from the audio file) — never ask the person to type seconds.
- **site_secret** is created automatically on first run and never shown on any page.

### 0d. Sending the files — free chat versions cut long answers
1. First send the **list of files** you will give (name + one line each) and how many messages it will take.
2. Then send **one file per message**, always complete from the first line to the last. Never write "…", "rest unchanged" or "same as before". If a file would be very long, split it into smaller files instead.
3. End every file message with one line in the person's language: "Type **next** for the next file." If a message is cut off, the person types **again** and you resend that whole file.
4. After the last file, send the **install steps** (0c), then the **test steps** (0f).

### 0e. Before you send the last file — check your own work
Walk through this list in your head and fix anything missing:
- [ ] Opening the admin page → set or enter the password → **the admin page with the upload form actually appears** (every page you planned is really printed/returned; every function you call exists).
- [ ] Uploading an MP3 → the song appears in the list with its length → its checkbox "Also publish on aimusiclist.com" is on by default.
- [ ] After the first public song: automatic registration (section 5) runs, the `site_id` is saved, then catalog.json shows the song.
- [ ] Every address in section 2 works exactly: catalog.json (with ETag/304), verify, `/a/{id}` (Range → 206, CORS for aimusiclist.com, `OPTIONS`), and `/i`, `/l`, `/p` when you offer images, lyrics or a profile picture.
- [ ] A ping (section 7) is sent once after each change, not once per file.
- [ ] Each song has `langs` (section 3a): default to the visitors' language the person told you, and let them change it per song.
- [ ] No redirects on these addresses, no secret in pages or catalog.json.
- [ ] Every visitor page ends with the small link **`Powered by aimusiclist.com`** (section 2a): before registration it opens `https://aimusiclist.com/`, after registration `https://aimusiclist.com/artist/{site_id}`. There is no setting to turn it off.

### 0f. Test steps to give the person at the end
1. Open the admin page, upload one song, keep "Also publish on aimusiclist.com" checked.
2. Open **https://aimusiclist.com/developers/check**, type the site address, press **Check**.
3. Green = done: the song shows up on aimusiclist.com within a few minutes. Red or yellow lines = copy them, paste them back into this chat, and you fix them.

### Request A — PHP shared hosting (cPanel / Apache)
```text
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).
Every visitor page ends with the "Powered by aimusiclist.com" link of section 2a (no off switch).
Follow section 0 for how to talk to me, how to send the files and how to explain the install.
```

### Request B — Cloudflare Workers + R2 (free plan)
```text
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. Every visitor page ends with the
"Powered by aimusiclist.com" link of section 2a (no off switch). Follow section 0 for how to talk to me,
how to send the files and how to explain the setup in the Cloudflare dashboard.
```

### Request C — static site + small server
```text
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). Every page
ends with the "Powered by aimusiclist.com" link of section 2a. Songs are stored in [R2 / my storage]. Follow section 0 for how to talk to me, how to send the files and
how to explain the setup.
```

---

## 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.

### 2a. "Powered by aimusiclist.com" at the bottom of every page (MUST)

Every page of your site that **visitors** see MUST end with one small line that contains a link with exactly this text: **`Powered by aimusiclist.com`**. It is a condition for building a site with this Rulebook (the official aimusiclist personal-site software does the same).

- **Link address**: after registration (you have a `site_id`, section 5) → `https://aimusiclist.com/artist/{site_id}` (your artist page). Before registration → `https://aimusiclist.com/`. Same tab (no `target="_blank"`). If your artist page has no public songs yet, aimusiclist sends the visitor to its home page.
- **Where and how**: in the very last line of the page (the footer), in the same size and color as the other footer text. Small and quiet is fine.
- **No off switch**: do not add a setting, skin option or switch that removes it, and never hide it (`display:none`, `visibility:hidden`, `opacity:0`, text in the background color, pushed off screen…). Admin pages do not need it.
- Example:
  ```html
  <footer>© 2026 My Music · <a href="https://aimusiclist.com/artist/s_8f3k2j9d">Powered by aimusiclist.com</a> · <a href="/admin" rel="nofollow">Admin</a></footer>
  ```

---

## 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) |
| `thumb_lg_url` | string | optional | only `{site.url}/i/{id}?s=l` (large image 200–400 px square WebP, ≤ 60 KB), only when `thumb_url` is `{site.url}/i/{id}` (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` / `thumb_lg_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).
- **PHP edition update package** (added 2026-10-29, field addition only): when aimusiclist offers one, `personal_site` also has
  `"php":{"latest":"3.11.0","zip":"https://aimusiclist.com/dl/personal-site/aml-personal-site-php-3.11.0.zip","sha256":"<64 hex>"}`.
  `zip` always starts with `https://aimusiclist.com/dl/personal-site/`; check `sha256` before installing. The field is sent only when all three values are valid — if it is missing, there is no PHP package (do nothing). Ignore fields you don't know.

---

## 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.

### 5a. For hub operators only (hosting many artists' sites)

*Ordinary artists can skip this.* A **hub** is a service that runs and registers many personal sites for different artists (for example free hosting at `name.example.com`). aimusiclist's admin creates the hub and gives its operator a hub ID (`h_…`) and a hub key, once. There is no sign-up form.

- Call the same `POST /api/v1/sites/auto` for **each** artist's site, with two extra headers:
  ```
  X-AML-Hub: h_example
  X-AML-Hub-Key: hk_…            (keep it on the hub server only, never in a page or a catalog)
  ```
- **Verify is still done per site.** aimusiclist calls that site's own `/.well-known/aimusiclist/verify?c=…` exactly as in section 6, with that site's own `site_secret`. A hub key never lets you register an address you do not control.
- New hub sites count against the **hub's own hourly limit** (default 500 new sites per hour), not the site-wide limit for other artists. Over the limit: `429 err.rate_limited` with `Retry-After`. Re-registering an existing address does not count.
- If the key is wrong or missing, or the hub is suspended, the call is **not an error**: it is handled exactly like an ordinary registration (no hub label, site-wide hourly limit).
- Success adds `"hub":"h_example"` to the `201` answer (`null` for an ordinary registration).
- `contact_email` is the **artist's** email (block/offline notices go to the artist). The hub's own contact is kept separately by aimusiclist.
- On aimusiclist the artist page and artist cards show a small "via {hub name}". catalog.json, ping and pulling work exactly as for any other site. If aimusiclist suspends a hub, all its sites are hidden until the hub is restored.

---

## 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).

### 8a. Your song stats — `GET https://aimusiclist.com/api/v1/sites/{site_id}/stats?days=30`

For your own admin screen: how your public songs did on aimusiclist. Optional — nothing breaks if you never call it.

```text
Authorization: Bearer {site_secret}
If-None-Match: "ss-…"        (optional, the ETag you got last time)
```
- `days` = 1–90 (default 30). aimusiclist builds the data **once a day after UTC 00:10**, so call it **once a day** (a different minute for each site is kind), e.g. from your daily cron. Same ETag → `304` with an empty body.
- Answer `200`:

```json
{"schema":"site-stats-1","site_id":"s_8f3k2j9d","generated":"2026-10-30T00:14:02Z","through":"2026-10-29","chart_date":"2026-10-29",
 "totals":{"plays7":120,"valid7":88,"plays30":410,"valid30":300,"plays90":900,"valid90":650},
 "days":[{"d":"2026-10-29","plays":12,"valid":9,"likes":1,"adds":2}],
 "tracks":[{"id":"t1","plays7":40,"valid7":31,"likes7":2,"adds7":3,"likes":12,"chart_rank":12,"chart_rank_prev":15,"genre":"rnb","genre_rank":3,"genre_rank_prev":null,"all_rank":3251,"all_rank_prev":3371,"all_rank_best":812,"all_rank_move":"up"}],
 "countries":[["KR",30],["US",8]]}
```

| Field | Meaning |
|---|---|
| `through` | last full UTC day included; `days` runs oldest → newest and ends here (days with nothing = zeros) |
| `plays` | play starts on aimusiclist |
| `valid` | valid plays: listened 30 s and passed the duplicate/limit checks (the plays the chart can count) |
| `likes`, `adds` | new likes / playlist adds that day |
| `tracks[]` | every public song of your site, by `plays7` (last 7 days); `id` = your catalog `tracks[].id`; `likes` = total likes now |
| `chart_rank`, `chart_rank_prev` | stored rank in the latest **weekly Global** chart (`chart_date`) and the week before; `null` = not in the chart |
| `genre_rank`, `genre_rank_prev` | same for the weekly chart of the song's main genre |
| `all_rank`, `all_rank_prev`, `all_rank_best`, `all_rank_move` | rank among **all** songs with listeners in the **last 7 days** (a moving window, Global scoring; no top limit — 1–100 can differ from the weekly Top 100): now, at the previous update, best ever, and `up`·`down`·`same`·`new`·`re` (compared with the previous update). All `null` when nobody listened in the last 7 days |
| `countries` | listener countries of valid plays, last 30 days, top 20 (`XX` unknown) |

- Before the first daily build: `200` with `"generated":null` and empty lists — hide the box.
- New fields may be added; ignore ones you do not know.

Errors: `401 err.auth.invalid_secret` (missing/wrong secret), `404 err.site.not_found`, `403 err.site.suspended`, `429 err.rate_limited` + `Retry-After` (24 calls/day per site, `304` included; 60/min per IP).

### 8b. Closing your site — `POST https://aimusiclist.com/api/v1/sites/{site_id}/close`

Send this when the artist **deletes or closes** the site for good. aimusiclist then removes the site right away (instead of hiding it ~12 h later after failed pulls). Optional — old sites that never send it keep working.

```text
Authorization: Bearer {site_secret}
Content-Type: application/json
{"reason":"deleted"}          (optional: "deleted" | "closed")
```
- Result = the same as the owner pressing *Remove* on aimusiclist: the site becomes `removed`, all its songs and albums leave every list, the fetch queue is cleared. Play history and past charts stay.
- Answer `200 {"ok":true,"status":"removed"}` — also when the site is already removed, so sending it twice is safe.
- Coming back later: register again with `POST /sites/auto` (section 5). The same `site_id` is kept.
- Do **not** send it for a pause you want to undo (e.g. a hosting operator's temporary suspension) — publish an empty catalog and ping instead.
- If the call fails (network, `5xx`, `429`), retry later (e.g. from your cron) for up to a day. `404` from an old aimusiclist that does not know this address = give up quietly.

Errors: `401 err.auth.invalid_secret`, `404 err.site.not_found`, `403 err.site.suspended` (a site suspended by aimusiclist stays suspended), `429 err.rate_limited` + `Retry-After` (10/hour per site).

### 8c. Chart insights and news for your admin screen — `GET https://aimusiclist.com/feed/insights.json` · `/feed/news.json`

Two public files aimusiclist builds **once a day** right after the charts (about UTC 00:30). Show them in your *admin* screen so artists can study the chart without leaving their site. No key, no login.

- Fetch **once a day** from your cron (a different minute per site is best, e.g. minute = hash(site_id) mod 1440) with `If-None-Match: {last ETag}`. `304` = nothing new. Never fetch when a page opens.
- If the fetch fails, the JSON is invalid, too big (insights > 200 KB, news > 100 KB) or the `schema` is unknown, keep showing the last good copy.
- Headers: `Cache-Control: public, max-age=3600, s-maxage=86400`, `Access-Control-Allow-Origin: *`, `ETag`.
- `insights.json` (`"schema":"insights-1"`): `period{daily,weekly}`, `global.top` (weekly Global Top 100: `rank`, `prev`, `move` up|down|same|new|re, `listeners`, `countries`, `track`), `global.rising` (daily Rising chart top 20 with `jump` = places moved up), `global.new` (new in this week's Top 100), `genres.{main genre}.top` (20) / `.rising` (10), `countries[]` (`cc`, `listeners`, `top`), `threshold{top100_listeners,rank100_score}` (null under 100 songs), `totals{tracks,sites,countries,plays7}`.
  `track` = `{"id":"123","title","artist","site","site_url","page","genres":[…],"img"}` — `id` is aimusiclist's song number (not your catalog id); find *your* songs by `site_url`. There is **no audio address**: link to `page` (aimusiclist) or your own song page to play.
- `news.json` (`"schema":"news-1"`): up to 30 published items `{id, date, kind: report|notice, title{lang:…}, summary{lang:…}, url, chart?}`. Show title, summary and the graph; *More* opens `url`. Missing language → use `en`. `chart` = `{type: bar|line, labels[], series[{key,label{lang},values[]}], title{lang}}` drawn from real numbers.
- New fields may be added; ignore ones you do not know. Full format: aimusiclist `docs/spec/차트분석-소식-v1.md`.

---

## 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.
- Large image (optional): also serve the same picture at `/i/{id}?s=l` (square WebP, 400×400 recommended, 200–400 px, ≤ 60 KB; return 404 when there is none — never the small one) and set `thumb_lg_url` to that. aimusiclist downloads it only for songs in the top 3 of a chart (the big cards), keeps a copy, and re-checks it once a day with ETag. Without it, the small image is shown on a blurred backdrop.

## 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` |
| `GET /sites/{id}/stats` | per site 24/day (304 included) | `429` |
| `POST /sites/{id}/close` | per site 10/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.
20. No `Powered by aimusiclist.com` link at the bottom of the visitor pages, a switch to turn it off, or a link that still points to `https://aimusiclist.com/` after registration (section 2a).

## 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 |
| v1.3 (addition) | 2026-10-29 | Section 5a for hub operators: `X-AML-Hub` / `X-AML-Hub-Key` headers, hub hourly limit, wrong key = ordinary registration, verify still per site, `"hub"` in the answer. No catalog change; nothing changes for ordinary artists |
| v1.3 (addition 2) | 2026-10-29 | Section 3d: `versions.json` `personal_site.php` = `{"latest","zip","sha256"}` for the PHP edition's one-click update (zip only from `https://aimusiclist.com/dl/personal-site/`). Field addition only; omitted when not set |
| v1.3 (addition 3) | 2026-10-29 | Section 8a: `GET /api/v1/sites/{site_id}/stats` (your song stats, `site-stats-1`, Bearer secret, once a day, ETag/304, 24/day). New endpoint only; catalog unchanged |
| v1.3 (addition 4) | 2026-10-29 | Section 8b: `POST /api/v1/sites/{site_id}/close` (the site tells aimusiclist it was deleted/closed → removed at once, Bearer secret, safe to repeat, 10/hour). New endpoint only; catalog unchanged |
| v1.3 (addition 5) | 2026-10-30 | Section 0 rewritten for beginners: drop the file into an AI chat and type Start; the AI asks a few questions, sends one file per message (for free chat versions), checks its own work, and explains the install with exact screen names and no terminal (Cloudflare dashboard / cPanel File Manager). Song length is read automatically. No catalog or API change |
| v1.3 (addition 7) | 2026-10-30 | Section 4/10: optional `tracks[].thumb_lg_url` = `{site.url}/i/{id}?s=l` (large song image for the top-3 chart cards). Schema stays 1.3; old sites and old aimusiclist ignore it |
| v1.3 (addition 6) | 2026-10-30 | Section 8c: public daily files `/feed/insights.json` (`insights-1`, chart insights for your admin screen) and `/feed/news.json` (`news-1`, aimusiclist news with graph data). Once a day with ETag/304. New files only; catalog unchanged |
| v1.3 (addition 8) | 2026-10-31 | Section 2a (MUST): every visitor page ends with a small `Powered by aimusiclist.com` link → `https://aimusiclist.com/artist/{site_id}` after registration, `https://aimusiclist.com/` before. No off switch. Added to the requests in section 0 and to the self-check (0e). No catalog or API change |
