music(chord): 拆两个 tab + 抓两种 (letters/functional)
deploy music / build-and-deploy (push) Successful in 1m54s
deploy music / build-and-deploy (push) Successful in 1m54s
- yopu 切 /song?title=&artist= 搜索(避免歌手词被搜糊)
- 抓的版本按搜索结果 nier-snippet svg <text> 数区分:
>0 = 字母谱 (G/Em7/C 弹唱谱);==0 = 功能谱 (1/4/5/6m 数字级数)
- sidecar fetch/status/state/image 都走 (id, mode) 维度,文件落 /data/chord-fetch/{id}-{mode}.png
- backend chord_fetch / chord_status 接 ?mode=letters|functional,import 时 role 分别为 chord_letters / chord_functional
- 前端 chord tab 拆「吉他谱」+「功能谱」,state/error/poll 各自独立;旧 role='chord' 显示在「吉他谱」兼容历史 import
- verified 标记探测:匿名访问 yopu HTML 里 0 hits(要登录可见),暂时只能按 svg_text 区分
This commit is contained in:
@@ -62,12 +62,12 @@ export function attachmentUrl(id) {
|
||||
return `/api/attachments/${id}`
|
||||
}
|
||||
|
||||
export function chordFetch(pieceId) {
|
||||
return fetch(`/api/pieces/${pieceId}/chord/fetch`, { method: 'POST' }).then(jsonOrThrow)
|
||||
export function chordFetch(pieceId, mode = 'functional') {
|
||||
return fetch(`/api/pieces/${pieceId}/chord/fetch?mode=${encodeURIComponent(mode)}`, { method: 'POST' }).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
export function chordStatus(pieceId) {
|
||||
return fetch(`/api/pieces/${pieceId}/chord/status`).then(jsonOrThrow)
|
||||
export function chordStatus(pieceId, mode = 'functional') {
|
||||
return fetch(`/api/pieces/${pieceId}/chord/status?mode=${encodeURIComponent(mode)}`).then(jsonOrThrow)
|
||||
}
|
||||
|
||||
// ---- chat ----
|
||||
|
||||
@@ -130,8 +130,43 @@
|
||||
>{{ line.text }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 谱面 -->
|
||||
<div v-show="['chord', 'numbered', 'staff'].includes(activeTab)" class="sheet-box">
|
||||
<!-- 谱面:弹唱谱 / 功能谱 共用渲染 -->
|
||||
<div v-show="['chord', 'functional'].includes(activeTab)" class="sheet-box">
|
||||
<img
|
||||
v-for="att in chordTabAttachments(activeTab)"
|
||||
:key="att.id"
|
||||
:src="attachmentUrl(att.id)"
|
||||
:alt="att.filename"
|
||||
class="sheet-img"
|
||||
@click="fullscreenSrc = attachmentUrl(att.id)"
|
||||
/>
|
||||
<div
|
||||
v-if="chordTabAttachments(activeTab).length === 0"
|
||||
class="auto-fetch"
|
||||
>
|
||||
<p v-if="chordStateOf(activeTab) === 'idle'" class="hint-line">
|
||||
<span v-if="activeTab === 'chord'">从 yopu.co 抓 <b>弹唱谱(字母 G/Em/C)</b>。</span>
|
||||
<span v-else>从 yopu.co 抓 <b>功能谱(数字 1/4/5/6m)</b>。</span>
|
||||
</p>
|
||||
<p v-else-if="['pending','processing'].includes(chordStateOf(activeTab))" class="hint-line">
|
||||
正在抓取,约 30-60s…
|
||||
</p>
|
||||
<p v-else-if="chordStateOf(activeTab) === 'failed'" class="hint-line err">
|
||||
抓取失败:{{ chordErrors[modeForTab(activeTab)] }}
|
||||
</p>
|
||||
<button
|
||||
class="btn-fetch"
|
||||
:disabled="['pending','processing'].includes(chordStateOf(activeTab))"
|
||||
@click="startChordFetch(modeForTab(activeTab))"
|
||||
>
|
||||
<span v-if="['pending','processing'].includes(chordStateOf(activeTab))" class="spin">⏳</span>
|
||||
<span v-else>🎸 自动抓取{{ activeTab === 'chord' ? '弹唱谱' : '功能谱' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 简谱 / 五线谱(手动上传的图) -->
|
||||
<div v-show="['numbered', 'staff'].includes(activeTab)" class="sheet-box">
|
||||
<img
|
||||
v-for="att in roleAttachments(activeTab)"
|
||||
:key="att.id"
|
||||
@@ -140,28 +175,6 @@
|
||||
class="sheet-img"
|
||||
@click="fullscreenSrc = attachmentUrl(att.id)"
|
||||
/>
|
||||
<div
|
||||
v-if="activeTab === 'chord' && roleAttachments('chord').length === 0"
|
||||
class="auto-fetch"
|
||||
>
|
||||
<p v-if="chordState === 'idle'" class="hint-line">
|
||||
从 yopu.co 抓 <b>功能谱 + 级数名</b>。
|
||||
</p>
|
||||
<p v-else-if="chordState === 'pending' || chordState === 'processing'" class="hint-line">
|
||||
正在抓取,浏览器后台跑 chromium 截图,约 30-60s…
|
||||
</p>
|
||||
<p v-else-if="chordState === 'failed'" class="hint-line err">
|
||||
抓取失败:{{ chordError }}
|
||||
</p>
|
||||
<button
|
||||
class="btn-fetch"
|
||||
:disabled="chordState === 'pending' || chordState === 'processing'"
|
||||
@click="startChordFetch"
|
||||
>
|
||||
<span v-if="chordState === 'pending' || chordState === 'processing'" class="spin">⏳</span>
|
||||
<span v-else>🎸 自动抓取吉他谱</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF -->
|
||||
@@ -339,11 +352,14 @@ let lastReportedId = null
|
||||
// fullscreen 乐谱
|
||||
const fullscreenSrc = ref(null)
|
||||
|
||||
// chord
|
||||
const chordState = ref('idle')
|
||||
const chordError = ref('')
|
||||
let chordPollTimer = null
|
||||
let chordPollStarted = 0
|
||||
// chord —— 两个 mode 各自独立 state
|
||||
const chordStates = ref({ letters: 'idle', functional: 'idle' })
|
||||
const chordErrors = ref({ letters: '', functional: '' })
|
||||
const chordPollTimers = { letters: null, functional: null }
|
||||
const chordPollStarted = { letters: 0, functional: 0 }
|
||||
function chordStateOf(tab) {
|
||||
return chordStates.value[modeForTab(tab)] || 'idle'
|
||||
}
|
||||
|
||||
// chat
|
||||
const chatBodyEl = ref(null)
|
||||
@@ -380,11 +396,26 @@ function roleAttachments(role) {
|
||||
)
|
||||
}
|
||||
|
||||
// chord 兼容:吉他谱 tab 显示历史 role='chord' + 新 role='chord_letters';功能谱 tab 显示 role='chord_functional'
|
||||
function chordTabAttachments(tab) {
|
||||
const set = tab === 'chord'
|
||||
? new Set(['chord', 'chord_letters'])
|
||||
: new Set(['chord_functional'])
|
||||
return (selected.value?.attachments || []).filter(
|
||||
a => a.kind === 'image' && set.has(a.role),
|
||||
)
|
||||
}
|
||||
|
||||
function modeForTab(tab) {
|
||||
return tab === 'functional' ? 'functional' : 'letters'
|
||||
}
|
||||
|
||||
const tabs = computed(() => {
|
||||
if (!selected.value) return []
|
||||
const list = []
|
||||
if (selected.value.lyrics) list.push({ key: 'lyrics', label: '歌词', count: 0 })
|
||||
list.push({ key: 'chord', label: '吉他谱', count: roleAttachments('chord').length })
|
||||
list.push({ key: 'chord', label: '吉他谱', count: chordTabAttachments('chord').length })
|
||||
list.push({ key: 'functional', label: '功能谱', count: chordTabAttachments('functional').length })
|
||||
const num = roleAttachments('numbered').length
|
||||
if (num) list.push({ key: 'numbered', label: '简谱', count: num })
|
||||
const staff = roleAttachments('staff').length
|
||||
@@ -504,9 +535,9 @@ async function promptNewPlaylist() {
|
||||
async function loadPiece(id) {
|
||||
selected.value = null
|
||||
notesDraft.value = ''
|
||||
stopChordPoll()
|
||||
chordState.value = 'idle'
|
||||
chordError.value = ''
|
||||
stopChordPoll('letters'); stopChordPoll('functional')
|
||||
chordStates.value = { letters: 'idle', functional: 'idle' }
|
||||
chordErrors.value = { letters: '', functional: '' }
|
||||
abortChat()
|
||||
chatMessages.value = []
|
||||
chatStreamText.value = ''
|
||||
@@ -660,54 +691,54 @@ function onNotesInput() {
|
||||
}
|
||||
|
||||
// chord
|
||||
async function startChordFetch() {
|
||||
async function startChordFetch(mode) {
|
||||
if (!selectedId.value) return
|
||||
chordState.value = 'pending'
|
||||
chordError.value = ''
|
||||
chordStates.value = { ...chordStates.value, [mode]: 'pending' }
|
||||
chordErrors.value = { ...chordErrors.value, [mode]: '' }
|
||||
try {
|
||||
const r = await chordFetch(selectedId.value)
|
||||
const r = await chordFetch(selectedId.value, mode)
|
||||
if (r.status === 'completed') {
|
||||
await reloadPiece()
|
||||
chordState.value = 'completed'
|
||||
chordStates.value = { ...chordStates.value, [mode]: 'completed' }
|
||||
return
|
||||
}
|
||||
chordState.value = r.status || 'pending'
|
||||
chordPollStarted = Date.now()
|
||||
if (chordPollTimer) clearInterval(chordPollTimer)
|
||||
chordPollTimer = setInterval(pollChord, 3000)
|
||||
chordStates.value = { ...chordStates.value, [mode]: r.status || 'pending' }
|
||||
chordPollStarted[mode] = Date.now()
|
||||
if (chordPollTimers[mode]) clearInterval(chordPollTimers[mode])
|
||||
chordPollTimers[mode] = setInterval(() => pollChord(mode), 3000)
|
||||
} catch (e) {
|
||||
chordState.value = 'failed'
|
||||
chordError.value = e.message || String(e)
|
||||
chordStates.value = { ...chordStates.value, [mode]: 'failed' }
|
||||
chordErrors.value = { ...chordErrors.value, [mode]: e.message || String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async function pollChord() {
|
||||
if (!selectedId.value) { stopChordPoll(); return }
|
||||
if (Date.now() - chordPollStarted > 90_000) {
|
||||
stopChordPoll()
|
||||
chordState.value = 'failed'
|
||||
chordError.value = '抓取超时'
|
||||
async function pollChord(mode) {
|
||||
if (!selectedId.value) { stopChordPoll(mode); return }
|
||||
if (Date.now() - chordPollStarted[mode] > 90_000) {
|
||||
stopChordPoll(mode)
|
||||
chordStates.value = { ...chordStates.value, [mode]: 'failed' }
|
||||
chordErrors.value = { ...chordErrors.value, [mode]: '抓取超时' }
|
||||
return
|
||||
}
|
||||
try {
|
||||
const r = await chordStatus(selectedId.value)
|
||||
chordState.value = r.status || 'pending'
|
||||
chordError.value = r.error || ''
|
||||
const r = await chordStatus(selectedId.value, mode)
|
||||
chordStates.value = { ...chordStates.value, [mode]: r.status || 'pending' }
|
||||
chordErrors.value = { ...chordErrors.value, [mode]: r.error || '' }
|
||||
if (r.status === 'completed') {
|
||||
stopChordPoll()
|
||||
stopChordPoll(mode)
|
||||
await reloadPiece()
|
||||
} else if (r.status === 'failed') {
|
||||
stopChordPoll()
|
||||
stopChordPoll(mode)
|
||||
}
|
||||
} catch (e) {
|
||||
chordError.value = e.message || String(e)
|
||||
chordErrors.value = { ...chordErrors.value, [mode]: e.message || String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
function stopChordPoll() {
|
||||
if (chordPollTimer) {
|
||||
clearInterval(chordPollTimer)
|
||||
chordPollTimer = null
|
||||
function stopChordPoll(mode) {
|
||||
if (chordPollTimers[mode]) {
|
||||
clearInterval(chordPollTimers[mode])
|
||||
chordPollTimers[mode] = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,7 +874,7 @@ onMounted(async () => {
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
if (notesTimer) clearTimeout(notesTimer)
|
||||
stopChordPoll()
|
||||
stopChordPoll('letters'); stopChordPoll('functional')
|
||||
abortChat()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user