Tài liệu API XuniBOX
API chỉ đọc để lấy danh sách nhạc, nghệ sĩ và thể loại từ kho XuniBOX. Mỗi chủ đề dưới đây là một trang riêng, có đường dẫn riêng để bạn chia sẻ cho đội kỹ thuật.
Thư viện client theo ngôn ngữ
XuniBOX không phát hành package trên npm hay Packagist — API đủ đơn giản để bạn giữ toàn quyền kiểm soát bằng một file duy nhất. Dưới đây là client tối giản nhưng đầy đủ cho từng nền tảng; copy vào dự án rồi sửa theo nhu cầu.
Các ví dụ server-side (TypeScript, Python, PHP, Go, .NET) gắn
X-API-Key trực tiếp. Các ví dụ mobile (Kotlin, Swift) trỏ về backend proxy của bạn vì app phát hành ra cửa hàng có thể bị dịch ngược để lấy key.TypeScript / Node.js
Chỉ dùng fetch chuẩn nên chạy được trên Node 18+, Bun, Deno, Cloudflare Workers và Vercel Edge mà không cần cài thêm gì.
TypeScript — xunibox.ts
// xunibox.ts — client gọn cho Node 18+, Deno, Bun, Cloudflare Workers.
export type XuniBoxTrack = {
id: string; title: string; artist: string; artist_id: string | null;
genre: string | null; duration: string;
audio_url: string; cover_url: string | null;
preview_duration_seconds: number; media_expires_in: number;
play_count: number;
};
export type Paged<T> = { data: T[]; meta: { total: number; page: number; limit: number; total_pages: number } };
export class XuniBox {
constructor(
private readonly apiKey: string,
private readonly base = "https://xunibox.vn/api/public",
) {}
private async get<T>(path: string, query: Record<string, string | number | undefined> = {}): Promise<T> {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value !== undefined && value !== "") search.set(key, String(value));
}
const url = `${this.base}${path}${search.size ? `?${search}` : ""}`;
const res = await fetch(url, { headers: { "X-API-Key": this.apiKey, Accept: "application/json" } });
if (!res.ok) throw new XuniBoxError(res.status, await res.text());
return res.json() as Promise<T>;
}
tracks(query: { q?: string; genre?: string; artist_id?: string; sort?: string; page?: number; limit?: number } = {}) {
return this.get<Paged<XuniBoxTrack>>("/tracks", query);
}
track(id: string) { return this.get<XuniBoxTrack>(`/tracks/${id}`); }
audioUrl(id: string) { return this.get<{ audio_url: string; expires_at: string }>(`/tracks/${id}/audio-url`); }
waveform(id: string, points = 120) { return this.get(`/tracks/${id}/waveform`, { points }); }
similar(id: string, limit = 10) { return this.get(`/tracks/${id}/similar`, { limit }); }
search(q: string, limit = 20) { return this.get("/search", { q, limit }); }
suggest(q: string, limit = 8) { return this.get("/search/suggest", { q, limit }); }
trending(days = 7, limit = 20) { return this.get("/trending", { days, limit }); }
genres() { return this.get("/genres"); }
artists(q?: string) { return this.get("/artists", { q }); }
usage(days = 30) { return this.get("/me/usage", { days }); }
}
export class XuniBoxError extends Error {
constructor(readonly status: number, readonly body: string) {
super(`XuniBOX API ${status}: ${body}`);
}
}Python
Python — xunibox.py
# xunibox.py — client đồng bộ dùng requests
import os
from dataclasses import dataclass
from typing import Any
import requests
@dataclass
class XuniBox:
api_key: str = os.environ.get("XUNIBOX_API_KEY", "")
base: str = "https://xunibox.vn/api/public"
def __post_init__(self) -> None:
self._session = requests.Session()
self._session.headers.update({"X-API-Key": self.api_key, "Accept": "application/json"})
def _get(self, path: str, **params: Any) -> dict:
params = {k: v for k, v in params.items() if v is not None}
res = self._session.get(f"{self.base}{path}", params=params, timeout=10)
res.raise_for_status()
return res.json()
def tracks(self, **q): return self._get("/tracks", **q)
def track(self, track_id: str): return self._get(f"/tracks/{track_id}")
def audio_url(self, track_id): return self._get(f"/tracks/{track_id}/audio-url")
def search(self, q, limit=20): return self._get("/search", q=q, limit=limit)
def trending(self, days=7): return self._get("/trending", days=days)
def genres(self): return self._get("/genres")
def iter_all_tracks(self, **q):
"""Duyệt hết kho nhạc, tự phân trang."""
page = 1
while True:
payload = self.tracks(page=page, limit=100, **q)
yield from payload["data"]
if page >= payload["meta"]["total_pages"]:
break
page += 1
if __name__ == "__main__":
client = XuniBox()
for track in client.iter_all_tracks(genre="Indie"):
print(track["id"], track["title"], track["artist"])PHP / Laravel
PHP — App\\Services\\XuniBox
<?php
// app/Services/XuniBox.php — Laravel service, đăng ký vào container.
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class XuniBox
{
private const BASE = 'https://xunibox.vn/api/public';
private function client()
{
return Http::withHeaders(['X-API-Key' => config('services.xunibox.key')])
->acceptJson()
->timeout(10)
->retry(3, 500);
}
/** Danh sách bài hát, cache 5 phút để tiết kiệm hạn mức. */
public function tracks(array $query = []): array
{
$cacheKey = 'xunibox:tracks:' . md5(json_encode($query));
return Cache::remember($cacheKey, now()->addMinutes(5), function () use ($query) {
return $this->client()->get(self::BASE . '/tracks', $query)->throw()->json();
});
}
/** Link nghe thử — KHÔNG cache, chỉ sống 1 giờ. */
public function audioUrl(string $trackId): array
{
return $this->client()->get(self::BASE . "/tracks/{$trackId}/audio-url")->throw()->json();
}
public function search(string $q, int $limit = 20): array
{
return $this->client()->get(self::BASE . '/search', compact('q', 'limit'))->throw()->json();
}
}Go
Go — xunibox.go
// xunibox.go
package xunibox
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type Client struct {
APIKey string
Base string
HTTP *http.Client
}
func New(apiKey string) *Client {
return &Client{
APIKey: apiKey,
Base: "https://xunibox.vn/api/public",
HTTP: &http.Client{Timeout: 10 * time.Second},
}
}
type Track struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Genre string `json:"genre"`
Duration string `json:"duration"`
AudioURL string `json:"audio_url"`
}
type TracksResponse struct {
Data []Track `json:"data"`
Meta struct {
Total int `json:"total"`
Page int `json:"page"`
TotalPages int `json:"total_pages"`
} `json:"meta"`
}
func (c *Client) Tracks(ctx context.Context, query url.Values) (*TracksResponse, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.Base+"/tracks?"+query.Encode(), nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", c.APIKey)
req.Header.Set("Accept", "application/json")
res, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("xunibox: HTTP %d (còn %s request trong phút này)",
res.StatusCode, res.Header.Get("X-RateLimit-Remaining"))
}
var out TracksResponse
return &out, json.NewDecoder(res.Body).Decode(&out)
}Kotlin / Android
Kotlin — Retrofit
// XuniBoxApi.kt — Android / Kotlin với Retrofit.
// LƯU Ý: app mobile KHÔNG được chứa API key. Trỏ BASE_URL về backend proxy của bạn.
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
data class Track(
val id: String,
val title: String,
val artist: String,
val genre: String?,
val duration: String,
val cover_url: String?,
)
data class Meta(val total: Int, val page: Int, val limit: Int, val total_pages: Int)
data class TracksResponse(val data: List<Track>, val meta: Meta)
data class AudioUrl(val audio_url: String, val media_expires_in: Int, val expires_at: String)
interface XuniBoxApi {
@GET("tracks")
suspend fun tracks(
@Query("q") q: String? = null,
@Query("genre") genre: String? = null,
@Query("page") page: Int = 1,
@Query("limit") limit: Int = 20,
): TracksResponse
@GET("tracks/{id}/audio-url")
suspend fun audioUrl(@Path("id") id: String): AudioUrl
}
val api: XuniBoxApi = Retrofit.Builder()
.baseUrl("https://your-app.com/api/xunibox/") // proxy của bạn
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(XuniBoxApi::class.java)Swift / iOS
Swift — async/await
// XuniBox.swift — iOS / macOS, async/await.
// App iOS cũng không nên chứa API key: gọi qua backend proxy của bạn.
import Foundation
struct Track: Decodable {
let id: String
let title: String
let artist: String
let genre: String?
let duration: String
let coverUrl: String?
enum CodingKeys: String, CodingKey {
case id, title, artist, genre, duration
case coverUrl = "cover_url"
}
}
struct Meta: Decodable { let total, page, limit, totalPages: Int
enum CodingKeys: String, CodingKey { case total, page, limit, totalPages = "total_pages" } }
struct TracksResponse: Decodable { let data: [Track]; let meta: Meta }
enum XuniBox {
static let base = URL(string: "https://your-app.com/api/xunibox")!
static func tracks(query: String? = nil, page: Int = 1) async throws -> TracksResponse {
var components = URLComponents(url: base.appendingPathComponent("tracks"), resolvingAgainstBaseURL: false)!
components.queryItems = [
query.map { URLQueryItem(name: "q", value: $0) },
URLQueryItem(name: "page", value: String(page)),
].compactMap { $0 }
let (data, response) = try await URLSession.shared.data(from: components.url!)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(TracksResponse.self, from: data)
}
}C# / .NET
C# — IHttpClientFactory
// XuniBoxClient.cs — .NET 8, đăng ký bằng IHttpClientFactory.
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public sealed record Track(
string Id,
string Title,
string Artist,
string? Genre,
string Duration,
[property: JsonPropertyName("cover_url")] string? CoverUrl);
public sealed record Meta(int Total, int Page, int Limit, [property: JsonPropertyName("total_pages")] int TotalPages);
public sealed record TracksResponse(IReadOnlyList<Track> Data, Meta Meta);
public sealed class XuniBoxClient(HttpClient http)
{
public async Task<TracksResponse> TracksAsync(string? q = null, int page = 1, int limit = 20, CancellationToken ct = default)
{
var url = $"tracks?page={page}&limit={limit}" + (q is null ? "" : $"&q={Uri.EscapeDataString(q)}");
var response = await http.GetAsync(url, ct);
response.EnsureSuccessStatusCode();
return (await response.Content.ReadFromJsonAsync<TracksResponse>(cancellationToken: ct))!;
}
}
// Program.cs
builder.Services.AddHttpClient<XuniBoxClient>(client =>
{
client.BaseAddress = new Uri("https://xunibox.vn/api/public/");
client.DefaultRequestHeaders.Add("X-API-Key", builder.Configuration["XuniBox:ApiKey"]);
client.Timeout = TimeSpan.FromSeconds(10);
});Tự sinh SDK từ OpenAPI
Nếu bạn muốn client có kiểu dữ liệu đầy đủ và tự cập nhật theo phiên bản đặc tả, hãy sinh code từ file OpenAPI thay vì viết tay.
Sinh SDK từ đặc tả
# Tải đặc tả (không cần API key)
curl -sSL https://xunibox.vn/api/public/openapi.v1.json -o xunibox.openapi.v1.json
# Sinh SDK TypeScript từ đặc tả
npx openapi-typescript xunibox.openapi.v1.json -o xunibox-api.d.ts| Ngôn ngữ | Công cụ gợi ý | Lệnh |
|---|---|---|
| TypeScript (kiểu dữ liệu) | openapi-typescript | npx openapi-typescript xunibox.openapi.v1.json -o xunibox-api.d.ts |
| TypeScript (client đầy đủ) | openapi-generator | npx @openapitools/openapi-generator-cli generate -i xunibox.openapi.v1.json -g typescript-fetch -o ./xunibox-sdk |
| Python | openapi-generator | openapi-generator-cli generate -i xunibox.openapi.v1.json -g python -o ./xunibox_sdk |
| PHP | openapi-generator | openapi-generator-cli generate -i xunibox.openapi.v1.json -g php -o ./xunibox-sdk |
| Go | oapi-codegen | oapi-codegen -package xunibox xunibox.openapi.v1.json > xunibox.gen.go |
| Dart / Flutter | openapi-generator | openapi-generator-cli generate -i xunibox.openapi.v1.json -g dart-dio -o ./xunibox_sdk |
Mọi client dù viết tay hay tự sinh đều nên thoả ba điều kiện: timeout tối đa 10 giây, retry có backoff cho lỗi 5xx và 429, và không bao giờ lưu
audio_url vào cơ sở dữ liệu.Sẵn sàng tích hợp?
Gửi thông tin sản phẩm để admin XuniBOX xét duyệt và cấp API key.