{"openapi":"3.1.0","info":{"title":"XuniBOX Public Music API","version":"1.0.0","summary":"REST API chỉ đọc để lấy nhạc, nghệ sĩ và thể loại từ kho XuniBOX.","description":"API công khai của XuniBOX cho phép website và ứng dụng đối tác hiển thị kho nhạc để người dùng gắn vào video.\n\n- Mọi endpoint đều là `GET`, trả JSON UTF-8 và bật CORS cho mọi origin.\n- Bắt buộc gửi header `X-API-Key`, trừ `GET /healthz`, `GET /readyz` và `GET /openapi.json`. Thiếu key trả 401, key sai hoặc bị thu hồi trả 403.\n- Mỗi key có hạn mức request theo phút; vượt hạn mức trả 429 kèm `Retry-After`.\n- `audio_url`, `cover_url`, `avatar_url` là URL ký hạn **1 giờ** — hãy lưu `id` rồi xin URL mới khi cần dùng.\n- Phản hồi 200 có `ETag`; gửi lại qua `If-None-Match` để nhận 304 và tiết kiệm băng thông.","contact":{"name":"XuniBOX Developer Support","url":"https://xunibox.vn/developers"},"license":{"name":"XuniBOX API Terms","url":"https://xunibox.vn/terms"}},"servers":[{"url":"https://xunibox.vn/api/public","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"System","description":"Kiểm tra trạng thái và sức khỏe API."},{"name":"Tracks","description":"Danh sách và chi tiết bài nhạc."},{"name":"Artists","description":"Nghệ sĩ và nhạc theo nghệ sĩ."},{"name":"Metadata","description":"Thể loại và đặc tả API."}],"paths":{"/metrics":{"get":{"tags":["System"],"operationId":"getMetrics","summary":"Số liệu Prometheus","description":"Trả số liệu theo định dạng Prometheus text exposition (version 0.0.4) để hệ thống giám sát scrape trực tiếp. Không yêu cầu API key, không tính vào hạn mức, không cache.","security":[],"responses":{"200":{"description":"Số liệu hiện tại.","content":{"text/plain":{"schema":{"type":"string"},"example":"# HELP xunibox_up 1 nếu API phục vụ được dữ liệu, 0 nếu không.\n# TYPE xunibox_up gauge\nxunibox_up 1\n# HELP xunibox_tracks_total Tổng số bài nhạc trong kho.\n# TYPE xunibox_tracks_total gauge\nxunibox_tracks_total 128\n"}}},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"503":{"description":"Không kết nối được cơ sở dữ liệu; vẫn trả số liệu dạng Prometheus với xunibox_up 0.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/healthz":{"get":{"tags":["System"],"operationId":"getHealthz","summary":"Kiểm tra sức khỏe API","description":"Trả phiên bản API, trạng thái hệ thống và thời gian phản hồi. Endpoint này không yêu cầu API key.","security":[],"responses":{"200":{"description":"Hệ thống hoạt động bình thường.","content":{"application/json":{"schema":{"type":"object","required":["status","api_version","response_time_ms","timestamp","db_connected"],"properties":{"status":{"type":"string","enum":["ok","degraded"],"example":"ok"},"api_version":{"type":"string","example":"1.0.0"},"response_time_ms":{"type":"integer","example":12},"timestamp":{"type":"string","format":"date-time"},"db_connected":{"type":"boolean","example":true},"db_response_time_ms":{"type":"integer","example":8}}}}}},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"503":{"description":"Kết nối cơ sở dữ liệu gặp sự cố.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể kết nối cơ sở dữ liệu."}}}}}}},"/readyz":{"get":{"tags":["System"],"operationId":"getReadyz","summary":"Kiểm tra readiness (sẵn sàng nhận traffic)","description":"Cho biết API đã sẵn sàng phục vụ traffic hay chưa. Trả timestamp, thời gian phản hồi và danh sách check chi tiết; khi chưa sẵn sàng sẽ có thêm `reason` và `reasons` mô tả lý do. Không yêu cầu API key. Hỗ trợ cả HEAD cho probe của Kubernetes/load balancer.","security":[],"responses":{"200":{"description":"Hệ thống sẵn sàng nhận traffic.","content":{"application/json":{"schema":{"type":"object","required":["status","ready","api_version","timestamp","response_time_ms","checks"],"properties":{"status":{"type":"string","enum":["ready","not_ready"],"example":"ready"},"ready":{"type":"boolean","example":true},"api_version":{"type":"string","example":"1.0.0"},"timestamp":{"type":"string","format":"date-time"},"response_time_ms":{"type":"integer","example":24},"checks":{"type":"array","items":{"type":"object","required":["name","ready","duration_ms","reason"],"properties":{"name":{"type":"string","example":"database_read"},"ready":{"type":"boolean","example":true},"duration_ms":{"type":"integer","example":12},"reason":{"type":"string","nullable":true,"example":null}}}},"reason":{"type":"string","nullable":true,"description":"Lý do đầu tiên khiến hệ thống chưa sẵn sàng."},"reasons":{"type":"array","items":{"type":"string"}}}}}}},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"503":{"description":"Hệ thống chưa sẵn sàng nhận traffic; body chứa reason và danh sách check thất bại.","content":{"application/json":{"schema":{"type":"object"},"example":{"status":"not_ready","ready":false,"api_version":"1.0.0","timestamp":"2026-08-05T15:08:08.988Z","response_time_ms":673,"checks":[{"name":"backend_config","ready":true,"duration_ms":0,"reason":null},{"name":"database_read","ready":false,"duration_ms":512,"reason":"Cơ sở dữ liệu trả về HTTP 500 khi đọc thử bảng tracks."}],"reason":"Cơ sở dữ liệu trả về HTTP 500 khi đọc thử bảng tracks.","reasons":["Cơ sở dữ liệu trả về HTTP 500 khi đọc thử bảng tracks."]}}}}}}},"/status":{"get":{"tags":["System"],"operationId":"getStatus","summary":"Chỉ số vận hành và cấu hình rate limit","description":"Trả trạng thái hệ thống, quy mô kho nhạc, hạn mức mặc định của API key và các chính sách xác thực, cache, phân trang, CORS cùng danh sách phiên bản đặc tả. Không yêu cầu API key.","security":[],"responses":{"200":{"description":"Hệ thống hoạt động bình thường.","content":{"application/json":{"schema":{"type":"object","required":["status","api_version","timestamp","database","rate_limit","policies"],"properties":{"status":{"type":"string","enum":["operational","degraded"],"example":"operational"},"api_version":{"type":"string","example":"1.0.0"},"latest_spec_version":{"type":"string","example":"1.0.0"},"timestamp":{"type":"string","format":"date-time"},"response_time_ms":{"type":"integer","example":24},"database":{"type":"object","properties":{"connected":{"type":"boolean"},"response_time_ms":{"type":"integer"}}},"catalog":{"type":"object","properties":{"tracks":{"type":["integer","null"],"example":428},"artists":{"type":["integer","null"],"example":63}}},"rate_limit":{"type":"object","properties":{"scope":{"type":"string"},"default_requests_per_minute":{"type":"integer","example":120},"exceeded_status":{"type":"integer","example":429},"exceeded_code":{"type":"string","example":"RATE_LIMIT_EXCEEDED"},"retry_after_header":{"type":"string"},"response_headers":{"type":"array","items":{"type":"string"}}}},"policies":{"type":"object","description":"Chính sách xác thực, cache, phân trang, CORS và danh sách phiên bản đặc tả."}}}}}},"503":{"description":"Hệ thống đang suy giảm; payload vẫn kèm cấu hình rate limit và chính sách.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/tracks":{"get":{"tags":["Tracks"],"operationId":"listTracks","summary":"Danh sách bài nhạc","description":"Trả danh sách bài nhạc có phân trang, lọc và tìm kiếm theo tiêu đề, nghệ sĩ, thể loại.","parameters":[{"name":"q","in":"query","required":false,"description":"Tìm đồng thời trong title, artist và genre (tối đa 100 ký tự).","schema":{"type":"string","maxLength":100},"example":"summer"},{"name":"title","in":"query","required":false,"description":"Tìm một phần tên bài hát.","schema":{"type":"string","maxLength":160}},{"name":"artist","in":"query","required":false,"description":"Tìm một phần tên nghệ sĩ.","schema":{"type":"string","maxLength":120}},{"name":"genre","in":"query","required":false,"description":"Tìm một phần thể loại.","schema":{"type":"string","maxLength":80},"example":"Pop"},{"name":"artist_id","in":"query","required":false,"description":"Lọc chính xác theo UUID nghệ sĩ.","schema":{"type":"string","format":"uuid"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["newest","oldest","popular","title_asc","title_desc"],"default":"newest"}},{"name":"page","in":"query","required":false,"description":"Trang hiện tại, số nguyên 1–5000.","schema":{"type":"integer","minimum":1,"maximum":5000,"default":1}},{"name":"limit","in":"query","required":false,"description":"Số bản ghi mỗi trang, 1–100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"If-None-Match","in":"header","required":false,"description":"Gửi lại ETag của lần gọi trước để nhận 304 Not Modified khi dữ liệu chưa đổi.","schema":{"type":"string"}}],"responses":{"200":{"description":"Danh sách bài nhạc.","headers":{"ETag":{"description":"ETag yếu của payload, dùng cho If-None-Match.","schema":{"type":"string"}},"Cache-Control":{"description":"public, max-age=60, s-maxage=300, stale-while-revalidate=600","schema":{"type":"string"}},"X-Total-Count":{"description":"Tổng số bản ghi khớp bộ lọc.","schema":{"type":"integer"}},"X-Page":{"description":"Trang hiện tại.","schema":{"type":"integer"}},"X-Limit":{"description":"Số bản ghi mỗi trang.","schema":{"type":"integer"}},"X-Offset":{"description":"Vị trí bắt đầu của trang.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Hạn mức request mỗi phút của API key.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Số request còn lại trong cửa sổ hiện tại.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Thời điểm reset hạn mức (Unix epoch, giây).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Track"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"304":{"description":"Not Modified — payload không đổi so với ETag đã gửi."},"400":{"description":"Tham số truy vấn không hợp lệ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Tham số truy vấn không hợp lệ."}}}},"401":{"$ref":"#/components/responses/MissingApiKey"},"403":{"$ref":"#/components/responses/InvalidApiKey"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"502":{"description":"Lỗi khi truy vấn kho dữ liệu nội bộ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"API chưa được cấu hình hoặc tạm ngưng.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"XuniBOX API chưa được cấu hình."}}}}}}},"/tracks/{id}":{"get":{"tags":["Tracks"],"operationId":"getTrack","summary":"Chi tiết một bài nhạc","description":"Trả một bài nhạc theo UUID. Endpoint công khai này **không** tăng play_count.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"If-None-Match","in":"header","required":false,"description":"Gửi lại ETag của lần gọi trước để nhận 304 Not Modified khi dữ liệu chưa đổi.","schema":{"type":"string"}}],"responses":{"200":{"description":"Bài nhạc.","headers":{"ETag":{"description":"ETag yếu của payload, dùng cho If-None-Match.","schema":{"type":"string"}},"Cache-Control":{"description":"public, max-age=60, s-maxage=300, stale-while-revalidate=600","schema":{"type":"string"}},"X-Total-Count":{"description":"Tổng số bản ghi khớp bộ lọc.","schema":{"type":"integer"}},"X-Page":{"description":"Trang hiện tại.","schema":{"type":"integer"}},"X-Limit":{"description":"Số bản ghi mỗi trang.","schema":{"type":"integer"}},"X-Offset":{"description":"Vị trí bắt đầu của trang.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Hạn mức request mỗi phút của API key.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Số request còn lại trong cửa sổ hiện tại.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Thời điểm reset hạn mức (Unix epoch, giây).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Track"}}}},"304":{"description":"Not Modified — payload không đổi so với ETag đã gửi."},"400":{"description":"Tham số truy vấn không hợp lệ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Tham số truy vấn không hợp lệ."}}}},"401":{"$ref":"#/components/responses/MissingApiKey"},"403":{"$ref":"#/components/responses/InvalidApiKey"},"404":{"description":"Không tìm thấy bài hát.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không tìm thấy bài hát."}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"502":{"description":"Lỗi khi truy vấn kho dữ liệu nội bộ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"API chưa được cấu hình hoặc tạm ngưng.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"XuniBOX API chưa được cấu hình."}}}}}}},"/tracks/{id}/audio-url":{"get":{"tags":["Tracks"],"operationId":"refreshTrackAudioUrl","summary":"Làm mới link nghe thử của một bài nhạc","description":"Trả về link audio đã ký còn hạn cho đoạn nghe thử. Dùng endpoint này ngay trước khi phát thay vì lưu cứng `audio_url` cũ. Phản hồi luôn `Cache-Control: no-store`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Link nghe thử mới.","content":{"application/json":{"schema":{"type":"object","required":["id","audio_url","media_expires_in","expires_at"],"properties":{"id":{"type":"string","format":"uuid"},"audio_url":{"type":"string","format":"uri"},"cover_url":{"type":"string","format":"uri","nullable":true},"preview_duration_seconds":{"type":"integer","nullable":true},"media_expires_in":{"type":"integer","description":"Số giây còn hiệu lực."},"expires_at":{"type":"string","format":"date-time"}}},"example":{"id":"550e8400-e29b-41d4-a716-446655440000","audio_url":"https://.../audio/preview.mp3?token=...","cover_url":"https://.../covers/cover.webp?token=...","preview_duration_seconds":30,"media_expires_in":3600,"expires_at":"2026-08-05T13:08:00.000Z"}}}},"304":{"description":"Not Modified — payload không đổi so với ETag đã gửi."},"400":{"description":"Tham số truy vấn không hợp lệ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Tham số truy vấn không hợp lệ."}}}},"401":{"$ref":"#/components/responses/MissingApiKey"},"403":{"$ref":"#/components/responses/InvalidApiKey"},"404":{"description":"Không tìm thấy bài hát hoặc bài hát chưa có đoạn nghe thử.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không tìm thấy bài hát."}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"502":{"description":"Lỗi khi truy vấn kho dữ liệu nội bộ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"API chưa được cấu hình hoặc tạm ngưng.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"XuniBOX API chưa được cấu hình."}}}}}}},"/tracks/by-ids":{"get":{"tags":["Tracks"],"operationId":"getTracksByIds","summary":"Lấy nhiều bài nhạc theo danh sách ID","description":"Dùng để làm mới hàng loạt URL media đã hết hạn. Tối đa 50 UUID mỗi lần, phân tách bằng dấu phẩy.","parameters":[{"name":"ids","in":"query","required":true,"description":"Danh sách UUID phân tách bằng dấu phẩy, tối đa 50 phần tử.","schema":{"type":"string"},"example":"550e8400-e29b-41d4-a716-446655440000,7c9e6679-7425-40de-944b-e07fc1f90ae7"},{"name":"If-None-Match","in":"header","required":false,"description":"Gửi lại ETag của lần gọi trước để nhận 304 Not Modified khi dữ liệu chưa đổi.","schema":{"type":"string"}}],"responses":{"200":{"description":"Các bài nhạc tìm thấy (ID không tồn tại sẽ bị bỏ qua).","headers":{"ETag":{"description":"ETag yếu của payload, dùng cho If-None-Match.","schema":{"type":"string"}},"Cache-Control":{"description":"public, max-age=60, s-maxage=300, stale-while-revalidate=600","schema":{"type":"string"}},"X-Total-Count":{"description":"Tổng số bản ghi khớp bộ lọc.","schema":{"type":"integer"}},"X-Page":{"description":"Trang hiện tại.","schema":{"type":"integer"}},"X-Limit":{"description":"Số bản ghi mỗi trang.","schema":{"type":"integer"}},"X-Offset":{"description":"Vị trí bắt đầu của trang.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Hạn mức request mỗi phút của API key.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Số request còn lại trong cửa sổ hiện tại.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Thời điểm reset hạn mức (Unix epoch, giây).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Track"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"304":{"description":"Not Modified — payload không đổi so với ETag đã gửi."},"400":{"description":"Tham số truy vấn không hợp lệ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Tham số truy vấn không hợp lệ."}}}},"401":{"$ref":"#/components/responses/MissingApiKey"},"403":{"$ref":"#/components/responses/InvalidApiKey"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"502":{"description":"Lỗi khi truy vấn kho dữ liệu nội bộ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"API chưa được cấu hình hoặc tạm ngưng.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"XuniBOX API chưa được cấu hình."}}}}}}},"/artists":{"get":{"tags":["Artists"],"operationId":"listArtists","summary":"Danh sách nghệ sĩ","parameters":[{"name":"q","in":"query","required":false,"description":"Tìm theo tên nghệ sĩ.","schema":{"type":"string","maxLength":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["name_asc","name_desc","newest","oldest"],"default":"name_asc"}},{"name":"page","in":"query","required":false,"description":"Trang hiện tại, số nguyên 1–5000.","schema":{"type":"integer","minimum":1,"maximum":5000,"default":1}},{"name":"limit","in":"query","required":false,"description":"Số bản ghi mỗi trang, 1–100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"If-None-Match","in":"header","required":false,"description":"Gửi lại ETag của lần gọi trước để nhận 304 Not Modified khi dữ liệu chưa đổi.","schema":{"type":"string"}}],"responses":{"200":{"description":"Danh sách nghệ sĩ.","headers":{"ETag":{"description":"ETag yếu của payload, dùng cho If-None-Match.","schema":{"type":"string"}},"Cache-Control":{"description":"public, max-age=60, s-maxage=300, stale-while-revalidate=600","schema":{"type":"string"}},"X-Total-Count":{"description":"Tổng số bản ghi khớp bộ lọc.","schema":{"type":"integer"}},"X-Page":{"description":"Trang hiện tại.","schema":{"type":"integer"}},"X-Limit":{"description":"Số bản ghi mỗi trang.","schema":{"type":"integer"}},"X-Offset":{"description":"Vị trí bắt đầu của trang.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Hạn mức request mỗi phút của API key.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Số request còn lại trong cửa sổ hiện tại.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Thời điểm reset hạn mức (Unix epoch, giây).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Artist"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"304":{"description":"Not Modified — payload không đổi so với ETag đã gửi."},"400":{"description":"Tham số truy vấn không hợp lệ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Tham số truy vấn không hợp lệ."}}}},"401":{"$ref":"#/components/responses/MissingApiKey"},"403":{"$ref":"#/components/responses/InvalidApiKey"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"502":{"description":"Lỗi khi truy vấn kho dữ liệu nội bộ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"API chưa được cấu hình hoặc tạm ngưng.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"XuniBOX API chưa được cấu hình."}}}}}}},"/artists/{id}/tracks":{"get":{"tags":["Artists"],"operationId":"listArtistTracks","summary":"Nhạc theo nghệ sĩ","description":"Trả thông tin nghệ sĩ kèm danh sách bài nhạc có phân trang.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["newest","oldest","popular","title_asc","title_desc"],"default":"newest"}},{"name":"page","in":"query","required":false,"description":"Trang hiện tại, số nguyên 1–5000.","schema":{"type":"integer","minimum":1,"maximum":5000,"default":1}},{"name":"limit","in":"query","required":false,"description":"Số bản ghi mỗi trang, 1–100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"If-None-Match","in":"header","required":false,"description":"Gửi lại ETag của lần gọi trước để nhận 304 Not Modified khi dữ liệu chưa đổi.","schema":{"type":"string"}}],"responses":{"200":{"description":"Nghệ sĩ và danh sách bài nhạc.","headers":{"ETag":{"description":"ETag yếu của payload, dùng cho If-None-Match.","schema":{"type":"string"}},"Cache-Control":{"description":"public, max-age=60, s-maxage=300, stale-while-revalidate=600","schema":{"type":"string"}},"X-Total-Count":{"description":"Tổng số bản ghi khớp bộ lọc.","schema":{"type":"integer"}},"X-Page":{"description":"Trang hiện tại.","schema":{"type":"integer"}},"X-Limit":{"description":"Số bản ghi mỗi trang.","schema":{"type":"integer"}},"X-Offset":{"description":"Vị trí bắt đầu của trang.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Hạn mức request mỗi phút của API key.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Số request còn lại trong cửa sổ hiện tại.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Thời điểm reset hạn mức (Unix epoch, giây).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["artist","data","meta"],"properties":{"artist":{"$ref":"#/components/schemas/Artist"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Track"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"304":{"description":"Not Modified — payload không đổi so với ETag đã gửi."},"400":{"description":"Tham số truy vấn không hợp lệ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Tham số truy vấn không hợp lệ."}}}},"401":{"$ref":"#/components/responses/MissingApiKey"},"403":{"$ref":"#/components/responses/InvalidApiKey"},"404":{"description":"Không tìm thấy nghệ sĩ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không tìm thấy nghệ sĩ."}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"502":{"description":"Lỗi khi truy vấn kho dữ liệu nội bộ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"API chưa được cấu hình hoặc tạm ngưng.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"XuniBOX API chưa được cấu hình."}}}}}}},"/genres":{"get":{"tags":["Metadata"],"operationId":"listGenres","summary":"Danh sách thể loại đang dùng","description":"Trả các thể loại có ít nhất một bài nhạc, dùng để dựng bộ lọc.","parameters":[{"name":"page","in":"query","required":false,"description":"Trang hiện tại, số nguyên 1–5000.","schema":{"type":"integer","minimum":1,"maximum":5000,"default":1}},{"name":"limit","in":"query","required":false,"description":"Số bản ghi mỗi trang, 1–100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"If-None-Match","in":"header","required":false,"description":"Gửi lại ETag của lần gọi trước để nhận 304 Not Modified khi dữ liệu chưa đổi.","schema":{"type":"string"}}],"responses":{"200":{"description":"Danh sách thể loại.","headers":{"ETag":{"description":"ETag yếu của payload, dùng cho If-None-Match.","schema":{"type":"string"}},"Cache-Control":{"description":"public, max-age=60, s-maxage=300, stale-while-revalidate=600","schema":{"type":"string"}},"X-Total-Count":{"description":"Tổng số bản ghi khớp bộ lọc.","schema":{"type":"integer"}},"X-Page":{"description":"Trang hiện tại.","schema":{"type":"integer"}},"X-Limit":{"description":"Số bản ghi mỗi trang.","schema":{"type":"integer"}},"X-Offset":{"description":"Vị trí bắt đầu của trang.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Hạn mức request mỗi phút của API key.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Số request còn lại trong cửa sổ hiện tại.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Thời điểm reset hạn mức (Unix epoch, giây).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"type":"string"},"example":["Pop","Ballad","EDM"]},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"304":{"description":"Not Modified — payload không đổi so với ETag đã gửi."},"400":{"description":"Tham số truy vấn không hợp lệ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Tham số truy vấn không hợp lệ."}}}},"401":{"$ref":"#/components/responses/MissingApiKey"},"403":{"$ref":"#/components/responses/InvalidApiKey"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Lỗi máy chủ XuniBOX.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Không thể tải kho nhạc."}}}},"502":{"description":"Lỗi khi truy vấn kho dữ liệu nội bộ.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"API chưa được cấu hình hoặc tạm ngưng.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"XuniBOX API chưa được cấu hình."}}}}}}},"/openapi.json":{"get":{"tags":["Metadata"],"operationId":"getOpenApiSpec","summary":"Tải đặc tả OpenAPI (mới nhất)","description":"Bí danh luôn trỏ tới phiên bản mới nhất. Không yêu cầu API key. Khi tích hợp production nên dùng URL có số phiên bản, ví dụ `/openapi.v1.json`.","security":[],"responses":{"200":{"description":"Đặc tả OpenAPI 3.1 dạng JSON.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/openapi.v1.json":{"get":{"tags":["Metadata"],"operationId":"getOpenApiSpecV1","summary":"Tải đặc tả OpenAPI phiên bản v1","description":"Đặc tả cố định cho phiên bản v1. Không yêu cầu API key. Phản hồi kèm header `X-API-Spec-Version`.","security":[],"responses":{"200":{"description":"Đặc tả OpenAPI 3.1 dạng JSON của v1.","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Khóa đối tác dạng xnb_live_… do quản trị viên XuniBOX cấp. Chỉ gửi từ máy chủ, không nhúng vào frontend."}},"schemas":{"Track":{"type":"object","required":["id","title","artist","duration","audio_url","play_count","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string","example":"Tên bài hát"},"artist":{"type":"string","example":"Tên nghệ sĩ"},"genre":{"type":["string","null"],"example":"Pop"},"duration":{"type":"string","description":"Độ dài bản gốc dạng mm:ss.","example":"3:24"},"audio_url":{"type":"string","format":"uri","description":"URL ký hạn 1 giờ, trỏ tới đoạn preview đã được duyệt (15/30/60 giây)."},"preview_start_seconds":{"type":"integer","example":42},"preview_duration_seconds":{"type":"integer","enum":[15,30,60],"example":30},"cover_url":{"type":["string","null"],"format":"uri","description":"Ảnh bìa ký hạn 1 giờ."},"play_count":{"type":"integer","example":1240},"created_at":{"type":"string","format":"date-time"}}},"Artist":{"type":"object","required":["id","name","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Tên nghệ sĩ"},"biography":{"type":["string","null"]},"avatar_url":{"type":["string","null"],"format":"uri","description":"Ảnh đại diện ký hạn 1 giờ."},"track_count":{"type":"integer","example":12},"created_at":{"type":"string","format":"date-time"}}},"PaginationMeta":{"type":"object","required":["total","page","limit"],"properties":{"total":{"type":"integer","description":"Tổng số bản ghi khớp bộ lọc."},"page":{"type":"integer"},"limit":{"type":"integer"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Mô tả lỗi bằng tiếng Việt."},"code":{"type":"string","description":"Mã lỗi máy đọc được (chỉ có ở một số lỗi, ví dụ RATE_LIMIT_EXCEEDED)."}}}},"responses":{"MissingApiKey":{"description":"Thiếu header X-API-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Thiếu API key. Gửi khóa qua header X-API-Key."}}}},"InvalidApiKey":{"description":"API key không hợp lệ hoặc đã bị thu hồi.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"API key không hợp lệ hoặc đã bị vô hiệu hóa."}}}},"RateLimited":{"description":"Vượt hạn mức request theo phút của API key.","headers":{"Retry-After":{"description":"Số giây nên chờ trước khi thử lại.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Hạn mức request mỗi phút của API key.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Số request còn lại trong cửa sổ hiện tại.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Thời điểm reset hạn mức (Unix epoch, giây).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Đã vượt quá giới hạn request của API key.","code":"RATE_LIMIT_EXCEEDED","limit":120,"retry_after_seconds":37,"reset_at":"2026-08-05T09:31:00.000Z"}}}}}}}