Файли
tvbox_collect/jaychouqq/yingshi/html/libvio.html
T
2026-06-29 03:41:14 +00:00

988 рядки
44 KiB
HTML
Неформатований Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
<!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>