niknok / static /index.html
ljx77qaq's picture
支持实况图(live图):抖音/小红书 live 提取、相册内图视频混发、Web 端可播放可下载
31dfd00 verified
Raw
History Blame Contribute Delete
11.8 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>视频/图文解析</title>
<style>
:root{
--bg:#0c0d10; --card:#16181d; --card2:#1e2128; --line:#282c34;
--fg:#e8eaed; --dim:#9aa0aa; --acc:#ff4d6d; --acc2:#4d8dff; --ok:#31c48d;
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{margin:0;background:var(--bg);color:var(--fg);
font:15px/1.6 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
min-height:100vh;padding:24px 16px 60px}
.wrap{max-width:820px;margin:0 auto}
h1{font-size:22px;margin:0 0 4px;letter-spacing:.5px}
h1 span{background:linear-gradient(90deg,var(--acc),var(--acc2));-webkit-background-clip:text;background-clip:text;color:transparent}
.sub{color:var(--dim);font-size:13px;margin-bottom:18px}
.box{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px}
.row{display:flex;gap:8px}
input,textarea{flex:1;background:var(--card2);border:1px solid var(--line);color:var(--fg);
border-radius:10px;padding:12px 14px;font-size:15px;outline:none;font-family:inherit}
input:focus{border-color:var(--acc2)}
button{background:var(--acc);color:#fff;border:0;border-radius:10px;padding:12px 20px;
font-size:15px;font-weight:600;cursor:pointer;white-space:nowrap}
button:disabled{opacity:.5;cursor:not-allowed}
button.ghost{background:var(--card2);border:1px solid var(--line);color:var(--fg);font-weight:500;padding:8px 14px;font-size:13px}
.hint{color:var(--dim);font-size:12px;margin-top:10px}
.hint b{color:#c9ced6;font-weight:600}
#msg{margin-top:14px;padding:12px 14px;border-radius:10px;font-size:14px;display:none}
#msg.err{background:#3a1c22;color:#ff9aab;display:block}
#msg.info{background:#16262f;color:#8fc7ff;display:block}
#result{margin-top:18px;display:none}
.meta{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:10px}
.pill{background:var(--card2);border:1px solid var(--line);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--dim)}
.title{font-size:15px;white-space:pre-wrap;word-break:break-word;margin:6px 0 10px}
.tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
.tag{color:var(--acc2);font-size:13px}
.author{color:var(--dim);font-size:13px;margin-bottom:12px}
.author a{color:var(--fg);text-decoration:none;border-bottom:1px dashed var(--line)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px}
.cell{position:relative;background:var(--card2);border-radius:10px;overflow:hidden;aspect-ratio:3/4}
.cell img{width:100%;height:100%;object-fit:cover;display:block}
.cell .n{position:absolute;left:6px;top:6px;background:rgba(0,0,0,.6);border-radius:6px;
padding:1px 7px;font-size:11px}
.cell .dl{position:absolute;right:6px;bottom:6px;background:rgba(0,0,0,.65);border-radius:8px;
padding:4px 9px;font-size:12px;color:#fff;text-decoration:none}
.cell video{width:100%;height:100%;object-fit:cover;display:block;border-radius:0;max-height:none}
.cell .lv{position:absolute;right:6px;top:6px;background:rgba(255,255,255,.92);color:#111;
border-radius:6px;padding:1px 6px;font-size:10px;font-weight:700;letter-spacing:.5px}
.cell.live{cursor:pointer}
video{width:100%;border-radius:12px;background:#000;max-height:60vh}
.acts{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
.sp{height:14px}
.loading{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.35);
border-top-color:#fff;border-radius:50%;animation:s .7s linear infinite;vertical-align:-2px;margin-right:6px}
@keyframes s{to{transform:rotate(360deg)}}
@media(max-width:520px){.row{flex-direction:column}button{width:100%}}
</style>
</head>
<body>
<div class="wrap">
<h1><span>解析器</span> · 视频 / 图文</h1>
<div class="sub">抖音 · 小红书 · B站 · YouTube · TikTok · 微博 · 快手 — 无水印直链</div>
<div class="box">
<div class="row">
<input id="url" placeholder="粘贴分享链接,含中文文案也行" autocomplete="off">
<button id="go">解析</button>
</div>
<div class="row" id="pwrow" style="display:none;margin-top:8px">
<input id="pw" type="password" placeholder="访问口令">
</div>
<div class="hint">支持 <b>xhslink.cn</b> / xhslink.com 小红书短链、抖音<b>图文</b>(/note/、/slides/)与视频。</div>
<div id="msg"></div>
</div>
<div id="result" class="box" style="margin-top:14px"></div>
</div>
<script>
const $ = s => document.querySelector(s);
let CFG = {telegram:false, need_password:false, default_chat_id:""};
let LAST = null;
fetch("/api/config").then(r=>r.json()).then(c=>{
CFG = c;
if (c.need_password) $("#pwrow").style.display = "flex";
}).catch(()=>{});
function msg(text, kind){
const m = $("#msg");
if(!text){ m.style.display="none"; return; }
m.className = kind || "info";
m.innerHTML = text;
}
function esc(s){ return (s||"").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;"}[c])); }
async function parse(){
const url = $("#url").value.trim();
if(!url){ msg("请输入链接","err"); return; }
$("#go").disabled = true;
$("#go").innerHTML = '<span class="loading"></span>解析中';
msg("");
$("#result").style.display = "none";
try{
const r = await fetch("/api/parse", {
method:"POST", headers:{"Content-Type":"application/json"},
body: JSON.stringify({url, password: $("#pw").value})
});
const j = await r.json();
if(!j.ok){ msg("❌ " + esc(j.error), "err"); return; }
LAST = j.data;
render(j.data);
}catch(e){
msg("❌ 请求失败:" + esc(String(e)), "err");
}finally{
$("#go").disabled = false;
$("#go").textContent = "解析";
}
}
function fmtDur(s){
s = parseInt(s||0); if(!s) return "";
const m = Math.floor(s/60), x = s%60;
return m + ":" + String(x).padStart(2,"0");
}
function render(d){
const box = $("#result");
const meta = [];
meta.push(`<span class="pill">${esc(d.platform_name||"")}</span>`);
meta.push(`<span class="pill">${d.type === "image" ? "图文 · " + d.count + " 张" : "视频"}</span>`);
if(d.width && d.height) meta.push(`<span class="pill">${d.width}×${d.height}</span>`);
if(d.duration) meta.push(`<span class="pill">${fmtDur(d.duration)}</span>`);
if(d.music) meta.push(`<span class="pill">🎵 ${esc(d.music)}</span>`);
if(d.live_count) meta.push(`<span class="pill">🎞 ${d.live_count} 个实况</span>`);
let media = "";
if(d.type === "image"){
const lv = d.lives_proxy || [];
media = '<div class="grid">' + d.images_proxy.map((u,i)=>{
const v = lv[i] || "";
const name = encodeURIComponent((d.title||'img').slice(0,20)+'_'+(i+1));
if(v){
// 实况图:鼠标移上去 / 点一下就播放,下载给的是 mp4
return `<div class="cell live" onmouseenter="playLive(this)" onmouseleave="stopLive(this)" onclick="toggleLive(this)">
<video src="${v}" poster="${u}" muted loop playsinline preload="none"></video>
<div class="n">${i+1}</div>
<div class="lv">实况</div>
<a class="dl" href="${v}&name=${name}" download onclick="event.stopPropagation()">下载</a>
</div>`;
}
return `<div class="cell">
<img src="${u}" loading="lazy">
<div class="n">${i+1}</div>
<a class="dl" href="${u}&name=${name}" download>下载</a>
</div>`;
}).join("") + "</div>";
} else {
const src = d.video_proxy || d.video_url || "";
media = `<video controls preload="metadata" ${d.cover_proxy?`poster="${d.cover_proxy}"`:""} src="${src}"></video>`;
}
const tgBtn = CFG.telegram
? `<button class="ghost" onclick="sendTG()">发送到 Telegram</button>` : "";
const dlAll = d.type === "image"
? `<button class="ghost" onclick="dlAll()">下载全部 ${d.count} 张</button>` : "";
const dlLive = (d.type === "image" && d.live_count)
? `<button class="ghost" onclick="dlLive()">下载 ${d.live_count} 个实况</button>` : "";
const dlVideo = d.type === "video"
? `<a class="ghost" style="display:inline-block;text-decoration:none;padding:8px 14px;border-radius:10px;background:var(--card2);border:1px solid var(--line);color:var(--fg);font-size:13px"
href="${(d.video_proxy||d.video_url)}&name=${encodeURIComponent((d.title||'video').slice(0,20))}" download>下载视频</a>` : "";
box.innerHTML = `
<div class="meta">${meta.join("")}</div>
${d.title ? `<div class="title">${esc(d.title)}</div>` : ""}
${(d.tags&&d.tags.length) ? `<div class="tags">${d.tags.map(t=>`<span class="tag">${esc(t)}</span>`).join("")}</div>` : ""}
${d.author ? `<div class="author">👤 ${d.author_url?`<a href="${d.author_url}" target="_blank">${esc(d.author)}</a>`:esc(d.author)}</div>` : ""}
${media}
<div class="acts">
${dlAll}${dlLive}${dlVideo}
<button class="ghost" onclick="copyText()">复制文案</button>
<button class="ghost" onclick="copyLinks()">复制直链</button>
${tgBtn}
</div>`;
box.style.display = "block";
box.scrollIntoView({behavior:"smooth", block:"nearest"});
}
function playLive(el){ const v = el.querySelector("video"); if(v) v.play().catch(()=>{}); }
function stopLive(el){ const v = el.querySelector("video"); if(v){ v.pause(); v.currentTime = 0; } }
function toggleLive(el){
const v = el.querySelector("video"); if(!v) return;
v.paused ? v.play().catch(()=>{}) : v.pause();
}
async function dlSeq(urls, base){
for(let i=0;i<urls.length;i++){
if(!urls[i]) continue;
const a = document.createElement("a");
a.href = urls[i] + "&name=" + encodeURIComponent(base+"_"+(i+1));
a.download = "";
document.body.appendChild(a); a.click(); a.remove();
await new Promise(r=>setTimeout(r,350));
}
}
async function dlLive(){
const d = LAST; if(!d) return;
const list = (d.lives_proxy||[]).filter(Boolean);
await dlSeq(list, (d.title||"live").slice(0,20));
msg("已触发 " + list.length + " 个实况下载", "info");
}
async function dlAll(){
const d = LAST; if(!d) return;
for(let i=0;i<d.images_proxy.length;i++){
const a = document.createElement("a");
a.href = d.images_proxy[i] + "&name=" + encodeURIComponent((d.title||"img").slice(0,20)+"_"+(i+1));
a.download = "";
document.body.appendChild(a); a.click(); a.remove();
await new Promise(r=>setTimeout(r,350));
}
msg("已触发 " + d.images_proxy.length + " 个下载", "info");
}
function copyText(){
const d = LAST; if(!d) return;
const t = [d.title||"", (d.tags||[]).join(" ")].filter(Boolean).join("\n");
navigator.clipboard.writeText(t).then(()=>msg("文案已复制","info"));
}
function copyLinks(){
const d = LAST; if(!d) return;
let list = d.type === "image" ? d.images.slice() : [d.video_url];
if(d.type === "image") list = list.concat((d.lives||[]).filter(Boolean));
navigator.clipboard.writeText(list.join("\n")).then(()=>msg("原始直链已复制","info"));
}
async function sendTG(){
const d = LAST; if(!d) return;
let cid = CFG.default_chat_id;
if(!cid){
cid = prompt("Telegram chat id(个人或群组,群组以 -100 开头)", localStorage.getItem("cid")||"");
if(!cid) return;
localStorage.setItem("cid", cid);
}
msg("提交中…","info");
const r = await fetch("/api/send",{method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({url:d.url, chat_id:cid, password:$("#pw").value})});
const j = await r.json();
msg(j.ok ? "✅ " + j.msg : "❌ " + esc(j.error), j.ok?"info":"err");
}
$("#go").addEventListener("click", parse);
$("#url").addEventListener("keydown", e=>{ if(e.key==="Enter") parse(); });
</script>
</body>
</html>