feat: add protected public web lyrics workflow

This commit is contained in:
Flechazo committed 2026-08-25 23:08:28 +08:00
1 parent e71e0c74dd
commit 54167c1b59
13 files changed
+425 -57

No files matched your search

+30 -17
View File
@@ -3,30 +3,43 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="搜索并下载 LRC、翻译歌词或纯文本歌词。">
<title>LRC Local</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<main class="shell">
<main>
<header>
<div class="brand">LRC <span>Local</span></div>
<div class="local-pill"><i></i> 本机运行中</div>
<h1>LRC Local</h1>
<a href="https://git.sighs.cc/Flechazo/lrc-local">源码</a>
</header>
<section class="hero">
<p class="eyebrow">LOCAL-FIRST LYRIC TOOL</p>
<h1>把歌词带走,<br><em>别把隐私留下。</em></h1>
<p class="lead">搜索与歌词请求从这台电脑直接发出。关键词、歌词和下载文件不会经过我们的服务器。</p>
<form id="search-form">
<label for="keyword">歌曲、歌手或专辑</label>
<div class="search-row">
<input id="keyword" name="keyword" maxlength="100" autocomplete="off" placeholder="例如:起风了" required autofocus>
<button type="submit">搜索歌词</button>
</div>
</form>
<div id="notice" class="notice" role="status" aria-live="polite"></div>
</section>
<p>搜索歌曲并下载 LRC、翻译 LRC 或纯文本歌词。</p>
<p id="mode-note" class="note">正在确认运行模式…</p>
<form id="search-form">
<label for="keyword">歌曲、歌手或专辑</label>
<div class="search-row">
<input id="keyword" name="keyword" maxlength="100" autocomplete="off" placeholder="例如:起风了" required autofocus>
<button type="submit">搜索</button>
</div>
</form>
<div id="notice" class="notice" role="status" aria-live="polite"></div>
<section id="results" class="results" aria-label="搜索结果"></section>
<footer>非 QQ 音乐官方工具。请仅下载和使用你有权访问的歌词内容。</footer>
<section id="local-downloads" class="downloads" hidden>
<h2>可选:本机版</h2>
<p>如果不希望查询经过本站,可以下载本机版。它只监听 <code>127.0.0.1</code>,关闭窗口即停止。</p>
<div class="download-links">
<a href="/download/lrc-local-windows-amd64.exe">Windows 64 位</a>
<a href="/download/lrc-local-linux-amd64.tar.gz">Linux x64</a>
<a href="/download/lrc-local-darwin-arm64.tar.gz">macOS Apple 芯片</a>
<a href="/download/lrc-local-darwin-amd64.tar.gz">macOS Intel</a>
<a href="/download/SHA256SUMS">SHA-256</a>
</div>
</section>
<footer>网页版请求由本站转发到固定的 QQ 音乐接口,不保存搜索词、歌词或历史。非 QQ 音乐官方工具,请仅使用你有权访问的内容。</footer>
</main>
<script src="/app.js" defer></script>
</body>
+24 -3
View File
@@ -2,6 +2,9 @@ const form = document.querySelector('#search-form');
const keyword = document.querySelector('#keyword');
const notice = document.querySelector('#notice');
const results = document.querySelector('#results');
const modeNote = document.querySelector('#mode-note');
const localDownloads = document.querySelector('#local-downloads');
const lyricCache = new Map();
function text(value) { return document.createTextNode(value ?? ''); }
function safeName(value) { return (value || 'unknown').replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_').trim().slice(0, 180) || 'unknown'; }
@@ -15,6 +18,21 @@ async function api(path, payload) {
return data;
}
async function loadInfo() {
try {
const response = await fetch('/api/info');
const info = await response.json();
if (info.mode === 'local') {
modeNote.textContent = '本机模式:查询从这台电脑直接发出,不经过 lrc.flechazo.xin。';
} else {
modeNote.textContent = '网页模式:查询由本站后端转发,但不会写入数据库或日志正文。下载文件在浏览器中生成。';
localDownloads.hidden = false;
}
} catch {
modeNote.textContent = '无法确认运行模式,请刷新页面重试。';
}
}
function download(name, body, type = 'text/plain;charset=utf-8') {
const url = URL.createObjectURL(new Blob([body], {type}));
const anchor = document.createElement('a');
@@ -27,7 +45,8 @@ 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});
if (!lyricCache.has(song.mid)) lyricCache.set(song.mid, api('/api/lyrics', {mid: song.mid}));
const data = await lyricCache.get(song.mid).catch(error => { lyricCache.delete(song.mid); throw error; });
const base = safeName(`${song.name} - ${song.singer}`);
if (kind === 'lrc') download(`${base}.lrc`, data.lyric);
if (kind === 'txt') download(`${base}.txt`, plainText(data.lyric));
@@ -35,7 +54,7 @@ async function fetchAndSave(song, kind, button) {
if (!data.trans) throw new Error('这首歌没有翻译歌词');
download(`${base}.trans.lrc`, data.trans);
}
notice.textContent = '文件已在浏览器中生成,内容未上传。';
notice.textContent = '文件已在浏览器中生成。';
} catch (error) {
notice.className = 'notice error'; notice.textContent = error.message;
} finally { button.disabled = false; }
@@ -61,10 +80,12 @@ function renderSongs(songs) {
form.addEventListener('submit', async event => {
event.preventDefault(); const submit = form.querySelector('button'); submit.disabled = true;
notice.className = 'notice'; notice.textContent = '正在从这台电脑搜索…'; results.replaceChildren();
notice.className = 'notice'; notice.textContent = '正在搜索…'; results.replaceChildren(); lyricCache.clear();
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; }
});
loadInfo();
+1 -1
View File
@@ -1 +1 @@
:root{font-family:Inter,"Segoe UI","PingFang SC",sans-serif;color:#eaf0ee;background:#09100e;color-scheme:dark;--mint:#75f0b2;--muted:#93a39e;--line:#26332f}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(circle at 85% 5%,#17442f 0,transparent 31rem),linear-gradient(145deg,#09100e,#101815 60%,#080d0b)}body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.12;background-image:linear-gradient(#fff 1px,transparent 1px),linear-gradient(90deg,#fff 1px,transparent 1px);background-size:48px 48px}.shell{width:min(1020px,calc(100% - 32px));margin:auto;padding:30px 0 48px;position:relative}header{display:flex;justify-content:space-between;align-items:center}.brand{font-size:1.1rem;font-weight:850;letter-spacing:.08em}.brand span{color:var(--mint)}.local-pill{border:1px solid #315044;background:#10251d;padding:8px 12px;border-radius:99px;color:#b9d3c9;font-size:.82rem}.local-pill i{display:inline-block;width:7px;height:7px;background:var(--mint);border-radius:50%;box-shadow:0 0 12px var(--mint);margin-right:6px}.hero{padding:90px 0 42px;max-width:780px}.eyebrow{color:var(--mint);font-family:ui-monospace,monospace;font-size:.78rem;letter-spacing:.18em}.hero h1{font-size:clamp(3.1rem,9vw,6.8rem);line-height:.91;letter-spacing:-.065em;margin:24px 0;font-weight:880}.hero h1 em{font-style:normal;color:transparent;-webkit-text-stroke:1px #9ec4b5}.lead{color:var(--muted);font-size:1.08rem;line-height:1.75;max-width:650px}form{margin-top:38px}label{display:block;font-size:.82rem;color:#b5c4bf;margin-bottom:9px}.search-row{display:flex;gap:10px}input{flex:1;min-width:0;background:#eff7f3;color:#0b1511;border:0;border-radius:8px;padding:17px 18px;font:inherit;font-size:1.04rem;outline:2px solid transparent}input:focus{outline-color:var(--mint)}button{border:0;border-radius:8px;padding:0 24px;background:var(--mint);color:#07100c;font:inherit;font-weight:800;cursor:pointer}button:hover{filter:brightness(1.06)}button:disabled{opacity:.55;cursor:wait}.notice{min-height:28px;color:#b7cac2;margin-top:14px}.notice.error{color:#ff9b9b}.results{display:grid;gap:10px}.song{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:20px;align-items:center;padding:20px;border:1px solid var(--line);background:#101916d9;border-radius:12px}.song h2{font-size:1.05rem;margin:0 0 7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meta{color:var(--muted);font-size:.86rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.actions{display:flex;gap:7px}.actions button{background:#1c2a25;color:#d9e7e2;border:1px solid #31453e;padding:9px 12px;font-size:.82rem}.actions button.primary{background:var(--mint);border-color:var(--mint);color:#07100c}footer{margin-top:70px;padding-top:20px;border-top:1px solid var(--line);color:#75847f;font-size:.78rem}@media(max-width:700px){.shell{width:min(100% - 22px,1020px)}.hero{padding-top:65px}.hero h1{font-size:3.6rem}.search-row{display:grid}.search-row button{padding:15px}.song{grid-template-columns:1fr}.actions{flex-wrap:wrap}.actions button{flex:1}.local-pill{font-size:.72rem}}
:root{font-family:system-ui,-apple-system,"Segoe UI","PingFang SC",sans-serif;color:#1f2937;background:#f7f8fa;color-scheme:light;line-height:1.55}*{box-sizing:border-box}body{margin:0}main{width:min(820px,calc(100% - 28px));margin:0 auto;padding:34px 0 60px}header{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #dfe3e8;padding-bottom:16px;margin-bottom:28px}h1{font-size:1.5rem;margin:0}h2{font-size:1.05rem;margin:0 0 8px}a{color:#155eef}p{margin:10px 0}.note{background:#eef4ff;border:1px solid #c9d9ff;border-radius:8px;padding:10px 12px;color:#344054;font-size:.9rem}form{margin:26px 0 0}label{display:block;font-weight:650;margin-bottom:7px}.search-row{display:flex;gap:8px}input{flex:1;min-width:0;border:1px solid #b8c0cc;border-radius:7px;background:#fff;color:#111827;padding:12px;font:inherit}input:focus{outline:2px solid #8bb6ff;border-color:#3977db}button{border:0;border-radius:7px;background:#2563eb;color:#fff;padding:0 20px;font:inherit;font-weight:650;cursor:pointer}button:disabled{opacity:.55;cursor:wait}.notice{min-height:28px;margin:10px 0;color:#475467}.notice.error{color:#b42318}.results{display:grid;gap:8px}.song{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:center;padding:14px;background:#fff;border:1px solid #dfe3e8;border-radius:8px}.song h2{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.meta{color:#667085;font-size:.86rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.actions{display:flex;gap:6px}.actions button{background:#fff;color:#344054;border:1px solid #b8c0cc;padding:7px 10px;font-size:.82rem}.actions button.primary{background:#2563eb;color:#fff;border-color:#2563eb}.downloads{margin-top:38px;padding-top:24px;border-top:1px solid #dfe3e8}.downloads p,footer{color:#667085;font-size:.88rem}.download-links{display:flex;flex-wrap:wrap;gap:8px 15px}code{background:#eceff3;padding:1px 4px;border-radius:4px}footer{margin-top:35px;padding-top:20px;border-top:1px solid #dfe3e8}@media(max-width:650px){main{padding-top:22px}.search-row{display:grid}.search-row button{padding:11px}.song{grid-template-columns:1fr}.actions{flex-wrap:wrap}.actions button{flex:1}}