feat: launch privacy-first local lyrics tool
This commit is contained in:
commit
70fbfc2889
17 files changed
+893
No files matched your search
+70
@@ -0,0 +1,70 @@
|
||||
const form = document.querySelector('#search-form');
|
||||
const keyword = document.querySelector('#keyword');
|
||||
const notice = document.querySelector('#notice');
|
||||
const results = document.querySelector('#results');
|
||||
|
||||
function text(value) { return document.createTextNode(value ?? ''); }
|
||||
function safeName(value) { return (value || 'unknown').replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_').trim().slice(0, 180) || 'unknown'; }
|
||||
function duration(seconds) { const n = Number(seconds || 0); return n ? `${Math.floor(n / 60)}:${String(n % 60).padStart(2, '0')}` : ''; }
|
||||
function plainText(lrc) { return lrc.split(/\r?\n/).map(line => line.replace(/\[[^\]]*\]/g, '').trim()).filter(Boolean).join('\n'); }
|
||||
|
||||
async function api(path, payload) {
|
||||
const response = await fetch(path, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) });
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(data.error || `请求失败 (${response.status})`);
|
||||
return data;
|
||||
}
|
||||
|
||||
function download(name, body, type = 'text/plain;charset=utf-8') {
|
||||
const url = URL.createObjectURL(new Blob([body], {type}));
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url; anchor.download = name; anchor.hidden = true;
|
||||
document.body.append(anchor); anchor.click(); anchor.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1500);
|
||||
}
|
||||
|
||||
async function fetchAndSave(song, kind, button) {
|
||||
button.disabled = true;
|
||||
notice.className = 'notice'; notice.textContent = `正在读取《${song.name}》…`;
|
||||
try {
|
||||
const data = await api('/api/lyrics', {mid: song.mid});
|
||||
const base = safeName(`${song.name} - ${song.singer}`);
|
||||
if (kind === 'lrc') download(`${base}.lrc`, data.lyric);
|
||||
if (kind === 'txt') download(`${base}.txt`, plainText(data.lyric));
|
||||
if (kind === 'trans') {
|
||||
if (!data.trans) throw new Error('这首歌没有翻译歌词');
|
||||
download(`${base}.trans.lrc`, data.trans);
|
||||
}
|
||||
notice.textContent = '文件已在浏览器中生成,内容未上传。';
|
||||
} catch (error) {
|
||||
notice.className = 'notice error'; notice.textContent = error.message;
|
||||
} finally { button.disabled = false; }
|
||||
}
|
||||
|
||||
function renderSongs(songs) {
|
||||
results.replaceChildren();
|
||||
songs.forEach((song, index) => {
|
||||
const article = document.createElement('article'); article.className = 'song';
|
||||
const info = document.createElement('div');
|
||||
const title = document.createElement('h2'); title.append(text(`${String(index + 1).padStart(2, '0')} ${song.name}`));
|
||||
const meta = document.createElement('div'); meta.className = 'meta';
|
||||
meta.append(text([song.singer, song.album ? `《${song.album}》` : '', duration(song.interval)].filter(Boolean).join(' · ')));
|
||||
info.append(title, meta);
|
||||
const actions = document.createElement('div'); actions.className = 'actions';
|
||||
[['lrc','下载 LRC','primary'],['txt','纯文本'],['trans','翻译 LRC']].forEach(([kind,label,klass]) => {
|
||||
const button = document.createElement('button'); button.type = 'button'; button.className = klass || ''; button.append(text(label));
|
||||
button.addEventListener('click', () => fetchAndSave(song, kind, button)); actions.append(button);
|
||||
});
|
||||
article.append(info, actions); results.append(article);
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async event => {
|
||||
event.preventDefault(); const submit = form.querySelector('button'); submit.disabled = true;
|
||||
notice.className = 'notice'; notice.textContent = '正在从这台电脑搜索…'; results.replaceChildren();
|
||||
try {
|
||||
const data = await api('/api/search', {keyword: keyword.value.trim(), limit: 15});
|
||||
renderSongs(data.songs || []); notice.textContent = data.songs?.length ? `找到 ${data.songs.length} 条结果。` : '没有找到相关歌曲。';
|
||||
} catch (error) { notice.className = 'notice error'; notice.textContent = error.message; }
|
||||
finally { submit.disabled = false; }
|
||||
});
|
||||
Reference in new issue
Block a user