Files
tvbox_collect/jaychouqq/yingshi/html/libvio.html
T
2026-06-29 03:41:14 +00:00

988 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0">
<title>LIBVIO</title>
<style>
*,::before,::after{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0a0a0f;
--bg2:#111118;
--surface:rgba(20,22,30,.75);
--surface-hover:rgba(30,33,45,.85);
--accent:#e74c3c;
--accent2:#f5a623;
--grad:linear-gradient(135deg,#e74c3c,#f5a623);
--text:#eaeef2;
--text2:#8892a4;
--text3:#5a6478;
--line:rgba(255,255,255,.06);
--radius:10px;
--radius-sm:6px;
--shadow:0 4px 24px rgba(0,0,0,.4);
}
html,body{
margin:0;min-height:100%;
background:transparent;color:var(--text);
font-family:-apple-system,"PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif;
font-size:14px;line-height:1.5;
-webkit-font-smoothing:antialiased;
}
html:not(.fm-native),html:not(.fm-native) body{background:var(--bg)}
::-webkit-scrollbar{width:4px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--text3);border-radius:2px}
::selection{background:var(--accent);color:#fff}
/* ── Header ── */
.header{
position:sticky;top:0;z-index:100;
padding:12px 16px;
background:rgba(10,10,15,.88);
backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
display:flex;align-items:center;gap:10px;
border-bottom:1px solid var(--line);
flex-wrap:wrap;
}
.header-logo{
font-size:20px;font-weight:800;flex-shrink:0;
background:var(--grad);-webkit-background-clip:text;-webkit-text-fill-color:transparent;
letter-spacing:-.5px;
}
.nav-wrap{display:flex;gap:3px;flex-shrink:0;overflow-x:auto;scrollbar-width:none;}
.nav-wrap::-webkit-scrollbar{display:none}
.nav-tab{
padding:6px 14px;border-radius:20px;font-size:13px;font-weight:500;
cursor:pointer;background:transparent;color:var(--text2);
transition:all .25s;white-space:nowrap;border:1px solid transparent;
}
.nav-tab:hover{color:var(--text);background:var(--surface)}
.nav-tab.on{background:var(--grad);color:#fff;border-color:transparent}
.nav-tab:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
.search-box{display:flex;gap:6px;width:100%}
.search-row{
padding:8px 16px 10px;
background:rgba(10,10,15,.75);
backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
border-bottom:1px solid var(--line);
}
.search-box input{
flex:1;padding:8px 14px;border-radius:20px;border:1px solid var(--line);
background:var(--surface);color:var(--text);font-size:13px;
outline:none;min-width:80px;transition:border-color .2s;
}
.search-box input:focus{border-color:var(--accent2)}
.search-box input::placeholder{color:var(--text3)}
.search-box button{
padding:8px 16px;border:none;border-radius:20px;
background:var(--grad);color:#fff;font-size:13px;font-weight:600;
cursor:pointer;white-space:nowrap;transition:opacity .2s;
}
.search-box button:hover{opacity:.9}
.search-box button:focus-visible{outline:2px solid #fff;outline-offset:2px}
/* ── Content ── */
.scroll-area{padding:4px 16px 24px}
.section{margin:18px 0}
.section-head{
display:flex;align-items:center;gap:10px;margin-bottom:10px;
}
.section-head h2{
font-size:17px;font-weight:700;position:relative;
}
.section-head h2::after{
content:'';position:absolute;bottom:-2px;left:0;width:28px;height:3px;
background:var(--grad);border-radius:2px;
}
.section-head .more{
margin-left:auto;font-size:12px;color:var(--text2);
cursor:pointer;padding:4px 12px;border-radius:14px;
border:1px solid var(--line);transition:all .2s;
}
.section-head .more:hover{color:var(--accent2);border-color:var(--accent2)}
/* ── Card Grid ── */
.card-grid{
display:grid;
grid-template-columns:repeat(auto-fill,minmax(120px,1fr));
gap:10px;
}
.card{
border-radius:var(--radius);overflow:hidden;cursor:pointer;
background:var(--surface);border:1px solid var(--line);
transition:transform .3s cubic-bezier(.34,1.56,.64,1),box-shadow .3s,background .2s;
position:relative;
}
.card:hover{transform:translateY(-4px) scale(1.02);box-shadow:0 8px 32px rgba(0,0,0,.5);background:var(--surface-hover)}
.card:active{transform:scale(.97)}
.card:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
.card-img-wrap{position:relative;overflow:hidden;background:var(--bg2)}
.card-img{
width:100%;aspect-ratio:2/3;object-fit:cover;display:block;
transition:transform .4s;
}
.card:hover .card-img{transform:scale(1.05)}
.card-rating{
position:absolute;top:6px;right:6px;
background:rgba(0,0,0,.75);color:var(--accent2);font-size:10px;font-weight:700;
padding:2px 7px;border-radius:4px;z-index:2;backdrop-filter:blur(4px);
border:1px solid rgba(245,166,35,.2);
}
.card-badge{
position:absolute;bottom:0;left:0;right:0;
background:linear-gradient(transparent 20%,rgba(0,0,0,.9));
padding:22px 7px 5px;text-align:right;font-size:11px;color:#fff;
font-weight:500;letter-spacing:.3px;
}
.card-body{padding:8px 8px 9px}
.card-title{
font-size:13px;font-weight:500;overflow:hidden;
text-overflow:ellipsis;white-space:nowrap;color:var(--text);
}
/* ── Detail Overlay ── */
.detail-view{
position:fixed;inset:0;z-index:200;
background:transparent;overflow-y:auto;
display:none;-webkit-overflow-scrolling:touch;
}
.detail-view.active{display:block;animation:fadeIn .25s ease}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
body.detail-active .app{visibility:hidden;pointer-events:none}
.detail-bg{
position:fixed;inset:0;
background:rgba(0,0,0,.6);
backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);
}
.detail-content{
position:relative;z-index:2;max-width:800px;margin:0 auto;padding:16px;
}
.detail-back{
display:inline-flex;align-items:center;gap:6px;
padding:8px 16px 8px 8px;margin-bottom:12px;border-radius:20px;
cursor:pointer;font-size:14px;font-weight:600;color:var(--text2);
background:var(--surface);border:1px solid var(--line);
transition:all .2s;backdrop-filter:blur(8px);
}
.detail-back:hover{color:var(--text);border-color:var(--text3)}
.detail-back:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
.detail-hero{
display:flex;gap:16px;flex-wrap:wrap;
background:var(--surface);border-radius:var(--radius);
padding:16px;border:1px solid var(--line);
backdrop-filter:blur(8px);
}
.detail-poster{
width:120px;border-radius:var(--radius-sm);flex-shrink:0;
aspect-ratio:2/3;object-fit:cover;box-shadow:0 4px 20px rgba(0,0,0,.5);
}
.detail-info{flex:1;min-width:180px}
.detail-title{font-size:20px;font-weight:700;margin-bottom:4px;line-height:1.3}
.detail-meta{
display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;
}
.detail-meta span{
font-size:12px;color:var(--text2);background:var(--bg2);
padding:2px 10px;border-radius:12px;border:1px solid var(--line);
}
.detail-field{font-size:12px;margin-bottom:3px;line-height:1.6}
.detail-field-label{color:var(--accent2);font-weight:500}
.detail-field-value{color:var(--text2)}
.detail-desc{
font-size:13px;color:var(--text2);line-height:1.7;
margin:12px 0;padding:12px;background:var(--bg2);
border-radius:var(--radius-sm);border-left:3px solid var(--accent);
}
.play-btn{
display:inline-flex;align-items:center;gap:6px;
padding:10px 28px;border:none;border-radius:24px;
background:var(--grad);color:#fff;font-size:14px;font-weight:700;
cursor:pointer;transition:all .25s;margin:4px 0;
box-shadow:0 4px 16px rgba(231,76,60,.3);
}
.play-btn:hover{transform:translateY(-2px);box-shadow:0 6px 24px rgba(231,76,60,.4)}
.play-btn:active{transform:translateY(0)}
.play-btn:focus-visible{outline:2px solid #fff;outline-offset:3px}
/* ── Episode Grid ── */
.episode-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.episode-section{margin-top:14px}
.episode-section .label{
font-size:13px;font-weight:600;margin-bottom:8px;color:var(--text2);
}
.episode-btn{
padding:7px 16px;border-radius:18px;border:1px solid var(--line);
background:var(--surface);color:var(--text);font-size:12px;font-weight:500;
cursor:pointer;transition:all .2s;
}
.episode-btn:hover{background:var(--surface-hover);border-color:var(--text3)}
.episode-btn.on{background:var(--accent);border-color:var(--accent);color:#fff}
.episode-btn:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
/* ── Loading ── */
.loading{
display:flex;flex-direction:column;align-items:center;justify-content:center;
padding:60px 20px;color:var(--text2);font-size:14px;gap:12px;
}
.loading-spinner{
width:36px;height:36px;border:3px solid var(--line);
border-top-color:var(--accent);border-radius:50%;
animation:spin .7s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* ── Pagination ── */
.pagination-bar{
display:flex;gap:5px;justify-content:center;padding:14px 0;flex-wrap:wrap;
}
.page-btn{
padding:6px 14px;border-radius:16px;border:1px solid var(--line);
background:var(--surface);color:var(--text2);font-size:12px;
cursor:pointer;transition:all .2s;font-weight:500;
}
.page-btn:hover{background:var(--surface-hover);color:var(--text)}
.page-btn.on{background:var(--grad);color:#fff;border-color:transparent}
.page-btn:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
/* ── Error ── */
.error-msg{text-align:center;padding:40px 20px;color:var(--accent2);font-size:14px;}
/* ── Rank List ── */
.rank-list{list-style:none}
.rank-item{
display:flex;align-items:center;gap:10px;
padding:10px 12px;border-bottom:1px solid var(--line);cursor:pointer;
border-radius:var(--radius-sm);transition:background .2s;
}
.rank-item:hover{background:var(--surface)}
.rank-item:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
.rank-item:last-child{border-bottom:none}
.rank-num{
width:24px;text-align:center;font-weight:800;font-size:14px;
color:var(--text3);flex-shrink:0;
}
.rank-num.r1{color:#f5a623}
.rank-num.r2{color:#c0c0c0}
.rank-num.r3{color:#cd7f32}
.rank-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}
.rank-date{font-size:11px;color:var(--text3);flex-shrink:0}
/* ── Skeleton ── */
.skeleton-row{
display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));
gap:10px;margin-bottom:16px;
}
.skeleton-card{
aspect-ratio:2/3;border-radius:var(--radius);
background:var(--surface);position:relative;overflow:hidden;
}
.skeleton-card::after{
content:'';position:absolute;inset:0;
background:linear-gradient(90deg,transparent,rgba(255,255,255,.04),transparent);
animation:shimmer 1.5s infinite;
}
@keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
/* ── PanSou / 网盘搜索 ── */
.pansou-head{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
.pansou-head input{flex:1;padding:8px 14px;border-radius:20px;border:1px solid var(--line);background:var(--surface);color:var(--text);font-size:13px;outline:none;min-width:120px}
.pansou-head input:focus{border-color:var(--accent2)}
.pansou-head button{padding:8px 20px;border:none;border-radius:20px;background:var(--grad);color:#fff;font-size:13px;font-weight:600;cursor:pointer}
.disk-types{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px}
.disk-type{padding:4px 12px;border-radius:14px;border:1px solid var(--line);background:transparent;color:var(--text2);font-size:11px;cursor:pointer;transition:all .2s}
.disk-type.on{background:var(--accent2);color:#fff;border-color:var(--accent2)}
.disk-type:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
.pansou-result{display:flex;flex-direction:column;gap:6px}
.pansou-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:var(--radius-sm);background:var(--surface);border:1px solid var(--line);cursor:pointer;transition:all .15s}
.pansou-item:hover{background:var(--surface-hover)}
.pansou-item:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
.pansou-item .pi-title{flex:1;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pansou-item .pi-type{font-size:10px;color:var(--accent2);background:rgba(245,166,35,.12);padding:1px 7px;border-radius:8px;flex-shrink:0}
.pansou-item .pi-status{width:16px;height:16px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:9px}
.pi-ok{background:#27ae60;color:#fff}
.pi-bad{background:#c0392b;color:#fff}
.pi-uncertain{background:#7f8c8d;color:#fff}
.pi-pending{background:var(--text3);color:var(--bg)}
.pi-none{display:none}
.pansou-info{text-align:center;padding:24px;color:var(--text3);font-size:13px}
/* ── Responsive ── */
@media(min-width:600px){.card-grid{grid-template-columns:repeat(auto-fill,minmax(145px,1fr))}}
@media(min-width:900px){.card-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr))}}
@media(min-width:1200px){.card-grid{grid-template-columns:repeat(auto-fill,minmax(175px,1fr))}}
@media(max-width:480px){
.header{padding:10px 10px;gap:6px}
.header-logo{font-size:17px}
.nav-tab{padding:4px 10px;font-size:12px}
.search-box{max-width:140px}
.scroll-area{padding:4px 10px 20px}
.card-grid{grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:7px}
.detail-hero{flex-direction:column;align-items:center}
.detail-poster{width:100px}
}
</style>
</head>
<body>
<div class="app" id="app">
<div class="header">
<span class="header-logo">LIBVIO</span>
<div class="nav-wrap">
<div class="nav-tab on" data-tab="home" tabindex="0">首页</div>
<div class="nav-tab" data-tab="1" tabindex="0">电影</div>
<div class="nav-tab" data-tab="2" tabindex="0">剧集</div>
<div class="nav-tab" data-tab="4" tabindex="0">番剧</div>
<div class="nav-tab" data-tab="3" tabindex="0">综艺</div>
<div class="nav-tab" data-tab="pansou" tabindex="0">网盘</div>
<div class="nav-tab" data-tab="trending" tabindex="0">推荐</div>
</div>
</div>
<div class="search-row">
<div class="search-box">
<input id="searchInput" placeholder="搜索电影、剧集..." maxlength="30">
<button id="searchBtn">搜索</button>
</div>
</div>
<div class="scroll-area" id="mainContent">
<div class="loading"><div class="loading-spinner"></div>加载中...</div>
</div>
</div>
<div class="detail-view" id="detailView">
<div class="detail-bg" id="detailBg"></div>
<div class="detail-content" id="detailContent"></div>
</div>
<script>
var BASE = 'https://www.libvio.run';
var TYPE_NAMES = { '1':'电影','2':'剧集','3':'综艺','4':'番剧','15':'日韩','16':'欧美' };
function _fm() {
return new Promise(function (r) {
if (window.fm) return r(window.fm);
var t = 0;
(function c() { if (window.fm) return r(window.fm); if (++t > 30) return r(null); setTimeout(c,100); })();
});
}
if (window.fongmiBridge || window.fm || window.fongmi)
document.documentElement.classList.add('fm-native');
function _req(url) {
return _fm().then(function (f) {
if (f && f.req) return f.req(url, { timeout:20 });
return fetch(url, { headers:{ 'User-Agent':'Mozilla/5.0' } }).then(function(r){return r.text();}).then(function(t){return{body:t};});
}).then(function(res){
return (res && res.body != null) ? (typeof res.body === 'string' ? res.body : JSON.stringify(res.body)) : '';
});
}
function _img(url) {
if (!url) return '';
try { if (window.fm && fm.res) return fm.res(url, { credentials:'include' }); } catch(e) {}
return url;
}
function _play(url, title) {
var fullUrl = url.indexOf('://') > 0 ? url : BASE + url;
if (fullUrl.indexOf('/w/') > 0) {
_playDecrypt(fullUrl, title);
return;
}
_fm().then(function (f) {
if (f && f.play) return f.play(fullUrl, title || '', {});
window.open(fullUrl);
});
}
function _playDecrypt(playUrl, title) {
_fm().then(function (f) {
if (!f || !f.req) { _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); return; }
f.req(playUrl, {timeout:20}).then(function(r1){
var h1 = r1 && r1.body;
if (typeof h1 !== 'string') h1 = h1 ? JSON.stringify(h1) : '';
// Check if this is a netdisk-only resource
if (h1.indexOf('netdisk') >= 0 && h1.indexOf('player_aaaa') < 0) {
_showNetdiskNotice(playUrl, title);
return;
}
var pi = h1.indexOf('player_aaaa');
if (pi < 0) { _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); return; }
var ch = h1.substring(pi, pi+5000);
var br = ch.indexOf('{');
var en = ch.indexOf('</scr' + 'ipt>');
if (br < 0 || en <= br) { _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); return; }
var player;
try { player = JSON.parse(ch.substring(br, en)); } catch(e) { _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); return; }
var encUrl = player.url;
if (!encUrl) { _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); return; }
var vid = player.id || '';
var nid = player.nid || '1';
var ty4 = BASE + '/vid/ty4.php?url=' + encodeURIComponent(encUrl) + '&next=&id=' + vid + '&nid=' + nid;
f.req(ty4, {timeout:20}).then(function(r2){
var h2 = r2 && r2.body;
if (typeof h2 !== 'string') h2 = h2 ? JSON.stringify(h2) : '';
var pm = h2.match(/"parseUrl"\s*:\s*"([^"]+)"/);
if (!pm) { _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); return; }
var pp = pm[1].replace(/\\u0026/g, '&');
var body = JSON.stringify({url: encUrl});
f.req(BASE + pp, {method:'POST', headers:{'Content-Type':'application/json'}, body:body, timeout:20})
.then(function(r3){
var data = r3 && r3.body;
if (data && typeof data === 'object' && data.url) {
if (f && f.play) f.play(data.url, title||'', {});
else window.open(data.url);
} else {
_fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});});
}
})['catch'](function(){ _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); });
})['catch'](function(){ _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); });
})['catch'](function(){ _fm().then(function(g){if(g&&g.play)g.play(playUrl,title||'',{});}); });
});
}
function _showNetdiskNotice(playUrl, title) {
// Extract netdisk links from play page
_fm().then(function(f){
if (!f || !f.req) return;
f.req(playUrl, {timeout:20}).then(function(r){
var html = r && r.body;
if (typeof html !== 'string') html = html ? JSON.stringify(html) : '';
// Parse netdisk panels
var nds = [];
var ndRe = /<div class="netdisk-list">([\s\S]*?)<\/div>/g;
var nm;
while ((nm = ndRe.exec(html)) !== null) {
var links = nm[1].match(/<a[^>]+href="([^"]+)"[^>]*>[\s\S]*?<span class="netdisk-name">([^<]+)<\/span>/);
if (links) nds.push({url: links[1], name: links[2].trim()});
}
// Also check for "panel-head netdisk-head" titles
var sections = [];
var shRe = /<div class="panel-head netdisk-head">([\s\S]*?)<\/div>/g;
while ((nm = shRe.exec(html)) !== null) {
var tm = nm[1].match(/<h3>([^<]+)<\/h3>/);
if (tm) sections.push(tm[1].trim());
}
var msg = '⚠️ 该资源仅提供网盘下载\n';
for (var i = 0; i < sections.length; i++) {
msg += '\n' + sections[i];
if (nds[i]) msg += ': ' + nds[i].url;
}
// Show in overlay (native alert doesn't work on TVBox, use custom overlay)
_showOverlay(msg);
});
});
}
function _showOverlay(msg) {
var old = document.getElementById('msg-overlay');
if (old) old.remove();
var ov = document.createElement('div');
ov.id = 'msg-overlay';
ov.style.cssText = 'position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.8);display:flex;align-items:center;justify-content:center;padding:30px;';
var box = document.createElement('div');
box.style.cssText = 'background:var(--surface,#1a1e26);border-radius:12px;padding:24px;max-width:400px;width:100%;color:#eaeef2;font-size:13px;line-height:1.7;white-space:pre-wrap;word-break:break-all;border:1px solid rgba(255,255,255,.1);';
box.textContent = msg;
var closeBtn = document.createElement('button');
closeBtn.textContent = '关闭';
closeBtn.style.cssText = 'margin-top:12px;padding:8px 24px;border:none;border-radius:20px;background:var(--accent,#e74c3c);color:#fff;font-size:13px;font-weight:600;cursor:pointer;display:block;width:100%;';
closeBtn.onclick = function(){ ov.remove(); };
box.appendChild(closeBtn);
ov.appendChild(box);
document.body.appendChild(ov);
ov.onclick = function(e){ if (e.target === ov) ov.remove(); };
}
function _esc(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function parseCardsFromList(listHtml) {
var cards = [];
var re = /<li[^>]*>([\s\S]*?)<\/li>/g;
var m;
while ((m = re.exec(listHtml)) !== null) {
var block = m[1];
if (block.indexOf('stui-vodlist__box') < 0) continue;
var hm = block.match(/href="(\/detail\/\d+\.html)"/);
if (!hm) continue;
var href = hm[1];
var idm = href.match(/(\d+)\.html/);
if (!idm) continue;
var im = block.match(/data-original="([^"]+)"/);
var img = im ? im[1] : '';
if (!img) { var im2 = block.match(/src="([^"]+)"/); if (im2) img = im2[1]; }
var tm = block.match(/title="([^"]+)"/);
var title = tm ? tm[1] : '';
var rm = block.match(/pic-tag pic-tag-top">([^<]+)/);
var rate = rm ? rm[1] : '';
var bm = block.match(/pic-text[^"]*"[^>]*>([^<]+)/);
var badge = bm ? bm[1].trim() : '';
cards.push({ id:idm[1], title:title, img:img, rate:rate, badge:badge, href:href });
}
return cards;
}
function renderCard(c) {
var imgUrl = _img(c.img);
var h = '<div class="card" data-href="' + _esc(c.href) + '" tabindex="0">';
h += '<div class="card-img-wrap">';
h += '<img class="card-img" src="' + _esc(imgUrl) + '" alt="' + _esc(c.title) + '" loading="lazy" onerror="this.style.display=\'none\'">';
if (c.badge) h += '<div class="card-badge">' + _esc(c.badge) + '</div>';
if (c.rate && c.rate !== '0.0') h += '<div class="card-rating">' + _esc(c.rate) + '</div>';
h += '</div><div class="card-body"><div class="card-title">' + _esc(c.title) + '</div></div></div>';
return h;
}
function showSkeleton() {
var el = document.getElementById('mainContent');
var h = '';
for (var s = 0; s < 3; s++) {
h += '<div class="skeleton-row">';
for (var i = 0; i < 8; i++) h += '<div class="skeleton-card"></div>';
h += '</div>';
}
el.innerHTML = h;
}
function loadHome() {
showSkeleton();
_req(BASE + '/').then(function(html){
if (!html) { document.getElementById('mainContent').innerHTML = '<div class="error-msg">加载失败</div>'; return; }
renderHome(html);
});
}
function renderHome(html) {
var el = document.getElementById('mainContent');
var h = '';
var headRe = /<div class="stui-vodlist__head">([\s\S]*?)<\/div>\s*<ul class="stui-vodlist[^"]*"[^>]*>([\s\S]*?)<\/ul>/g;
var hm;
while ((hm = headRe.exec(html)) !== null) {
var headHtml = hm[1];
var listHtml = hm[2];
var lm = headHtml.match(/<h3[^>]*>[\s\S]*?>\s*([^<]+?)\s*</);
var label = lm ? lm[1].trim() : '';
if (!label) continue;
var tm = headHtml.match(/\/show\/(\d+)-/);
var typeId = tm ? tm[1] : '';
var cards = parseCardsFromList(listHtml);
if (cards.length === 0) continue;
h += '<div class="section">';
h += '<div class="section-head"><h2>' + _esc(label) + '</h2>';
if (typeId) h += '<span class="more" data-type="' + typeId + '" tabindex="0">更多 </span>';
h += '</div><div class="card-grid">';
for (var i = 0; i < cards.length; i++) h += renderCard(cards[i]);
h += '</div></div>';
}
el.innerHTML = h || '<div class="error-msg">首页加载失败</div>';
bindEvents(el);
}
function loadCategory(typeId, page) {
page = page || 1;
showSkeleton();
var url = page <= 1 ? (BASE + '/show/' + typeId + '-----------.html') : (BASE + '/show/' + typeId + '--------' + page + '---.html');
_req(url).then(function(html){
if (!html) { document.getElementById('mainContent').innerHTML = '<div class="error-msg">加载失败</div>'; return; }
renderCategory(html, typeId, page);
});
}
function renderCategory(html, typeId, page) {
var el = document.getElementById('mainContent');
var ulm = html.match(/<ul class="stui-vodlist[^"]*"[^>]*>([\s\S]*?)<\/ul>/);
if (!ulm) { el.innerHTML = '<div class="error-msg">没有内容</div>'; return; }
var cards = parseCardsFromList(ulm[1]);
var name = TYPE_NAMES[typeId] || ('分类' + typeId);
var h = '<div class="section"><div class="section-head"><h2>' + name + '</h2></div>';
h += '<div class="card-grid">';
for (var i = 0; i < cards.length; i++) h += renderCard(cards[i]);
h += '</div>';
var maxPage = 1;
var pgRe = new RegExp('/show/' + typeId + '--------(\\d+)---\\.html', 'g');
var pm;
while ((pm = pgRe.exec(html)) !== null) { var pn = parseInt(pm[1]); if (pn > maxPage) maxPage = pn; }
var totalm = html.match(/>\d+\/(\d+)</);
if (totalm) { var tm2 = parseInt(totalm[1]); if (tm2 > maxPage) maxPage = tm2; }
if (maxPage > 1) {
h += '<div class="pagination-bar">';
if (page > 1) h += '<button class="page-btn pg-btn" data-type="' + typeId + '" data-page="' + (page-1) + '" tabindex="0"> 上一页</button>';
var start = Math.max(1, page - 3);
var end = Math.min(maxPage, page + 3);
for (var pp = start; pp <= end; pp++) h += '<button class="page-btn pg-btn' + (pp === page ? ' on' : '') + '" data-type="' + typeId + '" data-page="' + pp + '" tabindex="0">' + pp + '</button>';
if (page < maxPage) h += '<button class="page-btn pg-btn" data-type="' + typeId + '" data-page="' + (page+1) + '" tabindex="0">下一页 </button>';
h += '</div>';
}
h += '</div>';
el.innerHTML = h;
bindEvents(el);
}
function doSearch(keyword) {
if (!keyword.trim()) return;
showSkeleton();
_req(BASE + '/search/-------------.html?wd=' + encodeURIComponent(keyword)).then(function(html){
var el = document.getElementById('mainContent');
if (!html) { el.innerHTML = '<div class="error-msg">搜索失败</div>'; return; }
var ulm = html.match(/<ul class="stui-vodlist[^"]*"[^>]*>([\s\S]*?)<\/ul>/);
if (!ulm) { el.innerHTML = '<div class="section"><div class="section-head"><h2>"' + _esc(keyword) + '" 无结果</h2></div></div>'; return; }
var cards = parseCardsFromList(ulm[1]);
var h = '<div class="section"><div class="section-head"><h2>"' + _esc(keyword) + '" 搜索结果 (' + cards.length + ')</h2></div><div class="card-grid">';
for (var i = 0; i < cards.length; i++) h += renderCard(cards[i]);
h += '</div></div>';
el.innerHTML = h;
bindEvents(el);
});
}
function loadDetail(href) {
var el = document.getElementById('detailContent');
var view = document.getElementById('detailView');
view.classList.add('active');
document.body.classList.add('detail-active');
el.innerHTML = '<div class="loading"><div class="loading-spinner"></div>加载中...</div>';
_req(BASE + href).then(function(html){
if (!html) { el.innerHTML = '<div class="error-msg">加载失败</div>'; return; }
renderDetail(html, el, href);
});
}
function renderDetail(html, el, href) {
var poster = '', title = '', year = '', rate = '', desc = '';
var director = '', cast = '', genre = '', country = '';
var pm = html.match(/<div class="vod-poster__wrap">[\s\S]*?data-original="([^"]+)"/);
if (pm) poster = pm[1];
if (!poster) { var pm2 = html.match(/id="js-poster-img"[^>]+data-original="([^"]+)"/); if (pm2) poster = pm2[1]; }
var tm = html.match(/<h1[^>]*class="title">([^<]+)<\/h1>/);
if (tm) title = tm[1];
var metaItems = [];
var miRe = /<span class="meta-item">([^<]+)<\/span>/g;
var mi;
while ((mi = miRe.exec(html)) !== null) metaItems.push(mi[1].trim());
var genreSet = ['剧情','喜剧','动作','爱情','科幻','恐怖','悬疑','奇幻','冒险','犯罪','战争','动画','纪录片','惊悚','历史','传记','家庭','音乐','同性','歌舞','古装','运动','灾难','武侠','西部','儿童','短片','黑色电影','戏曲'];
for (var mi2 = 0; mi2 < metaItems.length; mi2++) {
var val = metaItems[mi2];
if (/^\d{4}$/.test(val)) { year = val; continue; }
if (/^\d{4}-\d{2}-\d{2}/.test(val)) continue;
var parts = val.split(',');
var isGenre = false;
for (var p = 0; p < parts.length; p++) { if (genreSet.indexOf(parts[p].trim()) >= 0) { isGenre = true; break; } }
if (isGenre) { genre = val; continue; }
var countryHints = ['国','美国','英国','法国','德国','韩国','日本','台湾','香港','意大利','西班牙','加拿大','澳大利亚','印度','泰国','俄罗斯'];
for (var ch = 0; ch < countryHints.length; ch++) { if (val.indexOf(countryHints[ch]) >= 0 && val.length < 20) { country = val; break; } }
}
var dm = html.match(/导演:<span>([\s\S]*?)<\/span>/);
if (dm) director = dm[1].replace(/<[^>]+>/g,'').trim();
var cm = html.match(/主演:<span>([\s\S]*?)<\/span>/);
if (cm) cast = cm[1].replace(/<[^>]+>/g,'').trim();
var rm = html.match(/class="score">([^<]+)/);
if (rm) rate = rm[1].replace('分','');
var dsm = html.match(/<span class="detail-content"[\s\S]*?>([\s\S]*?)<\/span>/);
if (dsm) desc = dsm[1].replace(/<[^>]+>/g,'').trim();
if (!desc) { var dsm2 = html.match(/<p class="vod-desc">([\s\S]*?)<\/p>/); if (dsm2) desc = dsm2[1].replace(/<[^>]+>/g,'').trim(); }
// Extract episodes - try detail page playlist first
var episodes = [];
var plm = html.match(/<ul class="stui-content__playlist[^"]*"[^>]*>([\s\S]*?)<\/ul>/);
if (plm) {
var epRe = /<a href="(\/w\/\d+-\d+-\d+\.html)">([^<]+)<\/a>/g;
var epm;
while ((epm = epRe.exec(plm[1])) !== null) episodes.push({ url: epm[1], label: epm[2].trim() });
}
// If no episodes from detail page, fetch play page to get episodes
if (episodes.length === 0 && title) {
// Extract vod_id from href
var vid = href.match(/(\d+)\.html/);
if (vid) {
// We'll fetch the play page asynchronously to get episodes
// For now, add a placeholder that will use fm.play with the play URL
// The actual episode fetch happens in a follow-up
}
}
if (episodes.length === 0) {
var fpm = html.match(/<a href="(\/w\/\d+-\d+-\d+\.html)">立即播放<\/a>/);
if (fpm) episodes.push({ url: fpm[1], label: '播放' });
}
// Last resort: construct play URL from vod_id
if (episodes.length === 0) {
var idMatch = href.match(/(\d+)\.html/);
if (idMatch) {
// Try fetching play page for episodes (async)
episodes.push({ url: '/w/' + idMatch[1] + '-1-1.html', label: '正在加载...' });
// Fetch play page in background to get actual episodes
(function(vid, title) {
_req(BASE + '/w/' + vid + '-1-1.html').then(function(ph) {
if (!ph) return;
var pm = ph.match(/<ul class="stui-play__list[^"]*"[^>]*>([\s\S]*?)<\/ul>/g);
if (!pm) return;
// Find the first BD/play section (not netdisk)
var newEps = [];
for (var pi = 0; pi < pm.length; pi++) {
var epRe2 = /<a href="(\/w\/\d+-\d+-\d+\.html)">([^<]+)<\/a>/g;
var em2;
while ((em2 = epRe2.exec(pm[pi])) !== null) {
newEps.push({ url: em2[1], label: em2[2].trim() });
}
if (newEps.length > 0) break; // Use first playlist section (BD)
}
if (newEps.length > 0) {
// Re-render the episode section
var epContainer = document.getElementById('ep-section-' + vid);
if (epContainer) {
var hh = '<div class="episode-section"><div class="label">选集 (' + newEps.length + ')</div><div class="episode-grid">';
for (var ei = 0; ei < newEps.length; ei++) {
hh += '<button class="episode-btn play-action" data-url="' + newEps[ei].url + '" data-title="' + _esc(title) + ' ' + newEps[ei].label + '" tabindex="0">' + _esc(newEps[ei].label) + '</button>';
}
hh += '</div></div>';
epContainer.innerHTML = hh;
// Rebind play actions
var pbs2 = epContainer.querySelectorAll('.play-action');
for (var xi = 0; xi < pbs2.length; xi++) {
pbs2[xi].onclick = function() { _play(this.getAttribute('data-url'), this.getAttribute('data-title')); };
}
}
}
});
})(idMatch[1], el);
}
}
var h = '';
h += '<div class="detail-back" id="detailBack" tabindex="0"><span> 返回</span></div>';
h += '<div class="detail-hero">';
h += '<img class="detail-poster" src="' + _img(poster) + '" alt="' + _esc(title) + '" onerror="this.style.display=\'none\'">';
h += '<div class="detail-info">';
h += '<div class="detail-title">' + _esc(title) + '</div>';
h += '<div class="detail-meta">';
if (year) h += '<span>' + _esc(year) + '</span>';
if (rate) h += '<span>' + _esc(rate) + '</span>';
if (country) h += '<span>' + _esc(country) + '</span>';
h += '</div>';
if (genre) h += '<div class="detail-field"><span class="detail-field-label">类型:</span><span class="detail-field-value">' + _esc(genre) + '</span></div>';
if (director) h += '<div class="detail-field"><span class="detail-field-label">导演:</span><span class="detail-field-value">' + _esc(director) + '</span></div>';
if (cast) h += '<div class="detail-field"><span class="detail-field-label">主演:</span><span class="detail-field-value">' + _esc(cast) + '</span></div>';
h += '</div></div>';
if (desc) h += '<div class="detail-desc">' + _esc(desc) + '</div>';
if (episodes.length === 1) {
var vidSingle = href.match(/(\d+)\.html/);
var secIdSingle = vidSingle ? ' id="ep-section-' + vidSingle[1] + '"' : '';
h += '<div' + secIdSingle + '><button class="play-btn play-action" data-url="' + episodes[0].url + '" data-title="' + _esc(title) + '" tabindex="0">▶ ' + (episodes[0].label === '正在加载...' ? '加载中...' : '播放') + '</button></div>';
}
if (episodes.length > 1) {
var vidForSection = href.match(/(\d+)\.html/);
var secId = vidForSection ? ' id="ep-section-' + vidForSection[1] + '"' : '';
h += '<div' + secId + ' class="episode-section"><div class="label">选集 (' + episodes.length + ')</div><div class="episode-grid">';
for (var eii = 0; eii < episodes.length; eii++) {
h += '<button class="episode-btn play-action" data-url="' + episodes[eii].url + '" data-title="' + _esc(title) + ' ' + episodes[eii].label + '" tabindex="0">' + _esc(episodes[eii].label) + '</button>';
}
h += '</div></div>';
}
el.innerHTML = h;
document.getElementById('detailBack').onclick = closeDetail;
document.getElementById('detailBg').onclick = closeDetail;
var pbs = el.querySelectorAll('.play-action');
for (var i = 0; i < pbs.length; i++) {
pbs[i].onclick = function() { _play(this.getAttribute('data-url'), this.getAttribute('data-title')); };
}
}
function closeDetail() {
document.getElementById('detailView').classList.remove('active');
document.body.classList.remove('detail-active');
}
function bindEvents(el) {
var cards = el.querySelectorAll('.card');
for (var i = 0; i < cards.length; i++) {
cards[i].onclick = function() { var h = this.getAttribute('data-href'); if (h) loadDetail(h); };
cards[i].onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); var h = this.getAttribute('data-href'); if (h) loadDetail(h); } };
}
var mores = el.querySelectorAll('.more[data-type]');
for (var i = 0; i < mores.length; i++) {
mores[i].onclick = function() { setTab(this.getAttribute('data-type')); };
mores[i].onkeydown = function(e) { if (e.key === 'Enter') { e.preventDefault(); setTab(this.getAttribute('data-type')); } };
}
var pbs = el.querySelectorAll('.pg-btn');
for (var i = 0; i < pbs.length; i++) {
pbs[i].onclick = function() { loadCategory(this.getAttribute('data-type'), parseInt(this.getAttribute('data-page'))); };
pbs[i].onkeydown = function(e) { if (e.key === 'Enter') { e.preventDefault(); loadCategory(this.getAttribute('data-type'), parseInt(this.getAttribute('data-page'))); } };
}
}
/* ── PanSou / 网盘搜索 ── */
var PAN_API = 'https://so.252035.xyz';
var ALL_DISKS = ['quark','aliyun','baidu','uc','tianyi','xunlei','123','115','mobile'];
var DISK_LABELS = {quark:'夸克',aliyun:'阿里',baidu:'百度',uc:'UC',tianyi:'天翼',xunlei:'迅雷','123':'123盘',mobile:'移动',mag:'磁力'};
function loadPanSou() {
var el = document.getElementById('mainContent');
var h = '<div class="section"><div class="section-head"><h2>网盘搜索</h2></div>';
h += '<div class="pansou-head">';
h += '<input id="panKeyword" placeholder="搜索资源名称..." maxlength="50">';
h += '<button id="panSearchBtn">搜索</button></div>';
h += '<div class="disk-types" id="diskTypes">';
for (var k in DISK_LABELS) {
if (k === 'mag') continue;
h += '<span class="disk-type on" data-disk="' + k + '" tabindex="0">' + DISK_LABELS[k] + '</span>';
}
h += '</div>';
h += '<div id="panResult" class="pansou-info">输入关键词搜索网盘资源</div>';
h += '</div>';
el.innerHTML = h;
document.getElementById('panSearchBtn').onclick = function(){ panSearch(); };
document.getElementById('panKeyword').addEventListener('keydown', function(e){ if (e.key === 'Enter') panSearch(); });
var dts = el.querySelectorAll('.disk-type');
for (var i = 0; i < dts.length; i++) {
dts[i].onclick = function(){ this.classList.toggle('on'); };
}
}
function panSearch() {
var kw = document.getElementById('panKeyword').value.trim();
if (!kw) return;
var selected = [];
var dts = document.querySelectorAll('.disk-type.on');
for (var i = 0; i < dts.length; i++) selected.push(dts[i].getAttribute('data-disk'));
if (selected.length === 0) { document.getElementById('panResult').innerHTML = '<div class="pansou-info">请至少选择一个网盘类型</div>'; return; }
var el = document.getElementById('panResult');
el.innerHTML = '<div class="pansou-info"><div class="loading-spinner" style="margin:0 auto 8px"></div>搜索中...</div>';
_fm().then(function(f){
if (!f || !f.req) { el.innerHTML = '<div class="pansou-info">SDK不可用</div>'; return; }
f.req(PAN_API + '/api/search', {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({kw:kw, res:'merge', src:'all', cloud_types:selected, channels:[]}),
responseType:'json',
timeout:20
}).then(function(res){
var data = res && res.body;
if (!data && typeof res === 'string') { try{ data = JSON.parse(res); }catch(e){} }
if (!data || !data.data) { el.innerHTML = '<div class="pansou-info">没有找到结果</div>'; return; }
renderPanResults(data.data, el, kw);
})['catch'](function(){ el.innerHTML = '<div class="pansou-info">搜索请求失败</div>'; });
});
}
function renderPanResults(data, el, kw) {
var merged = data.merged_by_type || data.list || data;
var allItems = [];
// Collect items by type
for (var type in merged) {
var list = merged[type];
if (!Array.isArray(list)) continue;
for (var i = 0; i < list.length; i++) {
var item = list[i];
var url = item.url || item.link || '';
var title = item.title || item.name || item.file_name || '';
allItems.push({type:type, url:url, title:title, pwd:item.password || item.pwd || ''});
}
}
if (allItems.length === 0) { el.innerHTML = '<div class="pansou-info">没有找到结果</div>'; return; }
// Dedup by type+url
var seen = {};
var unique = [];
for (var i = 0; i < allItems.length; i++) {
var key = allItems[i].type + ':' + allItems[i].url;
if (!seen[key]) { seen[key] = true; unique.push(allItems[i]); }
}
var h = '<div style="font-size:12px;color:var(--text2);margin-bottom:8px">找到 ' + unique.length + ' 条结果';
if (unique.length > 16) h += '(仅显示前16条)';
h += '</div><div class="pansou-result">';
var showItems = unique.slice(0, 16);
for (var i = 0; i < showItems.length; i++) {
var item = showItems[i];
var label = DISK_LABELS[item.type] || item.type;
var extra = '';
if (item.size) extra += ' ' + item.size;
if (item.date) extra += ' ' + item.date;
h += '<div class="pansou-item" data-type="' + item.type + '" data-url="' + _esc(item.url) + '" data-pwd="' + _esc(item.pwd) + '" data-title="' + _esc(item.title) + '" tabindex="0">';
h += '<span class="pi-status pi-pending" id="ps-' + i + '">?</span>';
h += '<span class="pi-title">' + _esc(item.title) + '</span>';
h += '<span class="pi-type">' + _esc(label) + '</span>';
if (item.pwd) h += '<span style="font-size:10px;color:var(--text3);flex-shrink:0">pwd:' + _esc(item.pwd) + '</span>';
h += '</div>';
}
h += '</div>';
el.innerHTML = h;
// Bind click -> play via fm.pan.play
var items = el.querySelectorAll('.pansou-item');
for (var i = 0; i < items.length; i++) {
items[i].onclick = function(){
var type = this.getAttribute('data-type');
var url = this.getAttribute('data-url');
var pwd = this.getAttribute('data-pwd');
var title = this.getAttribute('data-title');
_fm().then(function(f){
if (f && f.pan && f.pan.play) f.pan.play({type:type, url:url, password:pwd, title:title});
else window.open(url);
});
};
}
// Check validity via fm.pan.check (max 10 at a time)
var checkItems = showItems.filter(function(it){ return ['quark','aliyun','baidu','uc','tianyi','xunlei','123','115','mobile'].indexOf(it.type) >= 0; }).slice(0, 10);
if (checkItems.length > 0) {
_fm().then(function(f){
if (!f || !f.config) return;
f.config().then(function(cfg){
if (!cfg || cfg.driveCheck === false) return;
var payload = [];
for (var i = 0; i < checkItems.length; i++) {
var ci = checkItems[i];
var idx = showItems.indexOf(ci);
payload.push({type:ci.type, url:ci.url, password:ci.pwd});
}
if (payload.length > 0 && f.pan && f.pan.check) {
f.pan.check(payload).then(function(res){
if (!res || !res.results) return;
for (var ri = 0; ri < res.results.length; ri++) {
var r = res.results[ri];
var st = r.state || '';
var foundIdx = -1;
for (var si = 0; si < showItems.length; si++) {
if (showItems[si].url === r.url) { foundIdx = si; break; }
}
if (foundIdx >= 0) {
var badge = document.getElementById('ps-' + foundIdx);
if (badge) {
if (st === 'ok') { badge.className = 'pi-status pi-ok'; badge.textContent = '✓'; }
else if (st === 'bad') { badge.className = 'pi-status pi-bad'; badge.textContent = '✗'; }
else if (st === 'locked') { badge.className = 'pi-status pi-uncertain'; badge.textContent = '🔒'; }
else { badge.className = 'pi-status pi-none'; badge.textContent = ''; }
}
}
}
})['catch'](function(){});
}
})['catch'](function(){});
});
}
}
/* ── TMDB 推荐 ── */ // handled by nostr.html, accessed via window.location.href = '/nostr.html'
function setTab(tab) {
var tabs = document.querySelectorAll('.nav-tab');
for (var i = 0; i < tabs.length; i++) tabs[i].classList.toggle('on', tabs[i].getAttribute('data-tab') === tab);
if (tab === 'home') loadHome();
else if (tab === 'pansou') loadPanSou();
else if (tab === 'trending') { window.location.href = '/nostr.html'; }
else loadCategory(tab, 1);
}
document.addEventListener('DOMContentLoaded', function() {
var tabs = document.querySelectorAll('.nav-tab');
for (var i = 0; i < tabs.length; i++) {
tabs[i].onclick = function() { setTab(this.getAttribute('data-tab')); };
tabs[i].onkeydown = function(e) { if (e.key === 'Enter') { e.preventDefault(); setTab(this.getAttribute('data-tab')); } };
}
document.getElementById('searchBtn').onclick = function() { doSearch(document.getElementById('searchInput').value); };
document.getElementById('searchInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') doSearch(this.value); });
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeDetail(); });
window.addEventListener('popstate', function() { if (document.getElementById('detailView').classList.contains('active')) closeDetail(); });
loadHome();
});
</script>
</body>
</html>