Files
cube/apps/music/frontend/src/lib/api.js
T

236 lines
6.8 KiB
JavaScript
Raw Normal View History

// 薄薄一层 fetch 封装。错误统一抛 Error(message)。
async function jsonOrThrow(res) {
if (!res.ok) {
const text = await res.text().catch(() => '')
throw new Error(text || `${res.status} ${res.statusText}`)
}
return res.json()
}
export function listPieces({ tag, playlist } = {}) {
const qs = new URLSearchParams()
if (tag) qs.set('tag', tag)
if (playlist) qs.set('playlist', String(playlist))
const q = qs.toString()
return fetch('/api/pieces' + (q ? '?' + q : '')).then(jsonOrThrow)
}
export function getPiece(id) {
return fetch(`/api/pieces/${id}`).then(jsonOrThrow)
}
export function createPiece(body) {
return fetch('/api/pieces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(jsonOrThrow)
}
export function patchPiece(id, body) {
return fetch(`/api/pieces/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(jsonOrThrow)
}
export function deletePiece(id) {
return fetch(`/api/pieces/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
}
export function recordPlay(id) {
return fetch(`/api/pieces/${id}/play`, { method: 'POST' }).then(jsonOrThrow)
}
// `role`: null | 'chord' | 'numbered' | 'staff'
export function uploadAttachments(pieceId, files, role) {
const fd = new FormData()
for (const f of files) fd.append('files', f, f.name)
const url = role
? `/api/pieces/${pieceId}/attachments?role=${encodeURIComponent(role)}`
: `/api/pieces/${pieceId}/attachments`
return fetch(url, { method: 'POST', body: fd }).then(jsonOrThrow)
}
export function deleteAttachment(id) {
return fetch(`/api/attachments/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
}
export function attachmentUrl(id) {
return `/api/attachments/${id}`
}
export function chordFetch(pieceId, mode = 'functional') {
return fetch(`/api/pieces/${pieceId}/chord/fetch?mode=${encodeURIComponent(mode)}`, { method: 'POST' }).then(jsonOrThrow)
}
export function chordStatus(pieceId, mode = 'functional') {
return fetch(`/api/pieces/${pieceId}/chord/status?mode=${encodeURIComponent(mode)}`).then(jsonOrThrow)
}
// ---- chat ----
export function listChat(pieceId) {
return fetch(`/api/pieces/${pieceId}/chat`).then(jsonOrThrow)
}
export function clearChat(pieceId) {
return fetch(`/api/pieces/${pieceId}/chat`, { method: 'DELETE' }).then(jsonOrThrow)
}
/**
* stream a chat reply.
* @param {number} pieceId
* @param {string} message
* @param {(delta: string) => void} onDelta
* @param {AbortSignal} signal
* @returns {Promise<{ok: boolean, error?: string}>}
*/
export async function streamChat(pieceId, message, onDelta, signal) {
const resp = await fetch(`/api/pieces/${pieceId}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
signal,
})
if (!resp.ok) {
const text = await resp.text().catch(() => '')
return { ok: false, error: text || `${resp.status}` }
}
const reader = resp.body.getReader()
const dec = new TextDecoder()
let buf = ''
let lastEvent = 'message'
let errorMsg = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buf += dec.decode(value, { stream: true })
let idx
while ((idx = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, idx)
buf = buf.slice(idx + 1)
// SSE 协议:空行 = event 边界(已在内容中处理);event:/data: 各自一行
if (line.startsWith('event:')) {
lastEvent = line.slice(6).trim()
} else if (line.startsWith('data:')) {
const data = line.slice(5).replace(/^ /, '')
if (lastEvent === 'error') {
errorMsg = data
} else if (lastEvent === 'done') {
// 不带 data 也算结束
} else {
onDelta(data)
}
} else if (line === '') {
lastEvent = 'message'
}
}
}
if (errorMsg) return { ok: false, error: errorMsg }
return { ok: true }
}
// ---- inspire ----
export async function streamInspire(hint, onDelta, signal) {
const resp = await fetch('/api/inspire', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hint: hint || null }),
signal,
})
if (!resp.ok) {
const text = await resp.text().catch(() => '')
return { ok: false, error: text || `${resp.status}` }
}
const reader = resp.body.getReader()
const dec = new TextDecoder()
let buf = ''
let lastEvent = 'message'
let errorMsg = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buf += dec.decode(value, { stream: true })
let idx
while ((idx = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, idx)
buf = buf.slice(idx + 1)
if (line.startsWith('event:')) {
lastEvent = line.slice(6).trim()
} else if (line.startsWith('data:')) {
const data = line.slice(5).replace(/^ /, '')
if (lastEvent === 'error') errorMsg = data
else if (lastEvent !== 'done') onDelta(data)
} else if (line === '') {
lastEvent = 'message'
}
}
}
if (errorMsg) return { ok: false, error: errorMsg }
return { ok: true }
}
// ---- tags ----
export function listTags() {
return fetch('/api/tags').then(jsonOrThrow)
}
export function createTag(name) {
return fetch('/api/tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
}).then(jsonOrThrow)
}
export function deleteTag(id) {
return fetch(`/api/tags/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
}
// ---- playlists ----
export function listPlaylists() {
return fetch('/api/playlists').then(jsonOrThrow)
}
export function createPlaylist(name, description) {
return fetch('/api/playlists', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description: description || null }),
}).then(jsonOrThrow)
}
export function getPlaylist(id) {
return fetch(`/api/playlists/${id}`).then(jsonOrThrow)
}
export function patchPlaylist(id, body) {
return fetch(`/api/playlists/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(jsonOrThrow)
}
export function deletePlaylist(id) {
return fetch(`/api/playlists/${id}`, { method: 'DELETE' }).then(jsonOrThrow)
}
export function playlistAddPiece(playlistId, pieceId) {
return fetch(`/api/playlists/${playlistId}/pieces`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ piece_id: pieceId }),
}).then(jsonOrThrow)
}
export function playlistRemovePiece(playlistId, pieceId) {
return fetch(`/api/playlists/${playlistId}/pieces/${pieceId}`, { method: 'DELETE' }).then(jsonOrThrow)
}