# aimusiclist 규약집 v1.3 — 내가 만든 음악 사이트를 aimusiclist.com에 연결하기

- 버전: **Rulebook v1.3** (연동규약 v1.3 = catalog 1.3, 순위 산정 방식 v2.0 기준) · 날짜: **2026-10-31**
- 이 파일의 고정 주소: `https://aimusiclist.com/developers/aimusiclist-rulebook.ko.md`
  (영어 원본: `https://aimusiclist.com/developers/aimusiclist-rulebook.md` — 내용이 다르면 영어 원본이 기준)
- 검사 도구: `https://aimusiclist.com/developers/check`

## 0. 이 파일 쓰는 법

**이 파일 전체를 AI(ChatGPT, Claude, Gemini… 무료 버전도 됨)에게 붙여넣고, 아래 요청문 중 하나를 보내세요.**
AI는 이 파일의 "반드시(MUST)"를 그대로 지켜야 합니다. `코드 글꼴`로 쓴 이름은 정확한 이름이므로 번역하거나 바꾸면 안 됩니다.

### 요청문 A — PHP 웹호스팅 (cPanel / Apache)
```text
위 내용은 aimusiclist 규약집입니다. PHP 8로 내 음악 사이트를 만들어 주세요. 웹호스팅(Apache +
.htaccess)용이고, 프레임워크와 데이터베이스는 쓰지 말고 데이터는 웹에서 열 수 없는 폴더의 JSON
파일에 저장해 주세요. 기능: 관리자 비밀번호 1개, MP3/M4A 곡 올리기(최대 15MB), 제목·아티스트·장르
(genres.json의 키) 수정, "aimusiclist.com에도 공개" 체크(기본 체크됨), 앨범. 규약집 2장의 주소를
모두 정확히 만들어 주세요(ETag가 있는 catalog.json, verify, Range와 CORS가 있는 /a/{id}, /i, /l, /p).
자동 등록(5장)과 변경할 때마다 ping(7장)도 넣어 주세요. 방문자 화면 맨 아래에는 2a장의
"Powered by aimusiclist.com" 링크를 넣어 주세요(끄는 설정 없음). 모든 파일의 전체 코드와 초보자용 설치
순서를 주고, 마지막에 aimusiclist 검사 도구로 확인하는 방법을 알려 주세요.
```

### 요청문 B — Cloudflare Workers + R2 (무료 요금제)
```text
위 내용은 aimusiclist 규약집입니다. 내 음악 사이트를 R2 버킷이 연결된 Cloudflare Worker 1개로
만들어 주세요(무료 요금제, 요청 수와 CPU를 적게). 기능: 관리자 로그인, MP3/M4A 곡을 R2에 올리기
(최대 15MB), 제목·아티스트·장르(genres.json의 키) 수정, "aimusiclist.com에도 공개" 체크(기본 체크됨),
앨범. site_secret은 처음 실행할 때 만들어 R2에 보관하고 HTML/JS에는 절대 넣지 마세요. 규약집 2장의
주소를 모두 정확히 만들고, 자동 등록(5장), 변경할 때마다 ping(7장), cron 재시도를 넣어 주세요.
방문자 화면 맨 아래에는 2a장의 "Powered by aimusiclist.com" 링크를 넣어 주세요(끄는 설정 없음).
전체 코드, wrangler.toml, 초보자용 배포 순서, 검사 도구로 확인하는 방법을 알려 주세요.
```

### 요청문 C — 정적 사이트 + 작은 서버
```text
위 내용은 aimusiclist 규약집입니다. 내 화면은 정적 사이트(예: GitHub Pages, Netlify)입니다. 화면은
정적인 그대로 두고, 가장 작은 서버 부분(같은 도메인의 무료 Cloudflare Worker 1개, 또는 리다이렉트가
아닌 내부 rewrite를 쓰는 서버리스 함수)을 더해서 규약집 2장의 주소를 모두 제공하고, site_secret을
비밀로 지키고, 자동 등록(5장)과 ping(7장)을 하게 해 주세요. 모든 화면 맨 아래에는 2a장의
"Powered by aimusiclist.com" 링크를 넣어 주세요. 곡은 [R2 / 내 저장소]에 있습니다.
어떤 주소가 서버 부분으로 가야 하는지 정확히 설명하고, 전체 코드와 초보자용 순서, 검사 도구로
확인하는 방법을 알려 주세요.
```

팁: AI가 다 만들면 `https://aimusiclist.com/developers/check`를 열어 내 사이트 주소를 넣고, 빨간색·노란색 줄을 그대로 AI에게 붙여넣으세요.

**AI 자기 점검(마지막 파일을 보내기 전에)**: 영어 원본 0e장 목록을 모두 확인하세요. 특히
- [ ] 방문자 화면마다 맨 아래에 작은 링크 **`Powered by aimusiclist.com`**(2a장)이 있다: 등록 전에는 `https://aimusiclist.com/`, 등록 뒤에는 `https://aimusiclist.com/artist/{site_id}`. 끄는 설정은 없다.

---

## 1. 전체 흐름

aimusiclist.com은 **파일을 받지 않습니다.** 내 사이트에서 곡 목록을 **가져갑니다.** 내 사이트는 "다시 가져가 주세요"라는 신호(ping)만 보냅니다.

```text
 내 사이트 (https://your.site)                         aimusiclist.com
 ─────────────────────────────                         ─────────────────────────────
 1. 처음 실행: site_secret(16진수 64자리)을 만들어 비밀로 보관
 2. POST /api/v1/sites/auto {url, name, …, site_secret} ──►
                                    ◄── GET /.well-known/aimusiclist/verify?c=CHALLENGE
 3. 응답 {"site_id":…, "proof": HMAC(site_secret, "verify:"+CHALLENGE)} ──►
                                    ◄── 201 {"ok":true,"site_id":"s_xxxxxxxx"}
 4. site_id 저장, catalog.json에 넣기
                                    ◄── GET /.well-known/aimusiclist/catalog.json (약 3초 뒤)
 5. 바뀔 때마다: POST /api/v1/ping (Bearer site_secret) ──►
                                    ◄── GET catalog.json (대기열에서, 사이트별 6시간 분산)
 6. aimusiclist.com 청취자가 재생      ◄── 브라우저 GET /a/{id} (Range, CORS)
                                           브라우저 GET /i/{id} 이미지, /l/{id} 가사
```

- 곡은 **내 catalog.json에 있는 동안에만** aimusiclist에 공개됩니다. 목록에서 빼면 = 비공개.
- 내 사이트에는 모든 곡을 보여 줘도 됩니다. "aimusiclist.com에도 공개"가 켜진 곡만 catalog.json에 넣습니다.
- 주인 확인 = "그 주소에서 코드를 실제로 돌릴 수 있는가". 회원가입·수동 승인 없음.

---

## 2. 내 사이트가 반드시 제공해야 하는 주소

모두 내 **사이트 origin**(예: `https://music.example.com`, 경로 없음)에 있어야 합니다. HTTPS만. **리다이렉트 금지**(aimusiclist는 301/302를 따라가지 않음). 이 주소들에는 로그인·봇 확인 화면을 걸지 마세요.

| 주소 | 방식 | 필수 | 돌려줄 것 |
|---|---|---|---|
| `/.well-known/aimusiclist/catalog.json` | GET | 반드시 | catalog JSON(3장). `Content-Type: application/json`. ETag + `If-None-Match`면 `304` 권장 |
| `/.well-known/aimusiclist/verify?c={challenge}` | GET | 반드시 | `{"site_id": "s_…" 또는 null, "proof": "소문자 16진수 64자리"}` (6장) |
| `/a/{track_id}` | GET, HEAD, OPTIONS | 반드시 | 음원 파일. Range 지원(`206`), `Accept-Ranges: bytes`, `Cache-Control: no-cache`, CORS(아래) |
| `/i/{track_id}` | GET | 선택 | 곡 이미지 100×100 WebP (직접 올린 이미지를 쓸 때만) |
| `/l/{track_id}` | GET | 선택 | 가사 `.srt` 글자, `text/plain; charset=utf-8`, CORS(아래) |
| `/c/{album_id}` | GET | 선택 | 앨범 커버 200×200 WebP |
| `/p` | GET | 선택 | 프로필 이미지 200×200 WebP, 100KB 이하 (aimusiclist가 `/p?v={해시}`로 내려받음) |

**시간**: catalog는 **3초**(ping) / **10초**(정기) 안에, verify는 **5초** 안에 응답. 응답 최대 **1MB**(catalog 자체는 512KB 이하).

### CORS (`/a/`·`/l/`은 반드시)
aimusiclist.com 청취자의 브라우저가 `/a/`를 `crossOrigin`으로(비주얼라이저용), `/l/`을 `fetch()`로 불러옵니다. 요청 머리글 `Origin`이 `https://aimusiclist.com` 또는 `https://www.aimusiclist.com`이고 **그 곡이 catalog에 있으면** 다음을 보냅니다.

```text
Access-Control-Allow-Origin: https://aimusiclist.com        (요청의 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
```
`OPTIONS`에는 `204` + 같은 머리글. catalog에 **없는** 곡에는 CORS 머리글을 붙이지 않습니다(내 사이트에서는 그대로 재생됨).
`/a/`에 CORS가 없으면 aimusiclist에서 재생은 되지만 진짜 비주얼라이저가 움직이지 않고, `/l/`에 없으면 가사 창이 열리지 않습니다.

### 2a. 모든 화면 맨 아래 "Powered by aimusiclist.com" (반드시)

**방문자**가 보는 내 사이트의 모든 화면은 맨 아래에 정확히 이 글자의 링크가 든 작은 한 줄로 끝나야 합니다(MUST): **`Powered by aimusiclist.com`**. 이 규약집으로 사이트를 만드는 사용 조건입니다(aimusiclist 공식 개인사이트 프로그램도 똑같이 합니다).

- **링크 주소**: 등록 뒤(`site_id` 가 있음, 5장) → `https://aimusiclist.com/artist/{site_id}`(내 아티스트 페이지). 등록 전 → `https://aimusiclist.com/`. 같은 탭(`target="_blank"` 없음). 아티스트 페이지에 아직 공개곡이 없으면 aimusiclist 가 첫 화면으로 보냅니다.
- **위치·모양**: 화면 맨 마지막 줄(하단 문구)에, 다른 하단 글자와 같은 크기·같은 색. 작고 조용해도 됩니다.
- **끄기 없음**: 이 줄을 빼는 설정·스킨 옵션·스위치를 만들지 않고, 숨기지 않습니다(`display:none`, `visibility:hidden`, `opacity:0`, 바탕과 같은 글자색, 화면 밖으로 밀기 등). 관리 화면에는 없어도 됩니다.
- 예:
  ```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 (규격 1.3)

최상위 — 모두 반드시:

| 필드 | 타입 | 규칙 |
|---|---|---|
| `schema` | string | `"1.3"` (aimusiclist는 `1.0`·`1.1`·`1.2`·`1.3`과 앞으로의 `1.x`를 모두 받고 `2.0`은 거부) |
| `generated_at` | string | ISO 8601 UTC, 예 `"2026-10-28T04:23:21Z"` |
| `site` | object | 아래 |
| `albums` | array | 없으면 `[]` |
| `tracks` | array | 없으면 `[]` (형식이 맞는 빈 목록 = "모두 비공개") |

**site**

| 필드 | 타입 | 필수 | 규칙 |
|---|---|---|---|
| `site_id` | string | 반드시 | 등록 때 받은 값(`s_` + 8자). 틀리면 목록 전체 거부 |
| `url` | string | 반드시 | 등록한 origin과 정확히 같게, 예 `https://music.example.com`. 틀리면 목록 전체 거부 |
| `name` | string | 반드시 | 사이트 이름 1~40자 |
| `artist_name` | string | 반드시 | 대표 아티스트 1~40자 |
| `default_lang` | string | 반드시 | `en`, `ko`, `ja`, `pt-BR` … |
| `description` | string | 선택 | 200자 이내 |
| `profile_url` | string | 선택 | `{url}/p?v={소문자 16진수 8자리}`만 (v = 이미지 해시, 이미지를 바꾸면 v도 바꿈) |
| `socials` | string[] | 선택 (1.2) | 아티스트 SNS **종류 키**만 0~10개, 예 `["youtube","instagram","soundcloud"]` — **주소는 넣지 않음** (3b) |
| `ext` | object | 선택 (1.3) | fields.json에서 `scope: "site"`인 추가 정보 (3c), 2KB 이하 |

**tracks[]**

| 필드 | 타입 | 필수 | 규칙 |
|---|---|---|---|
| `id` | string | 반드시 | `[a-z0-9_-]`, 1~64자, 사이트 안에서 고유, **절대 바꾸지 않음** |
| `title` | string | 반드시 | 1~100자 |
| `artist` | string | 반드시 | 1~60자 (보통 사이트 이름, 또는 가상 아티스트 이름) |
| `genres` | string[] | 반드시 | 장르 **키** 1~3개, 첫 번째 = 메인 장르(4장) |
| `duration` | integer | 반드시 | 초, **정수** 30~900 (`221.4`나 `"221"` 안 됨) |
| `audio_url` | string | 반드시 | 정확히 `{site.url}/a/{id}` |
| `published_at` | string | 반드시 | ISO 8601, 예 `"2026-10-28T02:13:57Z"` (표시용) |
| `thumb_url` | string | 선택 | `{site.url}/i/{id}` 또는 `https://aimusiclist.com/img/default/{5자리}.webp`만 (10장) |
| `thumb_lg_url` | string | 선택 | `{site.url}/i/{id}?s=l` 만 (큰 그림 200~400px 정사각 WebP, 60KB 이하), `thumb_url` 이 `{site.url}/i/{id}` 일 때만 (10장) |
| `lyrics_url` | string | 선택 | `{site.url}/l/{id}`만, 가사가 있을 때만 |
| `langs` | string[] | 선택 (1.2) | 곡의 가사 언어 1~2개, 첫 번째 = 주 언어 (3a). 연주곡은 `["zxx"]` 하나만 |
| `ext` | object | 선택 (1.3) | `scope: "track"`인 추가 정보, 예 `{"mood":["calm","nostalgic"]}` (3c), 1KB 이하 |

**albums[]** (9장)

| 필드 | 타입 | 필수 | 규칙 |
|---|---|---|---|
| `id` | string | 반드시 | 곡 id와 같은 규칙 |
| `title` | string | 반드시 | 1~100자 |
| `artist` | string | 반드시 | 1~60자 |
| `cover_url` | string/null | 반드시 | `{site.url}/c/{id}` 또는 `null` |
| `track_ids` | string[] | 반드시 | 서로 다른 곡 id 10~30개, 모두 `tracks`에 있어야 함 |
| `published_at` | string | 반드시 | ISO 8601 |

**한도**: 파일 512KB, 곡 1,000개, 앨범 100개 이하. 모르는 필드는 무시합니다(내 필드를 더해도 안전 — 예를 들어 내 사이트에서만 쓰는 칸을 담은 `local`은 무시됩니다. 그런 칸은 `ext`에 넣지 마세요).

**틀렸을 때 aimusiclist의 처리**

| 틀린 것 | 결과 |
|---|---|
| JSON 오류, `schema`가 1.x 아님, `site.site_id`/`site.url` 다름, 한도 초과 | 목록 전체 거부, 이전 상태 유지, 가져가기 실패 1회 |
| 곡 하나가 틀림(id, title, artist, 메인 장르, duration, audio_url, published_at, 중복 id) | 그 곡만 거부 |
| `thumb_url` / `thumb_lg_url` / `lyrics_url` / `profile_url` 형식 틀림 | 그 필드만 무시 |
| (1.2) `langs`의 모르는 코드, `socials`의 모르는 키, SNS 10개 초과 | 그 값만 무시. 맞는 `langs`가 없으면 곡 언어 = `site.default_lang`의 `-` 앞부분(예 `pt-BR` → `pt`) |
| (1.3) fields.json에 없는 `ext` 키, 형식·옵션 틀림, 개수 초과, 1KB 초과 | 그 값만(1KB 초과는 `ext` 전체) 무시 — 곡은 유지 |
| 모르는 보조 장르·중복 장르 | 그 장르만 무시 |
| 앨범이 틀림(곡 수, 없는 곡, 필드) | 그 앨범만 거부 |
| 앨범 `cover_url` 형식 틀림 | 커버만 무시 |
| `site.name` / `artist_name` / `default_lang` 틀림 | 이전 값 유지 |

### 최소 예시 (곡 1개)
```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" }
  ]
}
```

### 전체 예시 (선택 필드 모두, 앨범은 줄여서 표시)
```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"] }
  ]
}
```
(위 앨범이 공개되려면 `t_01`…`t_10` 10곡이 모두 `tracks`에 있어야 합니다. 여기서는 2곡만 보였습니다.)
aimusiclist 표시: 곡 제목 아래에 `Hip2pm`(아티스트 = 사이트 이름, 대소문자 무시) 또는 `Hip2pm > Neon Ghost`(아티스트가 다름). 사이트 이름을 누르면 내 사이트로 갑니다.
곡 줄 오른쪽에는 [SNS 아이콘] → [SRT(가사가 있을 때만)] → [♥ 좋아요 수] → [+ 담기]가 보입니다. 위 예시의 SNS는 YouTube, Instagram, 🔗(SoundCloud) 세 개로 보입니다.

### 3a. 곡 언어 `langs` (catalog 1.2)

- 내 사이트 화면 언어가 아니라 **곡의 가사 언어**입니다. 1~2개, 첫 번째가 주 언어. 언어별 차트와 언어 필터는 주 언어 기준입니다.
- 코드 목록: `GET https://aimusiclist.com/api/v1/song-langs.json` (공개, CORS `*`, ETag, 하루 캐시). 지금:
  `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` = 연주곡·가사 없음 — 혼자만, `mis` = 기타 언어). 코드에는 **지역이 없습니다**: `pt-BR`이 아니라 `pt`, `zh-TW`가 아니라 `zh`.
- 언어 이름은 그 언어로 쓴 자기 이름을 그대로 보여 주세요(목록에 있음: `한국어`, `日本語`, `ไทย` …). `zxx`·`mis`만 이름이 없고(`"key":"songlang.zxx"`) 내 언어팩에서 번역합니다.
- 아티스트가 아무것도 고르지 않으면 기본값 = 사이트 기본 언어(`-` 앞부분). `langs`를 보내지 않으면 aimusiclist도 같은 값으로 봅니다.
- 듣는 사람은 "언어가 틀려요"를 신고할 수 있습니다. aimusiclist 관리자가 곡 언어를 고치면 그 값이 우선합니다(관리자가 고정을 풀 때까지). 계속 틀리게 표시하는 사이트는 언어별 차트에서 제외될 수 있습니다.

### 3b. SNS `site.socials` (catalog 1.2)

- aimusiclist는 SNS의 **종류(키)만** 받고 주소는 받지 않습니다. aimusiclist의 아이콘은 **내 사이트의** SNS 모음 화면 `{site.url}/#/links/{key}`(🔗는 `{site.url}/#/links/more`)로 보내므로, 내 사이트는 그 화면에 SNS 주소를 보여 줘야 합니다.
- 키 — 큰 5개(aimusiclist에 아이콘으로, 늘 이 순서): `youtube instagram tiktok facebook x`.
  그 밖의 SNS(aimusiclist에는 🔗 하나로): `threads soundcloud spotify applemusic bandcamp suno udio discord twitch line whatsapp telegram kakao bilibili patreon bluesky vk weibo`.
- `socials` 순서: 쓰는 큰 5개 먼저, 그다음 다른 SNS. 최대 10개. 모르는 키는 무시합니다.
- 내 관리 화면은 각 SNS의 공식 도메인 `https://` 주소만 받게 하세요(예: YouTube는 `youtube.com`, `youtu.be`).

### 3c. 추가 정보 `ext`와 `fields.json` (catalog 1.3)

새 선택 항목(예: **분위기 mood**)은 파일 하나로 알려 주므로, 항목이 생길 때마다 사이트 코드를 고칠 필요가 없습니다.

- `GET https://aimusiclist.com/api/v1/fields.json` (공개, CORS `*`, ETag, `Cache-Control: public, max-age=3600`, 64KB 이하, 데이터만):
  ```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"}]}
  ```
- 형식: `text`(글자, `max_length` ≤ 200) · `number`(`min`/`max`) · `bool` · `select`(옵션 키 1개) · `multiselect`(옵션 키 배열, `max_select` ≤ 5). `scope`가 `track`이면 `tracks[].ext`, `site`면 `site.ext`.
- 글자: 내 화면 언어로, 없는 언어는 `en`. 항상 이스케이프해서 출력하세요(HTML이 아니라 데이터).
- **하루 1번**, 사이트마다 흩어진 시각(예: 분 = hash(site_id) mod 1440)에 `If-None-Match`로 받습니다. 실패·잘못된 JSON·64KB 초과·모르는 `schema`(예 `fields-2`)면 마지막 정상본을 계속 씁니다. 모르는 `type`은 그 항목만 건너뛰고 "이 항목은 프로그램 업데이트 후 쓸 수 있습니다"를 보여 주세요.
- 새 항목은 항상 선택 사항입니다. `status: "deprecated"` = 입력란은 숨기되 있는 값은 계속 보냅니다. 키·옵션 키는 바꾸거나 다시 쓰지 않습니다.
- 내 사이트에서만 쓰는 칸은 별도 `local` 객체에 두고 catalog.json으로 **보내지 않습니다**.
- 값: 아티스트가 채운 것만 보냅니다. aimusiclist는 모든 값을 fields.json으로 검사해 틀린 값만 버립니다.
- aimusiclist는 `display.track_row` 항목을 장르 옆 회색 글자로, `display.filter` 항목을 목록 필터 칩으로 보여 줍니다. **추가 정보는 순위 점수에 쓰지 않습니다.**
- **경계**: 공통 입력 엔진(aimusiclist 개인사이트 코드: Workers판·PHP판·메쉬멜로 호스팅)을 쓰는 사이트는 새 항목이 자동으로 생깁니다. **직접 만든 업로드 화면**(자체 PHP, WordPress 등)은 aimusiclist 연동은 계속 되지만, 새 칸은 개발자가 fields.json을 읽는 코드를 붙여야 생깁니다. 공통 입력 엔진 사용을 권장합니다.

### 3d. 개인사이트 프로그램 새 버전 `versions.json`

- `GET https://aimusiclist.com/api/v1/versions.json` (공개, CORS `*`, 하루 캐시):
  `{"schema":"versions-1","personal_site":{"latest":"3.8.0","min_supported":"3.5.0","update_url":"https://…","notes_url":null}}`
- 설치된 버전이 `latest`보다 낮으면 관리 화면 머리에 "새 버전이 있습니다" 한 줄을 보여 주세요(강제 아님).
- **PHP판 업데이트 파일** (2026-10-29 추가, 필드 추가만): aimusiclist가 내려받을 파일을 준비하면 `personal_site`에
  `"php":{"latest":"3.11.0","zip":"https://aimusiclist.com/dl/personal-site/aml-personal-site-php-3.11.0.zip","sha256":"<64자리 hex>"}`가 더 붙습니다.
  `zip`은 항상 `https://aimusiclist.com/dl/personal-site/`로 시작하고, 설치 전에 `sha256`을 확인하세요. 세 값이 모두 맞을 때만 보내므로 칸이 없으면 PHP판 파일이 없는 것입니다(아무것도 하지 않음). 모르는 칸은 무시하세요.

---

## 4. 장르

- 목록: `GET https://aimusiclist.com/api/v1/genres.json` (공개, CORS `*`, 하루 캐시)
  ```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":"네오 소울"}}]}]}
  ```
- `genres`에는 **키**(`"rnb"`)를 넣고, 표시 이름(`"R&B"`, `"알앤비"`)은 넣지 않습니다.
- `genres[0]`은 반드시 **메인** 키(맨 위 단계 `key`). 세부 키(예 `"kpop"`)를 넣으면 → 그 곡 거부.
- `genres[1]`, `genres[2]`(선택) = 메인·세부 키 아무거나. 중복 금지. 모두 합쳐 최대 3개.
- 지금 메인 키: `pop rnb hiphop rock electronic dance ballad jazz classical folk latin soundtrack ambient world`. 최신 목록은 항상 genres.json에서 읽으세요(키는 바꾸거나 지우지 않습니다).
- aimusiclist에서 받아 쓰는 설정 파일(`genres.json`, `song-langs.json`, `fields.json`, `default-images.json`, `versions.json`)은 하루 1번만, `If-None-Match`로 받고, aimusiclist에 닿지 않으면 마지막 것을 계속 쓰세요.

---

## 5. 자동 등록 — `POST https://aimusiclist.com/api/v1/sites/auto`

회원가입·로그인 없음. 처음 곡을 공개할 때(또는 설치 직후) 보냅니다. 응답은 최대 20초 기다립니다. 요청을 처리하는 동안 aimusiclist가 내 verify 주소를 부르므로 **내 서버는 동시에 다른 요청에 응답할 수 있어야 합니다.**

요청 (`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": "소문자 16진수 64자리" }
```

| 필드 | 규칙 |
|---|---|
| `url` | `https://` + 호스트만. 경로·쿼리·포트·IP 주소 불가, 200자 이내. 주소 하나에 등록 하나 |
| `name`, `artist_name` | 1~40자 |
| `contact_email` | 선택(권장). 공개하지 않고 차단·응답 없음 알림에만 씀 |
| `default_lang` | 예 `en` (없으면 `en`) |
| `site_secret` | 16진수 64자리. 안전한 무작위로 한 번 만듦(PHP `bin2hex(random_bytes(32))`, JS `crypto.getRandomValues`). 서버에만 보관 |

성공: `201 {"ok":true,"site_id":"s_ez57qni8","renewed":false,"status":"active"}` → **`site_id`를 바로 저장**하고 catalog.json에 넣습니다. aimusiclist는 약 3초 뒤(늦어도 1~2분 안) catalog를 가져갑니다.

**다시 등록**(재설치, 비밀키 분실): 새 비밀키를 만들어 같은 `url`로 다시 보내면 됩니다. verify를 통과하면 `site_id`는 그대로, 비밀키만 바뀌고 `201` + `"renewed": true`.

오류 (본문 `{"ok":false,"error":{"code":"…","params":{…}}}`):

| HTTP | code | 뜻 / 할 일 |
|---|---|---|
| 400 | `err.site.url_invalid` | 주소 형식 오류(http, 경로, 포트, IP…) |
| 400 | `err.validation` | 필드 오류, `params.field`가 어느 필드인지 알려 줌 |
| 429 | `err.rate_limited` | 같은 url을 10분 안에 다시 보냄(실패한 시도도 셈), 또는 이번 시간 새 사이트가 너무 많음. `params.retry_after`초(`Retry-After` 머리글) 기다리기 |
| 403 | `err.site.suspended` | 관리자 차단. 다시 등록해도 풀리지 않음 |
| 422 | `err.site.verify_failed` | proof가 맞지 않음(6장) |
| 422 | `err.catalog.too_large` | verify 응답이 1MB 초과 |
| 503 | `err.site.offline` | verify에 연결 못 함(시간 초과, DNS, 인증서, 리다이렉트, HTTP 오류) |
| 503 | 내 사이트의 코드 | verify가 `{"ok":false,"error":{"code":"err.site.not_configured"}}`를 돌려주면 그 코드를 그대로 전달 |

내 사이트의 재시도: 실패하면 10분마다, 하루 최대 20번.

### 5a. 허브 운영자용 (여러 아티스트의 사이트를 대신 운영하는 곳)

*일반 아티스트는 읽지 않아도 된다.* **허브**는 여러 아티스트의 개인사이트를 대신 운영·등록하는 서비스다(예: `이름.example.com` 무료 호스팅). aimusiclist 관리자가 허브를 직접 만들고, 운영자에게 허브 ID(`h_…`)와 허브 키를 한 번 알려 준다. 가입 화면은 없다.

- 아티스트 사이트**마다** 같은 `POST /api/v1/sites/auto` 를 부르면서 머리글 두 개를 더한다:
  ```
  X-AML-Hub: h_example
  X-AML-Hub-Key: hk_…            (허브 서버에만 둔다. 화면·catalog 에 넣지 않는다)
  ```
- **주인 확인(verify)은 그대로 사이트마다** 한다. aimusiclist 가 그 사이트의 `/.well-known/aimusiclist/verify?c=…` 를 6장과 똑같이 그 사이트의 `site_secret` 으로 확인한다. 허브 키가 있어도 내 것이 아닌 주소는 등록할 수 없다.
- 허브 사이트의 새 등록은 **허브 자신의 시간당 한도**(기본 시간당 500개)로 세고, 다른 아티스트들이 쓰는 전체 한도에서 빼지 않는다. 넘으면 `429 err.rate_limited` + `Retry-After`. 이미 있는 주소의 재등록은 세지 않는다.
- 키가 틀리거나 없거나 허브가 차단(suspended)이면 **오류가 아니라** 일반 등록과 똑같이 처리된다(허브 표시 없음, 전체 시간당 한도 사용).
- 성공하면 `201` 응답에 `"hub":"h_example"` 이 붙는다(일반 등록이면 `null`).
- `contact_email` 은 **아티스트 이메일**이다(차단·응답 없음 알림이 아티스트에게 간다). 허브 연락처는 aimusiclist 가 따로 보관한다.
- aimusiclist 의 아티스트 페이지와 아티스트 카드에 작게 "via {허브 이름}" 이 보인다. catalog.json·ping·가져가기는 다른 사이트와 똑같다. aimusiclist 가 허브를 차단하면 해제할 때까지 그 허브의 모든 사이트가 보이지 않는다.

---

## 6. verify (주인 확인) — `GET /.well-known/aimusiclist/verify?c={challenge}`

- `c` = aimusiclist가 만든 무작위 값(지금은 16진수 32자리, `[A-Za-z0-9_-]{8,128}`을 받으면 됨).
- 응답 `200`, `Content-Type: application/json`, `Cache-Control: no-store`:
  `{"site_id": "<내 site_id, 첫 등록 전이면 null>", "proof": "<16진수>"}`
- **proof = HMAC-SHA256(키 = site_secret, 메시지 = "verify:" + c)**, 소문자 16진수 64자리.
- 키는 64자리 비밀키 **글자 그대로**(문자열의 UTF-8 바이트)입니다. 16진수를 바이트로 **바꾸지 마세요.**
- 아직 비밀키가 없으면: `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,                 // 저장해 둔 값
                  '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' } });
```
시험 값: secret = `"0123456789abcdef"`를 4번 이은 64자, c = `"abc123xyz0000000"` →
proof = `a7ebecd185ed00ae76912b177af4ba1dc0f3a67f57d546bbd473831840162f6c`

---

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

catalog.json이 바뀌는 **모든** 변경(올리기, 수정, 공개/비공개, 삭제) 뒤에 보냅니다. 여러 파일을 올리면 끝난 뒤 **한 번만**.

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

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

| HTTP | 본문 | 뜻 |
|---|---|---|
| 200 | `{"ok":true,"status":"updated","tracks":24}` / `"not_modified"` | 바로 가져감 |
| 202 | `{"ok":true,"status":"queued"}` (+ `error`) | 몇 분 안에 다시 시도 |
| 401 | `err.auth.invalid_secret` | 비밀키 틀림 → 다시 등록(5장) |
| 404 | `err.site.not_found` | 모르는 site_id → 다시 등록 |
| 403 | `err.site.suspended` | 차단됨 |
| 429 | `err.rate_limited` | 사이트당 분당 6회 또는 시간당 60회 초과 |

ping이 실패하면 10초 뒤, 그다음 1분쯤 뒤 한 번 더. ping은 즉시 HTTP 202를 반환하고 서버 호출은 대기열에서 수행합니다. 정기 가져가기는 6시간마다 사이트별로 다른 시각에 분산하며, 고정된 4개 시각에 일괄 요청하지 않습니다. 변경 확인에는 `If-None-Match`를 사용합니다.

---

## 8. 주소 바꾸기(개인 도메인) — `POST https://aimusiclist.com/api/v1/sites/{site_id}/url`

```text
Authorization: Bearer {site_secret}
{"url":"https://mymusic.com"}
```
1. 새 도메인이 **같은 비밀키**로 verify(6장)에 이미 응답해야 합니다.
2. aimusiclist가 새 주소에서 verify를 확인. 통과 → `200 {"ok":true,"site_id":"…","url":"https://mymusic.com","changed":true}` 그리고 새 주소에서 catalog를 가져감.
3. 그 뒤로 catalog의 `site.url`과 모든 `*_url`은 새 주소를 써야 합니다.
4. 옛 주소도 `/.well-known/`, `/a/`, `/c/`, `/i/`, `/l/`에는 계속 응답해야 합니다(저장된 플레이리스트가 그 주소를 씀). 방문자 화면만 301로 옮겨도 됩니다.

오류: `401`, `404`, `400 err.site.url_invalid`, `409 err.site.url_taken`(다른 사이트가 쓰는 주소), `422 err.site.verify_failed`, `503`, `403 err.site.suspended`, `429`(시간당 5회).

### 8a. 내 곡 통계 — `GET https://aimusiclist.com/api/v1/sites/{site_id}/stats?days=30`

내 관리 화면용: 내 공개곡이 aimusiclist 에서 어땠는지. 선택 기능 — 부르지 않아도 아무 문제 없습니다.

```text
Authorization: Bearer {site_secret}
If-None-Match: "ss-…"        (선택, 지난번에 받은 ETag)
```
- `days` = 1~90 (기본 30). aimusiclist 는 **하루 한 번 UTC 00:10 이후** 자료를 만들므로 **하루 한 번만** 부르세요(사이트마다 다른 분이면 좋음, 예: 하루 cron). ETag 가 같으면 `304`(빈 본문).
- 응답 `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]]}
```

| 필드 | 뜻 |
|---|---|
| `through` | 들어 있는 마지막 꽉 찬 날(UTC). `days` 는 오래된 날 → 최근 순, 여기서 끝남(없는 날은 0) |
| `plays` | aimusiclist 에서 재생 시작 |
| `valid` | 유효 재생: 30초를 듣고 중복·한도 검사를 통과한 재생(순위가 셀 수 있는 재생) |
| `likes`, `adds` | 그날 새 좋아요 / 플레이리스트 담기 |
| `tracks[]` | 내 사이트 공개곡 전부, `plays7`(최근 7일) 순. `id` = catalog `tracks[].id`, `likes` = 지금 좋아요 합계 |
| `chart_rank`, `chart_rank_prev` | 최신 **주간 Global** 차트(`chart_date`)의 저장 순위와 그 전 주. `null` = 차트에 없음 |
| `genre_rank`, `genre_rank_prev` | 곡 메인 장르의 주간 차트에서 같은 것 |
| `all_rank`, `all_rank_prev`, `all_rank_best`, `all_rank_move` | **지난 7일**(움직이는 기간, Global 점수)에 청취자가 있는 **모든 곡** 가운데 순위(상한 없음 — 1~100위가 주간 Top 100 과 다를 수 있음): 지금·직전 갱신·최고, 변동 `up`·`down`·`same`·`new`·`re`(직전 갱신과 비교). 지난 7일 동안 아무도 안 들었으면 모두 `null` |
| `countries` | 유효 재생의 청취 국가, 최근 30일, 상위 20 (`XX` 모름) |

- 첫 하루 집계 전: `200` + `"generated":null` + 빈 목록 — 그 칸을 숨기세요.
- 필드는 더해질 수 있습니다. 모르는 필드는 무시하세요.

오류: `401 err.auth.invalid_secret`(비밀키 없음·틀림), `404 err.site.not_found`, `403 err.site.suspended`, `429 err.rate_limited` + `Retry-After`(사이트당 하루 24번, `304` 포함 · IP당 분 60번).

### 8b. 사이트 닫기 — `POST https://aimusiclist.com/api/v1/sites/{site_id}/close`

아티스트가 사이트를 **지우거나 영구히 닫을 때** 보냅니다. aimusiclist 는 사이트를 바로 정리합니다(가져가기 실패로 약 12시간 뒤 숨기는 대신). 선택 사항 — 보내지 않는 옛 사이트도 그대로 동작합니다.

```text
Authorization: Bearer {site_secret}
Content-Type: application/json
{"reason":"deleted"}          (선택: "deleted" | "closed")
```
- 결과 = 주인이 aimusiclist 에서 *등록 해지*를 누른 것과 같음: 사이트는 `removed`, 곡·앨범은 모든 목록에서 빠지고 가져가기 대기열도 지워집니다. 재생 기록·지난 차트는 남습니다.
- 응답 `200 {"ok":true,"status":"removed"}` — 이미 removed 여도 같으므로 두 번 보내도 안전합니다.
- 나중에 돌아오기: `POST /sites/auto`(5장)로 다시 등록. 같은 `site_id` 를 유지합니다.
- 되돌릴 일시 정지(예: 호스팅 운영자의 임시 정지)에는 보내지 **마세요** — 빈 catalog 와 ping 을 쓰세요.
- 실패(네트워크·`5xx`·`429`)하면 하루 동안 나중에 다시(예: cron) 보내세요. 이 주소를 모르는 옛 aimusiclist 의 `404` = 조용히 포기.

오류: `401 err.auth.invalid_secret`, `404 err.site.not_found`, `403 err.site.suspended`(aimusiclist 가 정지한 사이트는 정지 그대로), `429 err.rate_limited` + `Retry-After`(사이트당 시간 10회).

### 8c. 관리 화면용 차트 분석·소식 — `GET https://aimusiclist.com/feed/insights.json` · `/feed/news.json`

aimusiclist 가 **하루 한 번** 순위 계산 직후(UTC 00:30 무렵) 만드는 공개 파일 두 개입니다. 개인사이트 *관리 화면*에 보여 주면 아티스트가 자기 사이트에서 차트를 분석할 수 있습니다. 키·로그인 없음.

- 내 사이트 cron 으로 **하루 1번**(사이트마다 다른 분이면 좋음, 예: 분 = hash(site_id) mod 1440) `If-None-Match: {지난 ETag}` 를 붙여 받으세요. `304` = 바뀐 것 없음. 화면을 열 때마다 받지 마세요.
- 실패·잘못된 JSON·크기 초과(insights 200KB, news 100KB)·모르는 `schema` 면 마지막 정상본을 계속 보여 주세요.
- 머리글: `Cache-Control: public, max-age=3600, s-maxage=86400`, `Access-Control-Allow-Origin: *`, `ETag`.
- `insights.json` (`"schema":"insights-1"`): `period{daily,weekly}`, `global.top`(주간 Global Top 100: `rank`·`prev`·`move` up|down|same|new|re·`listeners`·`countries`·`track`), `global.rising`(일간 Rising 차트 위 20, `jump` = 오른 계단), `global.new`(이번 주 Top 100 처음 진입), `genres.{메인 장르}.top`(20)·`.rising`(10), `countries[]`(`cc`·`listeners`·`top`), `threshold{top100_listeners,rank100_score}`(100곡 미만이면 null), `totals{tracks,sites,countries,plays7}`.
  `track` = `{"id":"123","title","artist","site","site_url","page","genres":[…],"img"}` — `id` 는 aimusiclist 곡 번호(내 catalog id 아님). *내 곡*은 `site_url` 로 고르세요. **음원 주소는 없습니다**: 재생은 `page`(aimusiclist)나 내 곡 화면으로 링크.
- `news.json` (`"schema":"news-1"`): 게시된 소식 최대 30개 `{id, date, kind: report|notice, title{언어:…}, summary{언어:…}, url, chart?}`. 제목·요약·그래프만 보여 주고 [자세히] = `url`. 없는 언어는 `en`. `chart` = `{type: bar|line, labels[], series[{key,label{언어},values[]}], title{언어}}` (실제 숫자).
- 새 필드가 생길 수 있습니다. 모르는 것은 무시하세요. 전체 형식: aimusiclist `docs/spec/차트분석-소식-v1.md`.

---

## 9. 앨범

- 내 사이트의 앨범은 몇 곡이어도 되지만, catalog.json에는 **10~30곡**인 앨범만 넣습니다.
- `track_ids`의 모든 id는 `tracks`에도 있어야 합니다(앨범을 공개하면 소속 곡도 공개).
- aimusiclist가 일부 곡을 거부해서 10곡 미만이 남으면 그 앨범은 숨겨집니다.
- aimusiclist 첫 화면은 10곡 이상인 새 앨범을 소개합니다. 플레이리스트도 10곡 이상이어야 소개됩니다.

## 10. 기본 곡 이미지

- aimusiclist가 무료 곡 이미지 9,963장(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"}`
- 곡을 올릴 때 1…count 중 무작위 번호를 골라 5자리로 앞에 0을 채워 `thumb_url` = base + 번호 + ext로 저장. 요청이 실패하면 count 9963을 씁니다. 그 곡은 번호를 계속 유지합니다.
- `thumb_url`이 없으면 aimusiclist가 자기 곡 번호로 기본 이미지를 고릅니다.
- 내 이미지를 쓰려면 `/i/{id}`(100×100 WebP, 50KB 이하)로 제공하고 `thumb_url`에 그 주소를 넣습니다.
- 큰 그림(선택): 같은 그림을 `/i/{id}?s=l`(정사각 WebP, 400×400 권장, 200~400px, 60KB 이하 — 없으면 404, 작은 그림을 대신 보내지 않음)로도 제공하고 `thumb_lg_url`에 그 주소를 넣습니다. aimusiclist 는 차트 1~3위(큰 카드) 곡만 받아 사본을 두고 하루 한 번 ETag 로 다시 확인합니다. 없으면 작은 그림을 흐린 바탕 위에 보여 줍니다.

## 11. 음원과 파일

| 항목 | 규칙 |
|---|---|
| 음원 | MP3(`audio/mpeg`) 또는 M4A/AAC(`audio/mp4`), 15MB 이하, 30초~15분 |
| `duration` | 실제 길이와 같게(정수 초) |
| 곡 이미지 | 100×100 WebP, 50KB 이하 |
| 앨범 커버 | 200×200 WebP, 100KB 이하 |
| 프로필 | 200×200 WebP, 100KB 이하, 진짜 WebP가 아니면 무시 |
| 가사 | `.srt`, UTF-8, 100KB 이하 |
| 공개곡 | 사이트당 1,000곡 이하 |

## 12. 호출 제한과 안전

| 대상 | 제한 | 넘으면 |
|---|---|---|
| `POST /sites/auto` | 같은 url 10분에 1회, aimusiclist 전체 시간당 새 사이트 200개 | `429` |
| `POST /ping` | 사이트당 분당 6회, 시간당 60회 | `429` |
| `POST /sites/{id}/url` | 사이트당 시간당 5회 | `429` |
| `GET /sites/{id}/stats` | 사이트당 하루 24회(304 포함) | `429` |
| `POST /sites/{id}/close` | 사이트당 시간 10회 | `429` |
| aimusiclist → 내 사이트 | 사이트당 10초에 1번까지 | 대기열에서 기다림 |
| 공개 GET API | IP당 분당 60회 | `429` |
| 검사 도구 | IP당 10분에 10회 | `429` |

aimusiclist가 내 사이트를 가져가는 방식: HTTPS만, 올바른 인증서, 리다이렉트 안 따라감, 공개 IP만, 1MB 이하, 시간 제한 3/5/10초, `User-Agent: aimusiclist-fetch/1.0 (+https://aimusiclist.com)`. 이 이름을 막지 마세요(`/.well-known/aimusiclist/*`에는 봇 확인 화면 끄기).

**응답 없음**: 정기 가져가기가 실패하면 5분, 30분 뒤 다시 시도합니다. 3회차 연속 실패(약 12시간)하면 사이트가 `hidden`(곡이 사라짐), 다음에 성공하면 바로 다시 보입니다. 30일 동안 실패하면 `contact_email`로 알립니다.

## 13. 권리·신고·차단

- **배포할 권리가 있는 곡만** 공개하세요(내가 만들었고, 쓴 AI 도구의 약관이 공개 배포를 허락함). 남의 곡·커버곡을 올리면 안 됩니다.
- 로그인한 aimusiclist 회원은 곡을 신고할 수 있습니다. 서로 다른 신고자 3명(가입 24시간 지난 계정)이면 관리자가 판단할 때까지(3일 안) 숨깁니다.
- 차단된 곡은 앞으로 가져가기에서 계속 무시됩니다. 도용 판정 3회 → 사이트 전체 `suspended`.
- 쓰레기 사이트(깨진 파일, 무음, 잡음, 음악이 아닌 것, 스팸, 속이는 정보)는 바로 `suspended`될 수 있습니다.
- 차단된 사이트는 다시 등록해도 풀리지 않습니다. 이의 제기는 `contact_email`로 받은 메일에 답장합니다.

## 14. 순위가 매겨지는 방식 (순위 산정 방식 v2.0 요약)

- **aimusiclist.com에서 재생된 것**만 셉니다. 내 사이트에서 들은 것은 세지 않습니다. 좋아요(♥)와 추가 정보는 **순위에 쓰지 않습니다**.
- 유효 재생 1회 = **실제로 30초 이상 들음**(건너뛴 시간 제외), 서버가 서명한 재생 시작표로 확인.
- 반복 재생이 아니라 **서로 다른 청취자**를 셉니다: 같은 청취자(회원 또는 기기)는 **같은 곡을 24시간에 1회**, 하루 최대 50곡까지. 주간 차트에서 한 사람이 한 곡에 줄 수 있는 점수는 **최대 3일분**. 아티스트 본인도 청취자 1명입니다.
- 가중치: 이메일 인증 회원 1.0, 그 밖의 회원·비회원 0.5, 가입 24시간 미만 ×0.5, 위치를 알 수 없는 재생 ×0.5, 회원 플레이리스트 담기 2.0(회원·곡마다 평생 1회).
- **나라 균형**(펜로즈 제곱근 방식): 청취자는 한 기간에 한 본국에만 속하고, 글로벌 점수는 나라마다 점수를 그 나라 활동 청취자 수의 제곱근으로 나눠 더합니다. 청취자가 4배 많은 나라는 2배의 영향력. 청취자가 아주 적은 나라들은 한 지역으로 묶습니다.
- 차트: **Global Top 100**, **나라별 Top 100**(그 나라 청취자가 충분할 때), **언어별 Top 100**(곡 주 언어), **장르별 Top 100**(장르 화면, 기본 정렬 "인기순"), **Rising 50**(처음 확인 28일 이내 곡).
- 기간(UTC): **주간 = 금요일 00:00 ~ 목요일 24:00**(대표 차트, 매주 금요일 00:30 UTC = 한국시간 09:30 확정), 일간 = UTC 하루, 매일 00:30 UTC 계산. 실시간 차트는 없습니다.
- 차트에 들어가려면 서로 다른 청취자가 일정 수 이상이어야 합니다(숫자는 비공개). 오래 머문 곡은 내려옵니다: 20주 뒤 50위 밖, 26주 뒤 25위 밖, 52주 뒤 10위 밖, 78주 뒤 5위 밖(주간 Global·나라·장르).
- 순위 옆 변동: ▲n / ▼n / – / `NEW`(이 차트 처음) / `RE`(빠졌다가 다시). 차트의 모든 곡에 청취자 수·청취 나라 수를 보여 줍니다.
- 이상한 곡은 **보류**하고 검토합니다: 아주 적은 네트워크에 몰린 재생, 새 계정 과다, 다른 곡은 듣지 않는 청취자 무리, 관련 없는 한 나라에서 갑자기 몰림.
- 지금 catalog에 있고 사이트가 `active`인 곡만 보여 줍니다(catalog에서 빼면 순위에서도 바로 빠짐).
- **조작 금지**(봇, 재생 농장, 가짜 계정 여러 개, 돈 받고 재생, 자동 반복 재생, VPN으로 나라 바꾸기). 조작된 곡은 보류되고 사이트가 차단될 수 있습니다.
- 방법 공개: `https://aimusiclist.com/charts/methodology` · 규칙(한국어 원문): `https://aimusiclist.com/developers/aimusiclist-ranking.ko.md`

## 15. 흔한 실수

1. HMAC 키를 16진수 바이트로 바꿈, 또는 메시지에 `verify:`를 빠뜨림 → `err.site.verify_failed`. 비밀키 글자를 그대로 쓰세요.
2. 어디든 리다이렉트(http→https, `www`↔`www` 없음, 끝의 `/`, `/catalog.json` → `/catalog.json/`) → 응답 없음 처리. aimusiclist는 리다이렉트를 따라가지 않습니다.
3. `site.url`이 등록한 origin과 다름(`www`, 경로, `http`, 포트) → 목록 전체 거부.
4. 등록 뒤에도 `site_id`가 `null`이거나 옛 값 → 목록 전체 거부. `201` 응답의 id부터 저장하세요.
5. `audio_url`이 R2/CDN/S3 주소이거나 `?쿼리`가 붙음 → 곡 거부. 반드시 `{site.url}/a/{id}`.
6. `duration`이 소수나 글자 → 곡 거부. 정수로 반올림하세요.
7. 첫 장르에 표시 이름(`"R&B"`)이나 세부 장르(`"kpop"`) → 곡 거부.
8. 10곡 미만·30곡 초과 앨범, 또는 `tracks`에 없는 id → 앨범 거부.
9. `/a/`에 CORS 없음(비주얼라이저 안 움직임) 또는 `/l/`에 없음(가사 안 열림). `OPTIONS`도 잊지 마세요.
10. catalog.json 대신 HTML 화면이 나옴(SPA 대체 화면, 404 화면, 로그인 화면, 봇 확인) → 거부.
11. 비밀키가 HTML/JS, catalog.json, 공개 Git 저장소에 보임. 비밀키를 가진 사람은 ping과 주소 변경을 할 수 있습니다.
12. 여러 곡을 올리면서 파일마다 ping, 또는 반복해서 ping → `429`.
13. 곡 id를 바꿈(예: 파일 이름으로 만듦) → 새 곡으로 처리되어 순위 기록이 사라짐.
14. 비공개로 할 곡을 catalog.json에 그대로 둠 — 공개 여부는 catalog가 정합니다.
15. `/sites/auto`를 기다리는 동안 verify에 응답하지 못하는 서버(요청을 하나씩만 받는 개발용 서버) → 시간 초과.
16. (1.2) `site.socials`에 SNS **주소**(`"https://youtube.com/@me"`) → 무시. 키(`"youtube"`)를 보내고 주소는 내 사이트 `#/links` 화면에 보여 주세요.
17. (1.2) `langs`에 화면 언어 코드(`"pt-BR"`, `"zh-TW"`) → 무시. `pt`, `zh`를 쓰세요. `zxx`를 다른 코드와 함께 → `zxx`가 버려짐.
18. (1.3) 지어낸 `ext` 키·옵션 키, 또는 내 사이트 전용 칸을 `ext`에 넣음 → 무시. 전용 칸은 `local`(보내지 않음)에.
19. (1.3) 화면을 열 때마다·1분마다 fields.json을 받음 — 하루 1번 받아 저장해 두세요.
20. 방문자 화면 맨 아래에 `Powered by aimusiclist.com` 링크가 없음, 끄는 스위치를 만듦, 등록 뒤에도 링크가 `https://aimusiclist.com/` 그대로임(2a장).

## 16. 직접 확인하는 법

- 웹: **`https://aimusiclist.com/developers/check`** — 내 사이트 주소를 넣으세요. 실제 가져가기와 같은 검사로 catalog를 확인하고, verify(형식만, 비밀키 필요 없음)와 음원 1개(HEAD + CORS)를 시험합니다. 아무것도 저장하지 않고 등록도 하지 않습니다.
- API (같은 결과를 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}'
  ```
  응답: `{"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"}}, …]}`. 각 `code`의 설명과 고치는 법은 웹 화면에 나옵니다.
- HMAC은 6장의 시험 값으로 확인하세요.
- 내 catalog: `curl -i https://your.site/.well-known/aimusiclist/catalog.json` 결과가 `HTTP/… 200`과 `content-type: application/json`으로 시작해야 합니다.

## 17. 변경 기록

| 버전 | 날짜 | 바뀐 점 |
|---|---|---|
| v1.1 | 2026-10-28 | 첫 공개 규약집. aimusiclist.com에 실제 구현된 연동규약 v1.1(자동 등록, verify `?c=`, `thumb_url`·`lyrics_url`·`profile_url`이 있는 catalog 1.1, 주소 변경, 기본 이미지)과 순위 v1.2 기준 |
| v1.2 | 2026-10-29 | catalog 1.2: `tracks[].langs`(곡 언어, `song-langs.json`), `site.socials`(SNS 종류 키만, SNS 모음 화면 `#/links/{key}`). 순위 v2.0 요약(나라 균형, 나라·언어·장르·Rising 차트, UTC 금~목 주간, NEW/RE) |
| v1.3 | 2026-10-29 | catalog 1.3: `tracks[].ext`·`site.ext`와 `fields.json`(첫 항목 `mood`), `local`은 보내지 않음, `versions.json`, 직접 만든 업로드 화면의 경계. 고정 주소는 그대로 |
| v1.3 (추가) | 2026-10-29 | 5a 허브 운영자용 절: 머리글 `X-AML-Hub`·`X-AML-Hub-Key`, 허브 시간당 한도, 키가 틀리면 일반 등록, verify 는 사이트마다, 응답의 `"hub"`. catalog 변화 없음, 일반 아티스트는 바뀌는 것 없음 |
| v1.3 (추가 2) | 2026-10-29 | 3d 절: `versions.json`의 `personal_site.php` = `{"latest","zip","sha256"}` (PHP판 [지금 업데이트], zip은 `https://aimusiclist.com/dl/personal-site/`에서만). 필드 추가만, 설정이 없으면 칸을 뺌 |
| v1.3 (추가 3) | 2026-10-29 | 8a 절: `GET /api/v1/sites/{site_id}/stats` (내 곡 통계, `site-stats-1`, Bearer 비밀키, 하루 1번, ETag/304, 하루 24회). 새 주소만 추가, catalog 변화 없음 |
| v1.3 (추가 4) | 2026-10-29 | 8b 절: `POST /api/v1/sites/{site_id}/close` (사이트가 지워짐·닫힘을 알림 → 바로 removed, Bearer 비밀키, 반복 안전, 시간 10회). 새 주소만 추가, catalog 변화 없음 |
| v1.3 (추가 7) | 2026-10-30 | 4·10장: 선택 필드 `tracks[].thumb_lg_url` = `{site.url}/i/{id}?s=l` (차트 1~3위 큰 카드용 큰 곡 그림). schema 는 1.3 그대로, 옛 사이트·옛 aimusiclist 는 무시 |
| v1.3 (추가 6) | 2026-10-30 | 8c 절: 하루 1번 공개 파일 `/feed/insights.json`(`insights-1`, 관리 화면용 차트 분석)·`/feed/news.json`(`news-1`, aimusiclist 소식과 그래프 자료). 하루 1번 ETag/304. 새 파일만 추가, catalog 변화 없음 |
| v1.3 (추가 8) | 2026-10-31 | 2a장(반드시): 방문자 화면마다 맨 아래 작은 `Powered by aimusiclist.com` 링크 → 등록 뒤 `https://aimusiclist.com/artist/{site_id}`, 전 `https://aimusiclist.com/`. 끄는 설정 없음. 0장 요청문·자기 점검에 추가. catalog·API 변화 없음 |
