| <!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 => ({"&":"&","<":"<",">":">","\"":"""}[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){ |
| |
| 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> |
|
|