music: 新建 music app,替换 piano-sheet
deploy cube / build-and-deploy (push) Successful in 1m10s
deploy music / build-and-deploy (push) Successful in 1m47s
deploy simpleasm / build-and-deploy (push) Successful in 1m20s

听歌 + 练琴曲目管理:
- 数据:piece (title/artist/category/lyrics/play_count/notes) + attachments (audio/video/pdf/image; image 带 role=chord/numbered/staff)
- 后端 axum + sqlite,附件流式落 PVC,ServeFile 支持 Range(视频拖动)
- 前端 guitar 风格 player:左 sidebar + tabs(歌词/吉他谱/简谱/五线谱/PDF/视频),LRC 同步、快捷键、笔记自动保存
- ns cube-music + music.famzheng.me + bodylimit 5GiB
- scripts/import_guitar.py 用于把 oci /data/guitar/ 旧曲库导入
This commit is contained in:
Fam Zheng
2026-05-09 22:36:14 +01:00
parent 58f344db85
commit 1a8f297302
30 changed files with 2683 additions and 1314 deletions
+59
View File
@@ -0,0 +1,59 @@
// 薄薄一层 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() {
return fetch('/api/pieces').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}`
}