// ============================================================ // LUXSIN X9 API — Client Library // Custom Base64 decode + HTTP API wrappers // ============================================================ const ALPHABET_CUSTOM = "KLMPQRSTUVWXYZABCGHdefIJjkNOlmnopqrstuvwxyzabcghiDEF34501289+67/"; const ALPHABET_STANDARD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // ============================================================ // Volume Conversion: dB (-100 to 0) <-> Device Value (0 to 200) // ============================================================ export function dbToVolume(db: number): number { // Formula: volume = 2 * db + 200 // e.g., -56dB -> 2 * (-56) + 200 = 88 return Math.round(2 * db + 200); } export function volumeToDb(vol: number): number { // Formula: db = (vol - 200) / 2 // e.g., 88 -> (88 - 200) / 2 = -56dB return (vol - 200) / 2; } export function decodeCustomBase64(encoded: string): string { let translated = ""; for (let i = 0; i < encoded.length; i++) { const char = encoded.charAt(i); const index = ALPHABET_CUSTOM.indexOf(char); translated += index !== -1 ? ALPHABET_STANDARD.charAt(index) : char; } // Strip whitespace / newlines that may be present in API responses. // Chrome 91 atob() is stricter than modern browsers and rejects these. translated = translated.replace(/\s/g, ""); // Ensure correct Base64 padding (=). Modern atob() tolerates missing padding, // but Chrome 91 throws "The string to be decoded is not correctly encoded". const padNeeded = (4 - (translated.length % 4)) % 4; if (padNeeded > 0) { translated += "=".repeat(padNeeded); } const decoded = atob(translated); const bytes = new Uint8Array(decoded.length); for (let i = 0; i < decoded.length; i++) { bytes[i] = decoded.charCodeAt(i); } return new TextDecoder("utf-8").decode(bytes); } export function encodeCustomBase64(data: string): string { // btoa only supports Latin1; convert UTF-8 bytes to a binary string first. const bytes = new TextEncoder().encode(data); let binary = ""; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } const standard = btoa(binary); let translated = ""; for (let i = 0; i < standard.length; i++) { const char = standard.charAt(i); const index = ALPHABET_STANDARD.indexOf(char); translated += index !== -1 ? ALPHABET_CUSTOM.charAt(index) : char; } return translated; } // ============================================================ // Device State Type // ============================================================ export interface DeviceState { device: string; version: string | number; mac: string; language: number; volume: number; soundStep: number; input: number; output: number; audioFormat: string; pcm: number; hdmimutepolar: number; hdmiType: number; vu: number; vuSensor: number; vu_count: number; screenLight: number; knob_breathlight: number; buttonLight: number; buttonShort: number; screenOff: number; sleep: number; autoHome: number; bootSound: number; dsp_enable: number; audio_enable: number; peqEnable: number; peqSelect: number; balance: number; xlr: number; dacGain: number; dacArc: number; dacImpedance: number; dreMode: number; dacVolumeDirect: number; analogGain: number; effect_enable: number; effect_value: number; width_enable: number; width_value: number; scene_enable: number; scene_value: number; crossfeed_enable: number; crossfeed_value: number; subwoofer_enable: number; subwoofer_value: number; subwoofer_rate: number; subwoofer_gain: number; loudness_enable: number; loudness_bass_gain: number; loudness_treble_gain: number; loudness_threshold_gain: number; bt_status: number; bt_srcname: string; bt_title: string; bt_artist: string; msgCount: number; led_enable: number; led_red: number; led_green: number; led_blue: number; } export interface PeqFilter { fc: number; gain: number; q: number; type: number; } export interface PeqState { filters: PeqFilter[]; peqSelect?: number; peq?: Array<{ name: string; filters?: PeqFilter[] | string; autoPre?: number; preamp?: number; canDel?: number; brand?: string; model?: string; target?: string; form?: string; }>; } /** Shared PEQ preset fields for device POST bodies. */ export interface PeqPresetBody { name: string; filters: PeqFilter[]; autoPre?: number; preamp?: number; canDel?: number; brand?: string; model?: string; target?: string; form?: string; } /** PEQ preset metadata used when building peqChange / peqApply bodies. */ export type PeqPresetMeta = { name: string; autoPre?: number; preamp?: number; canDel?: number; brand?: string; model?: string; target?: string; form?: string; }; /** Build device PEQ POST body; optional brand/model/target/form included only when non-empty. */ export function buildPeqPresetBody(peq: PeqPresetMeta, filters: PeqFilter[]): PeqPresetBody { const body: PeqPresetBody = { name: peq.name, filters, autoPre: peq.autoPre ?? 0, preamp: peq.preamp ?? 0, canDel: peq.canDel ?? 1, }; const brand = peq.brand?.trim(); const model = peq.model?.trim(); const target = peq.target?.trim(); const form = peq.form?.trim(); if (brand) body.brand = brand; if (model) body.model = model; if (target) body.target = target; if (form) body.form = form; return body; } /** POST body for `/dev/info.cgi` — full peq preset update (custom base64 `json` field). */ export interface PeqChangePayload { peqChange: PeqPresetBody; } /** POST body for `/dev/info.cgi` — apply A/B comparison curve without saving preset metadata. */ export interface PeqApplyPayload { peqApply: PeqPresetBody; } // ============================================================ // Input/Output Labels // ============================================================ export const INPUT_LABELS = ["USB", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "RCA"]; export const OUTPUT_LABELS = ["XLR", "RCA", "Headset", "XLR/RCA"]; export const LANGUAGE_LABELS = ["English", "繁體中文", "简体中文"]; export const SCREEN_LIGHT_LABELS = ["Bright", "Medium", "Dark"]; export const KNOB_LIGHT_LABELS = ["Off", "Bright", "Medium", "Dark"]; export const SCREEN_OFF_LABELS = ["Off", "30s", "1 min", "3 min", "5 min"]; export const SLEEP_LABELS = ["Off", "1 min", "5 min", "10 min"]; export const AUTO_HOME_LABELS = ["Off", "20s", "40s", "60s"]; export const FILTER_TYPE_LABELS = ["Low Pass", "High Pass", "Band Pass", "Notch", "Peak", "Low Shelf", "High Shelf", "All Pass"]; export const DAC_ARC_LABELS = ["ARC", "eARC"]; export const BT_STATUS_LABELS = ["Disconnected", "Playing", "Paused"]; function resolveDeviceProtocol(): "http" | "https" { if (typeof window !== "undefined" && window.location.protocol === "https:") { return "https"; } return "http"; } function normalizeDeviceHost(input: string): string { return input.trim().replace(/^https?:\/\//i, "").replace(/\/+$/, ""); } // ============================================================ // API Class // ============================================================ export class LuxsinAPI { private baseUrl: string; constructor(ip: string) { const host = normalizeDeviceHost(ip); const protocol = resolveDeviceProtocol(); this.baseUrl = `${protocol}://${host}`; } private maybeRedirectForHttpsCert(error: unknown) { // On HTTPS pages, first-time device access may fail before user trusts the cert. if (typeof window === "undefined") return; if (window.location.protocol !== "https:") return; if (!(error instanceof Error)) return; // fetch network failure is typically reported as TypeError/Failed to fetch. const msg = (error.message || "").toLowerCase(); if (error.name === "TypeError" || msg.includes("failed to fetch") || msg.includes("networkerror")) { window.location.href = this.baseUrl; } } async getDeviceState(): Promise { try { const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncData`); const text = await response.text(); const decoded = decodeCustomBase64(text.trim()); const state = JSON.parse(decoded) as DeviceState; console.log("[syncData]", state); return state; } catch (error) { this.maybeRedirectForHttpsCert(error); throw error; } } async getMsgCount(): Promise { const response = await fetch(`${this.baseUrl}/msgCount`); const text = await response.text(); return parseInt(text.trim(), 10); } async refreshState(): Promise { await fetch(`${this.baseUrl}/msgAdd`); } async setSetting(params: Record): Promise { const query = Object.entries(params) .map(([k, v]) => `${k}=${v}`) .join("&"); await fetch(`${this.baseUrl}/dev/info.cgi?action=setting&${query}`); } async getPeqState(): Promise { const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncPeq`); const text = await response.text(); const decoded = decodeCustomBase64(text.trim()); return JSON.parse(decoded) as PeqState; } async setPeqFilters(filters: PeqFilter[]): Promise { await this.postPeqJson({ peqChange: { filters } } as PeqChangePayload); } /** Full peq preset: name, filters, autoPre, preamp, canDel — same encoding as legacy `upgradePeq`. */ async upgradePeqChange(body: PeqChangePayload): Promise { await this.postPeqJson(body); } /** Apply current EQ filters (e.g. A/B curve switch) without a full preset save. */ async upgradePeqApply(body: PeqApplyPayload): Promise { await this.postPeqJson(body); } /** POST `json=` — matches legacy axios `upgradePeq`. */ private async postPeqJson(body: PeqChangePayload | PeqApplyPayload): Promise { const encoded = encodeCustomBase64(JSON.stringify(body)); const form = new URLSearchParams(); form.set("json", encoded); await fetch(`${this.baseUrl}/dev/info.cgi`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, body: form.toString(), }); } /** Remove one or more headphone PEQ profiles. */ async removePeq(names: string[]): Promise { const encoded = encodeCustomBase64(JSON.stringify({ peqRemove: names })); const form = new URLSearchParams(); form.set("json", encoded); await fetch(`${this.baseUrl}/dev/info.cgi`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, body: form.toString(), }); } setVolume(volume: number) { return this.setSetting({ volume }); } setInput(input: number) { return this.setSetting({ input }); } setOutput(output: number) { return this.setSetting({ output }); } setBalance(balance: number) { return this.setSetting({ balance }); } setLanguage(language: number) { return this.setSetting({ language }); } setDspEnable(enable: boolean) { return this.setSetting({ dsp_enable: enable ? 0 : 1 }); } setAudioEnable(enable: boolean) { return this.setSetting({ audio_enable: enable ? 1 : 0 }); } setPeqEnable(enable: boolean) { return this.setSetting({ peqEnable: enable ? 1 : 0 }); } setPeqSelect(index: number) { return this.setSetting({ peqSelect: index }); } setVu(vu: number) { return this.setSetting({ vu }); } setScreenLight(level: number) { return this.setSetting({ screenLight: level }); } setKnobLight(level: number) { return this.setSetting({ knob_breathlight: level }); } setButtonLight(on: boolean) { return this.setSetting({ buttonLight: on ? 0 : 1 }); } setScreenOff(timer: number) { return this.setSetting({ screenOff: timer }); } setSleep(timer: number) { return this.setSetting({ sleep: timer }); } setAutoHome(timer: number) { return this.setSetting({ autoHome: timer }); } setLoudnessEnable(enable: boolean) { return this.setSetting({ loudness_enable: enable ? 1 : 0 }); } setSubwooferEnable(enable: boolean) { return this.setSetting({ subwoofer_enable: enable ? 1 : 0 }); } btPlayPause() { return this.setSetting({ bt_play: 1 }); } btNext() { return this.setSetting({ bt_next: 1 }); } btPrev() { return this.setSetting({ bt_next: 0 }); } setEffectEnable(enable: boolean) { return this.setSetting({ effect_enable: enable ? 1 : 0 }); } setWidthEnable(enable: boolean) { return this.setSetting({ width_enable: enable ? 1 : 0 }); } setSceneEnable(enable: boolean) { return this.setSetting({ scene_enable: enable ? 1 : 0 }); } setCrossfeedEnable(enable: boolean) { return this.setSetting({ crossfeed_enable: enable ? 1 : 0 }); } setXlrPolarity(reverse: boolean) { return this.setSetting({ xlr: reverse ? 1 : 0 }); } setDacArc(earc: boolean) { return this.setSetting({ dacArc: earc ? 1 : 0 }); } /** Boot volume level: 0 = default, 1..6 = -5..-30 dB, 7..10 = -35..-50 dB (firmware >= 26). */ setBootSound(level: number) { return this.setSetting({ bootSound: Math.max(0, Math.min(10, Math.round(level))) }); } /** Device power off — `GET .../dev/info.cgi?action=setting&power=0` */ powerOff() { return this.setSetting({ power: 0 }); } } /** Parse firmware `version` for feature gating (e.g. bootSound extended steps). */ export function parseFirmwareVersion(version: unknown): number { if (version === null || version === undefined) return 0; if (typeof version === "number" && Number.isFinite(version)) return version; const text = String(version).trim(); if (!text) return 0; const direct = Number(text); if (Number.isFinite(direct)) return direct; const parts = text.match(/\d+/g); if (!parts?.length) return 0; return Number(parts[parts.length - 1]); } /** UI display: build number 26 → `1.0.0.26`. Already-prefixed values are unchanged. */ export function formatFirmwareVersionDisplay(version: unknown): string { if (version === null || version === undefined) return "—"; const raw = String(version).trim(); if (!raw) return "—"; if (/^1\.0\.0\./i.test(raw)) return raw; return `1.0.0.${raw}`; } /** Max `bootSound` index available for the current firmware. */ export function getBootSoundMaxIndex(firmwareVersion: number): number { return firmwareVersion >= 26 ? 10 : 6; } /** Ambient LED block on System page (firmware build 28+). */ export const AMBIENT_LED_MIN_FIRMWARE_VERSION = 28; export function supportsAmbientLed(firmwareVersion: number): boolean { return firmwareVersion >= AMBIENT_LED_MIN_FIRMWARE_VERSION; } export function clampLedChannel(value: unknown): number { const n = typeof value === "number" ? value : Number(value); if (!Number.isFinite(n)) return 0; return Math.max(0, Math.min(255, Math.round(n))); } /** Normalize `bootSound` from syncData (0 = default … 10 = -50 dB). */ export function normalizeBootSound(value: unknown): number { const n = typeof value === "number" ? value : Number(value); if (!Number.isFinite(n)) return 0; return Math.max(0, Math.min(10, Math.round(n))); } export function readBootSoundFromState(state: DeviceState | null | undefined): number { if (!state) return 0; const raw = state.bootSound ?? (state as Record).bootsound; return normalizeBootSound(raw); } // ============================================================ // Mock data for demo/offline mode // ============================================================ export const MOCK_DEVICE_STATE: DeviceState = { device: "Luxsin-X8", version: 28, mac: "AA:BB:CC:DD:EE:FF", language: 0, volume: 75, soundStep: 2, input: 0, output: 0, audioFormat: "PCM 44.1 KHz", pcm: 1, hdmimutepolar: 0, hdmiType: 0, vu: 0, vuSensor: 0, vu_count: 5, screenLight: 0, knob_breathlight: 1, buttonLight: 0, buttonShort: 0, screenOff: 2, sleep: 0, autoHome: 1, bootSound: 1, dsp_enable: 0, audio_enable: 1, peqEnable: 1, peqSelect: 0, balance: 0, xlr: 0, dacGain: 0, dacArc: 0, dacImpedance: 0, dreMode: 0, dacVolumeDirect: 0, analogGain: 0, effect_enable: 0, effect_value: 50, width_enable: 0, width_value: 50, scene_enable: 0, scene_value: 0, crossfeed_enable: 0, crossfeed_value: 0, subwoofer_enable: 0, subwoofer_value: 80, subwoofer_rate: 0, subwoofer_gain: 0, loudness_enable: 0, loudness_bass_gain: 3, loudness_treble_gain: 2, loudness_threshold_gain: 60, bt_status: 1, bt_srcname: "iPhone 15 Pro", bt_title: "Bohemian Rhapsody", bt_artist: "Queen", msgCount: 42, led_enable: 1, led_red: 128, led_green: 64, led_blue: 200, }; export const MOCK_PEQ_STATE: PeqState = { filters: [ { fc: 80, gain: 3.0, q: 0.7, type: 5 }, { fc: 250, gain: -1.5, q: 1.4, type: 4 }, { fc: 1000, gain: 0.0, q: 1.0, type: 4 }, { fc: 4000, gain: 2.0, q: 1.4, type: 4 }, { fc: 12000, gain: 1.5, q: 0.7, type: 6 }, ], }; // ============================================================ // Luxsin cloud audio catalog (brands / models) // Dev: proxied via Vite (`/luxsin-audio-api` → `/audio`) to avoid Origin-based 403. // ============================================================ export interface LuxsinAudioBrand { id: number; name: string; } export interface LuxsinAudioModel { id: number; name: string; form?: string; } export interface LuxsinAudioModelListItem { brandName: string; modelName: string; form?: string; source?: string; } export function buildLuxsinAudioUrl(resourcePath: string): string { const path = resourcePath.replace(/^\//, ""); if (import.meta.env.DEV) { return `/luxsin-audio-api/${path}`; } return `https://api.luxsin.com.cn/audio/${path}`; } /** Fetches `getBrand`; body is custom Base64 text, decodes to JSON array of `{ id, name }`. */ export async function fetchLuxsinAudioBrands(): Promise { const res = await fetch(buildLuxsinAudioUrl("getBrand")); if (!res.ok) { throw new Error(`getBrand HTTP ${res.status}`); } const body = (await res.text()).trim(); const json = decodeCustomBase64(body); const data = JSON.parse(json) as unknown; if (!Array.isArray(data)) { throw new Error("getBrand: response is not an array"); } return data .map((row) => { if (!row || typeof row !== "object") return null; const r = row as { id?: unknown; name?: unknown }; const name = typeof r.name === "string" ? r.name : ""; if (!name) return null; const id = typeof r.id === "number" ? r.id : Number(r.id); return { id: Number.isFinite(id) ? id : 0, name }; }) .filter((b): b is LuxsinAudioBrand => b !== null); } /** Fetches `getModel?brandName=...`; body is custom Base64 text, decodes to JSON array of model rows. */ export async function fetchLuxsinAudioModels(brandName: string): Promise { const brand = brandName.trim(); if (!brand) return []; const res = await fetch(buildLuxsinAudioUrl(`getModel?brandName=${encodeURIComponent(brand)}`)); if (!res.ok) { throw new Error(`getModel HTTP ${res.status}`); } const body = (await res.text()).trim(); const json = decodeCustomBase64(body); const data = JSON.parse(json) as unknown; if (!Array.isArray(data)) { throw new Error("getModel: response is not an array"); } return data .map((row) => { if (!row || typeof row !== "object") return null; const r = row as { id?: unknown; name?: unknown; form?: unknown }; const name = typeof r.name === "string" ? r.name : ""; if (!name) return null; const id = typeof r.id === "number" ? r.id : Number(r.id); const form = typeof r.form === "string" ? r.form : undefined; return { id: Number.isFinite(id) ? id : 0, name, ...(form ? { form } : {}), }; }) .filter((m): m is LuxsinAudioModel => m !== null); } /** Fetches `modelList?key=...&count=...`; body is custom Base64 text, decodes to brand+model rows. */ export async function fetchLuxsinAudioModelList( key: string, count = 1000, ): Promise { const q = key.trim(); if (!q) return []; const safeCount = Number.isFinite(count) ? Math.max(1, Math.floor(count)) : 1000; const res = await fetch( buildLuxsinAudioUrl(`modelList?key=${encodeURIComponent(q)}&count=${safeCount}`), ); if (!res.ok) { throw new Error(`modelList HTTP ${res.status}`); } const body = (await res.text()).trim(); const json = decodeCustomBase64(body); const data = JSON.parse(json) as unknown; if (!Array.isArray(data)) { throw new Error("modelList: response is not an array"); } return data .map((row): LuxsinAudioModelListItem | null => { if (!row || typeof row !== "object") return null; const r = row as { brand_name?: unknown; name?: unknown; form?: unknown; source?: unknown; }; const brandName = typeof r.brand_name === "string" ? r.brand_name : ""; const modelName = typeof r.name === "string" ? r.name : ""; if (!brandName || !modelName) return null; const form = typeof r.form === "string" ? r.form : undefined; const source = typeof r.source === "string" ? r.source : undefined; return { brandName, modelName, ...(form ? { form } : {}), ...(source ? { source } : {}), }; }) .filter((item): item is LuxsinAudioModelListItem => item !== null); } /** Fetches `getCurve` and returns decoded text payload. */ export async function fetchLuxsinAudioCurve( brand: string, name: string, target: string, ): Promise { const brandName = encodeURIComponent(brand.trim()); const modelName = encodeURIComponent(name.trim()); const targetName = encodeURIComponent(target.trim()); const res = await fetch( buildLuxsinAudioUrl(`getCurve?brand=${brandName}&name=${modelName}&target=${targetName}`), ); if (!res.ok) { throw new Error(`getCurve HTTP ${res.status}`); } const body = (await res.text()).trim(); return decodeCustomBase64(body); } // ============================================================ // Share Code API — shareCreate / shareList / shareQuery / shareAccept // ============================================================ const SHARE_DEVICE_MODELS = ["Luxsin-X9", "Luxsin-X8"] as const; export type ShareDeviceModel = (typeof SHARE_DEVICE_MODELS)[number]; /** Map DeviceState.device to app-api share code model enum. */ export function resolveShareDeviceModel(device: string): ShareDeviceModel | null { const trimmed = device.trim(); if ((SHARE_DEVICE_MODELS as readonly string[]).includes(trimmed)) { return trimmed as ShareDeviceModel; } const hyphenated = trimmed.replace(/\s+/g, "-"); if ((SHARE_DEVICE_MODELS as readonly string[]).includes(hyphenated)) { return hyphenated as ShareDeviceModel; } const lower = trimmed.toLowerCase(); if (lower.includes("x9")) return "Luxsin-X9"; if (lower.includes("x8")) return "Luxsin-X8"; return null; } export interface ShareCodeCreateResponse { code: number; msg: string; share_code?: string; expire_at?: string; eq_data?: Record; } export interface ShareCodeListItem { share_code: string; expire_at: string; eq_data: Record; } export interface ShareCodeListResponse { code: number; msg: string; share_codes: ShareCodeListItem[]; } export interface ShareCodeAcceptResponse { code: number; msg: string; eq_data?: Record; model?: string; } export interface ShareCodeQueryResponse { code: number; msg: string; eq_data?: Record; expire_at?: string; model?: string; } /** Unwrap stringified PEQ filters (remove JSON escape layers) for API submit. */ export function normalizePeqFiltersForSubmit(filters: unknown): PeqFilter[] { if (Array.isArray(filters)) return filters as PeqFilter[]; if (typeof filters !== "string" || !filters.trim()) return []; let current: unknown = filters.trim(); for (let depth = 0; depth < 3; depth++) { if (Array.isArray(current)) return current as PeqFilter[]; if (typeof current !== "string") return []; try { current = JSON.parse(current); } catch { return []; } } return Array.isArray(current) ? (current as PeqFilter[]) : []; } /** POST `/audio/shareCreate` — 创建 EQ 分享码,返回 share_code + expire_at + eq_data */ export async function createShareCode( mac: string, model: ShareDeviceModel, eqData: Record, ): Promise { const payload = { ...eqData, ...(eqData.filters !== undefined ? { filters: normalizePeqFiltersForSubmit(eqData.filters) } : {}), }; const res = await fetch(buildLuxsinAudioUrl("shareCreate"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mac, model, eq_data: payload }), }); if (!res.ok) throw new Error(`shareCreate HTTP ${res.status}`); return (await res.json()) as ShareCodeCreateResponse; } /** GET `/audio/shareList?mac=...` — 查询某 MAC 尚未过期的分享码列表 */ export async function listShareCodes(mac: string): Promise { const res = await fetch( buildLuxsinAudioUrl(`shareList?mac=${encodeURIComponent(mac)}`), ); if (!res.ok) throw new Error(`shareList HTTP ${res.status}`); return (await res.json()) as ShareCodeListResponse; } /** GET `/audio/shareQuery?shareCode=...` — 预览分享码 EQ 数据(不记录导入) */ export async function queryShareCode(shareCode: string): Promise { const res = await fetch( buildLuxsinAudioUrl(`shareQuery?shareCode=${encodeURIComponent(shareCode)}`), ); if (!res.ok) throw new Error(`shareQuery HTTP ${res.status}`); return (await res.json()) as ShareCodeQueryResponse; } /** GET `/audio/shareAccept?mac=...&model=...&shareCode=...` — 确认导入分享码并记录流水 */ export async function acceptShareCode( mac: string, model: ShareDeviceModel, shareCode: string, ): Promise { const res = await fetch( buildLuxsinAudioUrl( `shareAccept?mac=${encodeURIComponent(mac)}&model=${encodeURIComponent(model)}&shareCode=${encodeURIComponent(shareCode)}`, ), ); if (!res.ok) throw new Error(`shareAccept HTTP ${res.status}`); return (await res.json()) as ShareCodeAcceptResponse; } export interface ShareCodeDeleteResponse { code: number; msg: string; } /** GET `/audio/shareDelete?mac=...&shareCode=...` — 删除分享码 */ export async function deleteShareCode( mac: string, shareCode: string, ): Promise { const res = await fetch( buildLuxsinAudioUrl( `shareDelete?mac=${encodeURIComponent(mac)}&shareCode=${encodeURIComponent(shareCode)}`, ), ); if (!res.ok) throw new Error(`shareDelete HTTP ${res.status}`); return (await res.json()) as ShareCodeDeleteResponse; }