(async function bilibiliDownloader() {
'use strict';
document.getElementById('bdl-root')?.remove();
document.getElementById('bdl-css')?.remove();
/* ═══════════ 常量 & 工具 ═══════════ */
const QN={127:'8K',126:'Dolby',125:'HDR',120:'4K',116:'1080P60',112:'1080P+',80:'1080P',74:'720P60',64:'720P',32:'480P',16:'360P'};
const VC={av01:'AV1',hev1:'HEVC',hvc1:'HEVC',avc1:'AVC'};
const AC_MAP={mp4a:'AAC',flac:'FLAC',fLaC:'FLAC','ec-3':'Dolby'};
const AN={30216:'64K',30232:'128K',30280:'320K',30250:'Dolby',30251:'Hi-Res'};
const BT={1:'番剧',2:'电影',3:'纪录片',4:'国创',5:'电视剧',7:'综艺',11:'课堂'};
const NAME_PRESETS={default:'{title} - P{ep} {epName}',title_ep:'{title} - EP{epPad}',flat:'{title}_{epName}',sec_ep:'{sec} - P{epPad}',full:'{title}_{epPad}_{epName}_{quality}'};
const HQ_RE=/1080|4K|8K|Dolby|HDR/i;
const $=s=>document.querySelector(s);
const fmtN=n=>n==null?'-':n>=1e4?(n/1e4).toFixed(1)+'万':String(n);
const fmtD=s=>{s=Math.floor(s||0);return`${~~(s/60)}:${String(s%60).padStart(2,'0')}`};
const fmtB=b=>{if(!b)return'-';return b>=1073741824?(b/1073741824).toFixed(2)+' GB':(b/1048576).toFixed(1)+' MB'};
const fixU=u=>u?(u.startsWith('//')?'https:'+u:u):'';
const san=s=>(s||'').replace(/[\\/:*?"<>|\n\r]/g,'_').slice(0,120);
const estSz=(bw,dur)=>bw&&dur?fmtB(bw*dur/8):'-';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const log=s=>console.log('%c[BiliDL] '+s,'color:#00a1d6;font-weight:bold');
const bgQ=q=>{if(/8K|4K|Dolby/i.test(q))return'bg-yw';if(/HDR/i.test(q))return'bg-or';if(/1080/i.test(q))return'bg-bl';if(/720/i.test(q))return'bg-gn';return'bg-rd'};
/* MD5 */
function md5(s){function S(x,y){var l=(x&0xffff)+(y&0xffff);return(((x>>16)+(y>>16)+(l>>16))<<16)|(l&0xffff)}function R(x,n){return(x<<n)|(x>>>(32-n))}function C(q,a,b,x,s,t){return S(R(S(S(a,q),S(x,t)),s),b)}function FF(a,b,c,d,x,s,t){return C((b&c)|((~b)&d),a,b,x,s,t)}function GG(a,b,c,d,x,s,t){return C((b&d)|(c&(~d)),a,b,x,s,t)}function HH(a,b,c,d,x,s,t){return C(b^c^d,a,b,x,s,t)}function II(a,b,c,d,x,s,t){return C(c^(b|(~d)),a,b,x,s,t)}var utf=s.replace(/\r\n/g,'\n'),t='',i,n,c;for(i=0;i<utf.length;i++){c=utf.charCodeAt(i);if(c<128)t+=String.fromCharCode(c);else if(c<2048){t+=String.fromCharCode((c>>6)|192);t+=String.fromCharCode((c&63)|128)}else{t+=String.fromCharCode((c>>12)|224);t+=String.fromCharCode(((c>>6)&63)|128);t+=String.fromCharCode((c&63)|128)}}n=t.length;var m=[];for(i=0;i<n*8;i+=8)m[i>>5]|=(t.charCodeAt(i/8)&255)<<(i%32);m[n>>5]|=0x80<<(n%32);m[((n+64)>>>9<<4)+14]=n;var a=1732584193,b=-271733879,c2=-1732584194,d=271733878;for(i=0;i<m.length;i+=16){var oa=a,ob=b,oc=c2,od=d;a=FF(a,b,c2,d,m[i],7,-680876936);d=FF(d,a,b,c2,m[i+1],12,-389564586);c2=FF(c2,d,a,b,m[i+2],17,606105819);b=FF(b,c2,d,a,m[i+3],22,-1044525330);a=FF(a,b,c2,d,m[i+4],7,-176418897);d=FF(d,a,b,c2,m[i+5],12,1200080426);c2=FF(c2,d,a,b,m[i+6],17,-1473231341);b=FF(b,c2,d,a,m[i+7],22,-45705983);a=FF(a,b,c2,d,m[i+8],7,1770035416);d=FF(d,a,b,c2,m[i+9],12,-1958414417);c2=FF(c2,d,a,b,m[i+10],17,-42063);b=FF(b,c2,d,a,m[i+11],22,-1990404162);a=FF(a,b,c2,d,m[i+12],7,1804603682);d=FF(d,a,b,c2,m[i+13],12,-40341101);c2=FF(c2,d,a,b,m[i+14],17,-1502002290);b=FF(b,c2,d,a,m[i+15],22,1236535329);a=GG(a,b,c2,d,m[i+1],5,-165796510);d=GG(d,a,b,c2,m[i+6],9,-1069501632);c2=GG(c2,d,a,b,m[i+11],14,643717713);b=GG(b,c2,d,a,m[i],20,-373897302);a=GG(a,b,c2,d,m[i+5],5,-701558691);d=GG(d,a,b,c2,m[i+10],9,38016083);c2=GG(c2,d,a,b,m[i+15],14,-660478335);b=GG(b,c2,d,a,m[i+4],20,-405537848);a=GG(a,b,c2,d,m[i+9],5,568446438);d=GG(d,a,b,c2,m[i+14],9,-1019803690);c2=GG(c2,d,a,b,m[i+3],14,-187363961);b=GG(b,c2,d,a,m[i+8],20,1163531501);a=GG(a,b,c2,d,m[i+13],5,-1444681467);d=GG(d,a,b,c2,m[i+2],9,-51403784);c2=GG(c2,d,a,b,m[i+7],14,1735328473);b=GG(b,c2,d,a,m[i+12],20,-1926607734);a=HH(a,b,c2,d,m[i+5],4,-378558);d=HH(d,a,b,c2,m[i+8],11,-2022574463);c2=HH(c2,d,a,b,m[i+11],16,1839030562);b=HH(b,c2,d,a,m[i+14],23,-35309556);a=HH(a,b,c2,d,m[i+1],4,-1530992060);d=HH(d,a,b,c2,m[i+4],11,1272893353);c2=HH(c2,d,a,b,m[i+7],16,-155497632);b=HH(b,c2,d,a,m[i+10],23,-1094730640);a=HH(a,b,c2,d,m[i+13],4,681279174);d=HH(d,a,b,c2,m[i],11,-358537222);c2=HH(c2,d,a,b,m[i+3],16,-722521979);b=HH(b,c2,d,a,m[i+6],23,76029189);a=HH(a,b,c2,d,m[i+9],4,-640364487);d=HH(d,a,b,c2,m[i+12],11,-421815835);c2=HH(c2,d,a,b,m[i+15],16,530742520);b=HH(b,c2,d,a,m[i+2],23,-995338651);a=II(a,b,c2,d,m[i],6,-198630844);d=II(d,a,b,c2,m[i+7],10,1126891415);c2=II(c2,d,a,b,m[i+14],15,-1416354905);b=II(b,c2,d,a,m[i+5],21,-57434055);a=II(a,b,c2,d,m[i+12],6,1700485571);d=II(d,a,b,c2,m[i+3],10,-1894986606);c2=II(c2,d,a,b,m[i+10],15,-1051523);b=II(b,c2,d,a,m[i+1],21,-2054922799);a=II(a,b,c2,d,m[i+8],6,1873313359);d=II(d,a,b,c2,m[i+15],10,-30611744);c2=II(c2,d,a,b,m[i+6],15,-1560198380);b=II(b,c2,d,a,m[i+13],21,1309151649);a=II(a,b,c2,d,m[i+4],6,-145523070);d=II(d,a,b,c2,m[i+11],10,-1120210379);c2=II(c2,d,a,b,m[i+2],15,718787259);b=II(b,c2,d,a,m[i+9],21,-343485551);a=S(a,oa);b=S(b,ob);c2=S(c2,oc);d=S(d,od)}function H(x){var h='0123456789abcdef',r='';for(var j=0;j<4;j++){var v=(x>>>(j*8))&255;r+=h[v>>4]+h[v&15]}return r}return H(a)+H(b)+H(c2)+H(d)}
/* WBI */
const ET=[46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52];
let wbiK=null,wbiT=0;
async function getWbi(){if(wbiK&&Date.now()-wbiT<3600000)return wbiK;const j=await api('https://api.bilibili.com/x/web-interface/nav');if(!j?.data?.wbi_img)throw new Error('WBI失败');const img=j.data.wbi_img.img_url.split('/').pop().split('.')[0];const sub=j.data.wbi_img.sub_url.split('/').pop().split('.')[0];const raw=img+sub;wbiK=ET.reduce((a,i)=>a+(raw[i]||''),'').slice(0,32);wbiT=Date.now();return wbiK;}
function sign(base,params,key){const p={...params,wts:String(Math.floor(Date.now()/1000))};const qs=Object.entries(p).sort(([a],[b])=>a.localeCompare(b)).map(([k,v])=>k+'='+encodeURIComponent(v)).join('&');p.w_rid=md5(qs+key);return base+'?'+Object.entries(p).map(([k,v])=>k+'='+encodeURIComponent(v)).join('&');}
async function api(url){const r=await fetch(url,{credentials:'include'});if(!r.ok)throw new Error('HTTP '+r.status);return r.json()}
/* ═══════════ 页面识别 & 获取信息 ═══════════ */
const URL_=location.href;let pgType=null;
const bvM=URL_.match(/(BV\w{10})/i),epM=URL_.match(/\/play\/ep(\d+)/i),ssM=URL_.match(/\/play\/ss(\d+)/i),mdM=URL_.match(/\/media\/md(\d+)/i),avM=URL_.match(/av(\d+)/i);
if(bvM)pgType='video';else if(epM||ssM||mdM)pgType='bangumi';else if(avM)pgType='video';
if(!pgType)return console.error('❌ 请在哔哩哔哩视频或番剧页面运行');
const S={type:pgType,title:'',desc:'',cover:'',duration:0,owner:null,stat:null,pages:[],
bvid:bvM?bvM[1].toUpperCase():null,avid:avM?+avM[1]:null,cid:null,epId:null,
bType:0,bTypeName:'',rating:null,newEp:null,allEps:[],sections:[],ci:0,curLabel:'',curSec:'',
play:null,str:null,selV:0,selA:0,
secGroups:[],curSecIdx:0,curEpIdx:0,batchSet:new Set(),
showAllQ:false,dirHandle:null,nameFmt:NAME_PRESETS.default};
if(pgType==='video'){
log('📡 获取视频信息...');
const j=await api('https://api.bilibili.com/x/web-interface/view?'+(S.bvid?'bvid='+S.bvid:'aid='+S.avid));
if(j.code!==0)return console.error('❌',j.message);
const D=j.data;Object.assign(S,{bvid:D.bvid,avid:D.aid,cid:D.cid,title:D.title,desc:D.desc||'',cover:fixU(D.pic),duration:D.duration,owner:D.owner,stat:D.stat,pages:D.pages});
S.secGroups=[{title:'分P列表',eps:S.pages.map((p,i)=>({gi:i,num:i+1,name:p.part||'P'+(i+1)}))}];
S.curEpIdx=Math.max(0,S.pages.findIndex(p=>p.cid===S.cid));
}else{
log('📡 获取PGC信息...');
let sp=epM?'ep_id='+epM[1]:ssM?'season_id='+ssM[1]:'md_id='+mdM[1];
if(sp.startsWith('md_id=')){const mj=await api('https://api.bilibili.com/pgc/review/media?media_id='+sp.slice(6));if(mj.code!==0)return console.error('❌');sp='season_id='+mj.result.media.season_id;}
const sj=await api('https://api.bilibili.com/pgc/view/web/season?'+sp);if(sj.code!==0)return console.error('❌',sj.message);
const R=sj.result,mainEps=R.episodes||[],extraSecs=R.section||[],allSections=[];
if(mainEps.length)allSections.push({title:'正片',episodes:mainEps});
const mainIds=new Set(mainEps.map(e=>e.id));
for(const sec of extraSecs){const eps=(sec.episodes||[]).filter(e=>!mainIds.has(e.id));if(eps.length)allSections.push({title:sec.title||'其他',episodes:eps})}
if(!allSections.length)return console.error('❌ 无可播放剧集');
const allEps=[];let gi=0;
S.secGroups=allSections.map(sec=>({title:sec.title,eps:sec.episodes.map(ep=>{const info={gi,num:gi+1,name:ep.long_title||ep.title||'第'+(gi+1)+'话'};allEps.push({...ep,_sec:sec.title});gi++;return info;})}));
if(!allEps.length)return console.error('❌ 无可播放剧集');
Object.assign(S,{allEps});
let ci=0;if(epM){ci=allEps.findIndex(e=>e.id===+epM[1]);if(ci<0)ci=0}
for(let si=0;si<S.secGroups.length;si++){if(S.secGroups[si].eps.some(e=>e.gi===ci)){S.curSecIdx=si;break;}}
const ce=allEps[ci];Object.assign(S,{bvid:ce.bvid,avid:ce.aid,cid:ce.cid,epId:ce.id,title:R.title,desc:R.evaluate||'',cover:fixU(ce.cover||ce.horizontal_cover||R.cover),duration:ce.duration?ce.duration/1000:0,bType:R.type,bTypeName:BT[R.type]||'番剧',rating:R.rating,newEp:R.new_ep,sections:allSections,ci,curEpIdx:ci,curLabel:ce.long_title||ce.title||'第'+(ci+1)+'话',curSec:ce._sec});
log('📺 '+S.bTypeName+' ['+S.title+'] · '+allSections.length+'分区 · '+allEps.length+'集');
}
/* ═══════════ 播放地址 ═══════════ */
async function getVideoPlay(){try{const m=await getWbi();const j=await api(sign('https://api.bilibili.com/x/player/wbi/playurl',{bvid:S.bvid,cid:String(S.cid),qn:'127',fnval:'4048',fourk:'1',fnver:'0',voice_balance:'1'},m));if(j.code===0)return j.data}catch(e){log('⚠ '+e.message)}const j=await api('https://api.bilibili.com/x/player/playurl?bvid='+S.bvid+'&cid='+S.cid+'&qn=127&fnval=4048&fourk=1');if(j.code!==0)throw new Error(j.message);return j.data;}
async function getPgcPlay(){try{const j=await api('https://api.bilibili.com/pgc/player/web/playurl?avid='+S.avid+'&cid='+S.cid+'&qn=127&fnval=4048&fourk=1&ep_id='+(S.epId||0)+'&fnver=0&voice_balance=1');if(j.code===0)return j.result||j.data}catch{}return getVideoPlay();}
async function getPlayFor(gi){if(S.type==='video'){S.cid=S.pages[gi].cid;return await getVideoPlay();}const ep=S.allEps[gi];S.cid=ep.cid;S.avid=ep.aid;S.bvid=ep.bvid;S.epId=ep.id;return await getPgcPlay();}
log('📡 获取播放地址...');
try{S.play=pgType==='bangumi'?await getPgcPlay():await getVideoPlay()}catch(e){return console.error('❌',e.message)}
function parseStreams(pd){let fmt='DASH',vs=[],as=[];if(pd.dash){vs=(pd.dash.video||[]).map((v,i)=>({i,qid:v.id,quality:QN[v.id]||v.id+'P',cn:Object.entries(VC).find(([k])=>v.codecs?.startsWith(k))?.[1]||v.codecs||'-',w:v.width,h:v.height,fps:v.frame_rate||v.frameRate||'-',bw:v.bandwidth,kbps:v.bandwidth?(v.bandwidth/1e3).toFixed(0):'0',url:fixU(v.baseUrl||v.base_url)}));as=(pd.dash.audio||[]).map((a,i)=>({i,qid:a.id,quality:AN[a.id]||'',cn:Object.entries(AC_MAP).find(([k])=>a.codecs?.includes(k))?.[1]||a.codecs||'-',bw:a.bandwidth,kbps:a.bandwidth?(a.bandwidth/1e3).toFixed(0):'0',url:fixU(a.baseUrl||a.base_url)}));if(pd.dash.flac?.audio){const a=pd.dash.flac.audio;as.push({i:as.length,qid:a.id,quality:'Hi-Res',cn:'FLAC',bw:a.bandwidth,kbps:a.bandwidth?(a.bandwidth/1e3).toFixed(0):'0',url:fixU(a.baseUrl||a.base_url)})}if(pd.dash.dolby?.audio?.length)pd.dash.dolby.audio.forEach(a=>{as.push({i:as.length,qid:a.id,quality:'Dolby',cn:'Dolby',bw:a.bandwidth,kbps:a.bandwidth?(a.bandwidth/1e3).toFixed(0):'0',url:fixU(a.baseUrl||a.base_url)})});}else if(pd.durl?.length){fmt=pd.durl[0]?.url?.includes('.flv')?'FLV':'MP4';vs=pd.durl.map((d,i)=>({i,url:d.url,quality:'原始',cn:fmt,w:0,h:0,fps:'-',bw:0,kbps:0,size:d.size}));}return{fmt,vs,as};}
S.str=parseStreams(S.play);
S.showAllQ=!S.str.vs.some(v=>HQ_RE.test(v.quality));
/* ═══════════ fMP4 合并 ═══════════ */
function mergefMP4(videoAB,audioAB){const v=new Uint8Array(videoAB),a=new Uint8Array(audioAB);const r32=(d,o)=>((d[o]<<24)|(d[o+1]<<16)|(d[o+2]<<8)|d[o+3])>>>0;const w32=(d,o,val)=>{d[o]=(val>>>24)&0xff;d[o+1]=(val>>>16)&0xff;d[o+2]=(val>>>8)&0xff;d[o+3]=val&0xff;};const fcc=(d,o)=>String.fromCharCode(d[o],d[o+1],d[o+2],d[o+3]);function boxAt(d,o){if(o+8>d.length)return null;let s=r32(d,o),h=8;if(s===1){if(o+16>d.length)return null;s=Number(r32(d,o+8))*0x100000000+r32(d,o+12);h=16;}if(s<8||o+s>d.length)return null;return{t:fcc(d,o+4),o,s,h};}function findBoxes(d,s,e){const r=[];let o=s;while(o+8<=e){const b=boxAt(d,o);if(!b)break;r.push(b);o+=b.s;}return r;}function findBox(d,p,t){let o=p.o+p.h,e=p.o+p.s;while(o+8<=e){const b=boxAt(d,o);if(!b)break;if(b.t===t)return b;o+=b.s;}return null;}function getTrackId(d,t){const tk=findBox(d,t,'tkhd');if(!tk)return 1;const ver=d[tk.o+tk.h];return r32(d,tk.o+tk.h+(ver===0?12:20));}const vB=findBoxes(v,0,v.length),aB=findBoxes(a,0,a.length);const vFtyp=vB.find(b=>b.t==='ftyp'),vMoov=vB.find(b=>b.t==='moov'),aMoov=aB.find(b=>b.t==='moov');if(!vFtyp||!vMoov||!aMoov)throw new Error('无效fMP4');const vTrak=findBox(v,vMoov,'trak'),aTrak=findBox(a,aMoov,'trak');if(!vTrak||!aTrak)throw new Error('缺少trak');const vTid=getTrackId(v,vTrak),origATid=getTrackId(a,aTrak),newATid=origATid===vTid?vTid+1:origATid;const vMvhd=findBox(v,vMoov,'mvhd');const mvhd=new Uint8Array(v.slice(vMvhd.o,vMvhd.o+vMvhd.s));w32(mvhd,mvhd.length-4,newATid+1);const vTrakData=v.slice(vTrak.o,vTrak.o+vTrak.s);const aTrakData=new Uint8Array(a.slice(aTrak.o,aTrak.o+aTrak.s));if(newATid!==origATid){let o=aTrak.h;while(o+8<=aTrakData.length){const b=boxAt(aTrakData,o);if(!b)break;if(b.t==='tkhd'){const ver=aTrakData[o+b.h];w32(aTrakData,o+b.h+(ver===0?12:20),newATid);break;}o+=b.s;}}const trexParts=[];const vMvex=findBox(v,vMoov,'mvex');if(vMvex){let o=vMvex.o+vMvex.h,e=vMvex.o+vMvex.s;while(o+8<=e){const b=boxAt(v,o);if(!b)break;trexParts.push(v.slice(o,o+b.s));o+=b.s;}}else{const t=new Uint8Array(32);w32(t,0,32);t[4]=0x74;t[5]=0x72;t[6]=0x65;t[7]=0x78;w32(t,12,vTid);trexParts.push(t);}const at=new Uint8Array(32);w32(at,0,32);at[4]=0x74;at[5]=0x72;at[6]=0x65;at[7]=0x78;w32(at,12,newATid);trexParts.push(at);const mvexBody=trexParts.reduce((s,p)=>s+p.length,0);const mvex=new Uint8Array(8+mvexBody);w32(mvex,0,mvex.length);mvex[4]=0x6d;mvex[5]=0x76;mvex[6]=0x65;mvex[7]=0x78;let mo=8;for(const p of trexParts){mvex.set(new Uint8Array(p),mo);mo+=p.length;}const moovBody=mvhd.length+vTrakData.length+aTrakData.length+mvex.length;const newMoov=new Uint8Array(8+moovBody);w32(newMoov,0,newMoov.length);newMoov[4]=0x6d;newMoov[5]=0x6f;newMoov[6]=0x6f;newMoov[7]=0x76;let np=8;newMoov.set(mvhd,np);np+=mvhd.length;newMoov.set(new Uint8Array(vTrakData),np);np+=vTrakData.length;newMoov.set(aTrakData,np);np+=aTrakData.length;newMoov.set(mvex,np);function patchMedia(md,from,to){let o=0;while(o+8<=md.length){const b=boxAt(md,o);if(!b)break;if(b.t==='sidx'){md[o+4]=0x66;md[o+5]=0x72;md[o+6]=0x65;md[o+7]=0x65;}if(b.t==='moof'){let t=o+b.h;while(t+8<=o+b.s){const tb=boxAt(md,t);if(!tb)break;if(tb.t==='traf'){let f=tb.h;while(f+8<=tb.s){const fb=boxAt(md,t+f);if(!fb)break;if(fb.t==='tfhd'){const off=t+f+fb.h+4;if(off+4<=t+f+fb.s&&r32(md,off)===from)w32(md,off,to);}f+=fb.s;}}t+=tb.s;}}o+=b.s;}}const aMedia=new Uint8Array(a.subarray(aMoov.o+aMoov.s));patchMedia(aMedia,origATid,newATid);const vMedia=new Uint8Array(v.subarray(vMoov.o+vMoov.s));patchMedia(vMedia,vTid,vTid);const ftyp=v.subarray(vFtyp.o,vFtyp.o+vFtyp.s);const out=new Uint8Array(ftyp.length+newMoov.length+vMedia.length+aMedia.length);let off=0;out.set(ftyp,off);off+=ftyp.length;out.set(newMoov,off);off+=newMoov.length;out.set(vMedia,off);off+=vMedia.length;out.set(aMedia,off);return new Blob([out],{type:'video/mp4'});}
/* ═══════════ 命名系统 ═══════════ */
function getEpInfo(gi){if(S.type==='video')return{gi,num:gi+1,name:S.pages[gi]?.part||'P'+(gi+1),sec:'分P'};const ep=S.allEps[gi];return{gi,num:gi+1,name:ep?.long_title||ep?.title||'第'+(gi+1)+'话',sec:ep?._sec||''};}
function fillName(ep,fmt){return(fmt||S.nameFmt).replace(/\{title\}/g,san(S.title)).replace(/\{epPad\}/g,String(ep.num).padStart(2,'0')).replace(/\{ep\}/g,String(ep.num)).replace(/\{epName\}/g,san(ep.name)).replace(/\{sec\}/g,san(ep.sec||'')).replace(/\{bvid\}/g,S.bvid||'').replace(/\{quality\}/g,S.str.vs[S.selV]?.quality||'').replace(/\{codec\}/g,S.str.vs[S.selV]?.cn||'')+'.mp4';}
function previewName(){return fillName(getEpInfo(S.curEpIdx));}
/* ═══════════ CSS ═══════════ */
const css=document.createElement('style');css.id='bdl-css';
css.textContent=`
#bdl-root{position:fixed;inset:0;z-index:999999;display:flex;align-items:center;justify-content:center;font-family:'Noto Sans SC',-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;animation:bf .22s}
#bdl-ov{position:absolute;inset:0;background:rgba(0,0,0,.32);backdrop-filter:blur(12px)}
#bdl-md{position:relative;width:800px;max-width:95vw;max-height:92vh;display:flex;flex-direction:column;background:#fff;border:1px solid #e2e4ea;border-radius:18px;box-shadow:0 30px 80px rgba(0,0,0,.18);animation:bs .38s cubic-bezier(.16,1,.3,1);overflow:hidden}
#bdl-hd{display:flex;align-items:center;padding:12px 20px;background:#f8f9fb;border-bottom:1px solid #e8eaef;cursor:move;user-select:none;gap:10px}
.bdl-logo{width:32px;height:32px;background:linear-gradient(135deg,#00a1d6,#fb7299);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:15px;color:#fff;font-weight:800;flex-shrink:0}
.bdl-hd-t{flex:1;font-size:14px;font-weight:700;color:#1a1d2e}
.bdl-hd-s{font-size:11px;color:#9ca0b0;margin-right:10px}
#bdl-x{width:30px;height:30px;border:none;background:0;border-radius:8px;color:#9ca0b0;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}
#bdl-x:hover{background:#fee;color:#e55}
#bdl-bd{flex:1;overflow-y:auto;padding:16px 20px;background:#fff}
#bdl-bd::-webkit-scrollbar{width:6px}#bdl-bd::-webkit-scrollbar-track{background:0 0}#bdl-bd::-webkit-scrollbar-thumb{background:#d4d7e0;border-radius:3px}
.bdl-s{background:#f8f9fb;border:1px solid #ebedf2;border-radius:12px;padding:14px 16px;margin-bottom:10px}
.bdl-st{font-size:12px;font-weight:700;color:#00a1d6;margin-bottom:10px;display:flex;align-items:center;gap:5px}
.bdl-st-h{font-weight:400;color:#adb0be;font-size:11px}
.bdl-vr{display:flex;gap:14px}
.bdl-cw{position:relative;flex-shrink:0;width:200px;aspect-ratio:16/9;border-radius:10px;overflow:hidden;background:#ebedf2}
.bdl-cw img{width:100%;height:100%;object-fit:cover;display:block}
.bdl-cw .dur{position:absolute;bottom:6px;right:6px;background:rgba(0,0,0,.78);color:#fff;font-size:10px;padding:2px 7px;border-radius:4px}
.bdl-vm{flex:1;min-width:0}
.bdl-vt{font-size:15px;font-weight:700;color:#1a1d2e;margin:0 0 4px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.bdl-vd{font-size:11px;color:#8a8ea0;margin:0 0 7px;line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.bdl-vs{display:flex;flex-wrap:wrap;gap:2px 10px;font-size:11px;color:#6a6e80}
.bdl-vid{display:flex;gap:4px;margin-top:5px;flex-wrap:wrap}
.bdl-vid span{font-size:10px;color:#8a8ea0;background:#f0f1f5;padding:2px 7px;border-radius:4px;font-family:'DM Mono','SF Mono',Consolas,monospace}
.bdl-ur{display:flex;align-items:center;gap:10px;margin-top:10px;padding-top:10px;border-top:1px solid #ebedf2}
.bdl-av{width:36px;height:36px;border-radius:50%;object-fit:cover;border:2px solid #e0eaff;flex-shrink:0}
.bdl-an{font-size:13px;font-weight:600;color:#1a1d2e}
.bdl-au{font-size:10px;color:#9ca0b0;font-family:'DM Mono',monospace;margin-top:1px}
.bdl-fg2{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:6px}
.bdl-fc{background:#fff;border:1px solid #ebedf2;border-radius:8px;padding:8px;text-align:center;transition:all .15s}
.bdl-fc:hover{border-color:#c8d0e0}
.bdl-fc-l{font-size:9px;color:#9ca0b0;text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.bdl-fc-v{font-size:12px;color:#1a1d2e;font-weight:700}
.bdl-sec-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
.bdl-sec-tab{padding:5px 14px;border:1px solid #e2e4ea;border-radius:20px;font-size:12px;font-weight:600;color:#5a5e70;background:#fff;cursor:pointer;transition:all .2s;font-family:inherit}
.bdl-sec-tab:hover{border-color:#00a1d6;color:#00a1d6}
.bdl-sec-tab.active{background:#00a1d6;color:#fff;border-color:#00a1d6}
.bdl-ep-grid{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}
.bdl-ep-sq{width:48px;height:34px;border:1.5px solid #e2e4ea;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:#4a4e62;cursor:pointer;transition:all .15s;background:#fff;position:relative}
.bdl-ep-sq:hover{border-color:#00a1d6;color:#00a1d6}
.bdl-ep-sq.sel{background:#00a1d6;border-color:#00a1d6;color:#fff}
.bdl-ep-sq.batch::after{content:'';position:absolute;top:-3px;right:-3px;width:10px;height:10px;border-radius:50%;background:#fb7299;border:1.5px solid #fff}
.bdl-ep-sq.sel.batch{box-shadow:0 0 0 2px #fb7299}
.bdl-ep-info{font-size:12px;color:#5a5e70;padding:4px 0;font-weight:500}
.bdl-ep-ctrl{display:flex;align-items:center;gap:6px;margin-top:8px;flex-wrap:wrap}
.bdl-batch-cnt{font-size:11px;color:#9ca0b0;margin-left:auto}
.bdl-sel{width:100%;background:#fff;border:1px solid #dde0e8;border-radius:8px;color:#4a4e62;padding:8px 12px;font-size:12px;font-family:inherit;outline:0;cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;transition:border-color .2s}
.bdl-sel:focus{border-color:#00a1d6}.bdl-sel option{background:#fff;color:#4a4e62}
.bdl-tw{overflow-x:auto}
.bdl-t{width:100%;border-collapse:collapse;font-size:11px}
.bdl-t th{text-align:left;padding:7px 8px;font-size:10px;font-weight:700;color:#9ca0b0;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid #ebedf2;white-space:nowrap}
.bdl-t td{padding:7px 8px;color:#6a6e80;border-bottom:1px solid #f2f3f7;white-space:nowrap}
.bdl-t tbody tr{cursor:pointer;transition:background .15s}
.bdl-t tbody tr:hover{background:#f0f5ff}
.bdl-t tbody tr.sel{background:#e8f4ff;box-shadow:inset 3px 0 0 #00a1d6}
.bdl-t tbody tr.sel td{color:#2a2e3e;font-weight:500}
.bg{display:inline-block;font-size:10px;padding:1px 6px;border-radius:3px;font-weight:700;line-height:1.5}
.bg-bl{background:#e0f0ff;color:#0090c0}.bg-pk{background:#ffe4ee;color:#e85d8a}.bg-gn{background:#e0fbe8;color:#1baa52}.bg-yw{background:#fff6d8;color:#c89600}.bg-or{background:#fff0e0;color:#d97706}.bg-rd{background:#f2f3f7;color:#8a8ea0}.bg-r{background:#ffe4ee;color:#e85d8a}
.bdl-lr{display:flex;align-items:center;gap:6px;margin-bottom:5px}
.bdl-ll{font-size:10px;color:#9ca0b0;min-width:28px;font-weight:700;flex-shrink:0}
.bdl-lu{flex:1;font-size:10px;color:#6a6e80;font-family:'DM Mono',monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background:#f2f3f7;padding:5px 8px;border-radius:5px;user-select:all;cursor:text}
.bdl-cp{font-size:10px;padding:3px 9px;border:1px solid #dde0e8;background:#fff;color:#00a1d6;border-radius:5px;cursor:pointer;transition:all .2s;white-space:nowrap;flex-shrink:0}
.bdl-cp:hover{background:#f0f5ff;border-color:#00a1d6}
.bdl-act{display:flex;flex-wrap:wrap;gap:8px;padding-top:4px}
.bdl-btn{flex:1;min-width:90px;padding:10px 14px;border:none;border-radius:10px;font-size:12px;font-weight:700;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:5px;font-family:inherit}
.bdl-btn:active{transform:scale(.97)}.bdl-btn:disabled{opacity:.45;cursor:not-allowed;transform:none!important}
.bdl-b1{background:linear-gradient(135deg,#00a1d6,#0090c0);color:#fff}
.bdl-b1:hover:not(:disabled){box-shadow:0 4px 18px rgba(0,161,214,.28)}
.bdl-b2{background:linear-gradient(135deg,#fb7299,#e85d8a);color:#fff}
.bdl-b2:hover:not(:disabled){box-shadow:0 4px 18px rgba(251,114,153,.28)}
.bdl-b3{background:#f2f3f7;color:#5a5e70;border:1px solid #e0e2ea}
.bdl-b3:hover:not(:disabled){background:#eaecf2;color:#3a3e50}
.bdl-sm{flex:0;min-width:auto;padding:6px 12px;font-size:11px}
.bdl-q-toggle{width:100%;padding:6px;border:1px dashed #dde0e8;border-radius:6px;background:0;color:#9ca0b0;font-size:11px;cursor:pointer;transition:all .2s;margin-top:6px;font-family:inherit}
.bdl-q-toggle:hover{border-color:#00a1d6;color:#00a1d6}
.bdl-name-input{width:100%;border:1px solid #dde0e8;border-radius:6px;padding:6px 10px;font-size:11px;font-family:'DM Mono','SF Mono',Consolas,monospace;color:#4a4e62;background:#fff;outline:0;transition:border-color .2s}
.bdl-name-input:focus{border-color:#00a1d6}
.bdl-name-pv{font-size:10px;color:#9ca0b0;margin-top:4px;padding:4px 8px;background:#f0f1f5;border-radius:4px;word-break:break-all;font-family:'DM Mono',monospace}
.bdl-name-vars{font-size:9px;color:#adb0be;margin-top:3px;line-height:1.6}
.bdl-pg{display:none;padding-top:10px}.bdl-pg.on{display:block}
.bdl-pb-bg{height:6px;background:#ebedf2;border-radius:3px;overflow:hidden}
.bdl-pb{height:100%;background:linear-gradient(90deg,#00a1d6,#fb7299);border-radius:3px;transition:width .25s;width:0}
.bdl-pt{font-size:11px;color:#8a8ea0;padding-top:5px;font-variant-numeric:tabular-nums;display:flex;justify-content:space-between}
.bdl-msg{font-size:11px;color:#6a6e80;padding:6px 0;text-align:center;min-height:22px}
.bdl-msg.ok{color:#1baa52}.bdl-msg.err{color:#e55}
@keyframes bf{from{opacity:0}to{opacity:1}}
@keyframes bs{from{opacity:0;transform:translateY(18px) scale(.97)}to{opacity:1;transform:none}}
@media(max-width:640px){.bdl-vr{flex-direction:column}.bdl-cw{width:100%}#bdl-md{max-height:95vh}.bdl-ep-sq{width:40px;height:30px;font-size:11px}}
`;
document.head.appendChild(css);
/* ═══════════ 构建弹窗 ═══════════ */
const root=document.createElement('div');root.id='bdl-root';
function buildBody(){
const st=S.str,avC=[...new Set(st.vs.map(v=>v.cn))].join(', ')||'-',auC=[...new Set(st.as.map(a=>a.cn))].join(', ')||'-',qs=[...new Set(st.vs.map(v=>v.quality))];
let h='';
if(S.type==='video'){
h+=`<div class="bdl-s"><div class="bdl-st">📺 视频信息</div><div class="bdl-vr"><div class="bdl-cw"><img src="${S.cover}"/><span class="dur">${fmtD(S.duration)}</span></div><div class="bdl-vm"><h2 class="bdl-vt">${S.title}</h2><p class="bdl-vd">${(S.desc||'暂无简介').replace(/\n/g,' ')}</p><div class="bdl-vs"><span>▶ ${fmtN(S.stat.view)}</span><span>💬 ${fmtN(S.stat.danmaku)}</span><span>👍 ${fmtN(S.stat.like)}</span><span>⭐ ${fmtN(S.stat.favorite)}</span></div><div class="bdl-vid"><span>${S.bvid}</span><span>AV${S.avid}</span><span>CID:${S.cid}</span></div><div class="bdl-ur"><img class="bdl-av" src="${fixU(S.owner.face)}"/><div><div class="bdl-an">${S.owner.name}</div><div class="bdl-au">UID ${S.owner.mid}</div></div></div></div></div></div>`;
}else{
h+=`<div class="bdl-s"><div class="bdl-st">📺 ${S.bTypeName}信息</div><div class="bdl-vr"><div class="bdl-cw"><img src="${S.cover}"/><span class="dur">${fmtD(S.duration)}</span></div><div class="bdl-vm"><h2 class="bdl-vt">${S.title}</h2><p class="bdl-vd">${S.desc||'暂无简介'}</p><div class="bdl-vs">${S.rating?`<span style="color:#c89600;font-weight:700">⭐ ${S.rating.score}</span><span>${fmtN(S.rating.count)}人评价</span>`:''}<span class="bg bg-r">${S.bTypeName}</span>${S.newEp?`<span>${S.newEp.desc||''}</span>`:''}</div><div class="bdl-vid"><span>EP${S.epId}</span><span>${S.allEps.length}个可播</span></div><div style="margin-top:6px;font-size:12px;color:#5a5e70;font-weight:500">当前: ${getEpInfo(S.curEpIdx).name}</div></div></div></div>`;
}
h+=`<div class="bdl-s"><div class="bdl-st">📦 格式</div><div class="bdl-fg2"><div class="bdl-fc"><div class="bdl-fc-l">容器</div><div class="bdl-fc-v">${st.fmt}</div></div><div class="bdl-fc"><div class="bdl-fc-l">视频</div><div class="bdl-fc-v">${avC}</div></div><div class="bdl-fc"><div class="bdl-fc-l">音频</div><div class="bdl-fc-v">${auC}</div></div><div class="bdl-fc"><div class="bdl-fc-l">最高</div><div class="bdl-fc-v">${qs[0]||'-'}</div></div><div class="bdl-fc"><div class="bdl-fc-l">时长</div><div class="bdl-fc-v">${fmtD(S.duration)}</div></div><div class="bdl-fc"><div class="bdl-fc-l">画质数</div><div class="bdl-fc-v">${qs.length}</div></div></div></div>`;
const sg=S.secGroups,curG=sg[S.curSecIdx];
h+=`<div class="bdl-s"><div class="bdl-st">📑 选集</div>`;
if(sg.length>1)h+=`<div class="bdl-sec-tabs">${sg.map((g,i)=>`<button class="bdl-sec-tab${i===S.curSecIdx?' active':''}" data-si="${i}">${g.title} (${g.eps.length})</button>`).join('')}</div>`;
if(curG){h+=`<div class="bdl-ep-grid" id="bdl-epg">${curG.eps.map(ep=>`<div class="bdl-ep-sq${ep.gi===S.curEpIdx?' sel':''}${S.batchSet.has(ep.gi)?' batch':''}" data-gi="${ep.gi}" title="${ep.name}">${ep.num}</div>`).join('')}</div><div class="bdl-ep-info">当前: <b>${getEpInfo(S.curEpIdx).name}</b></div>`;
h+=`<div class="bdl-ep-ctrl"><button class="bdl-btn bdl-b3 bdl-sm" id="bdl-selall">☑ 全选</button><button class="bdl-btn bdl-b3 bdl-sm" id="bdl-selnone">☐ 清除</button><button class="bdl-btn bdl-b3 bdl-sm" id="bdl-selinv">🔀 反选</button><span class="bdl-batch-cnt" id="bdl-bcnt">已选 ${S.batchSet.size} 集</span></div>`;}
h+=`</div>`;
if(st.vs.length){
const hiVs=st.vs.filter(v=>HQ_RE.test(v.quality)),loVs=st.vs.filter(v=>!HQ_RE.test(v.quality));
const show=S.showAllQ?st.vs:hiVs;
h+=`<div class="bdl-s"><div class="bdl-st">🎬 视频流 <span class="bdl-st-h">— 点击选择</span></div><div class="bdl-tw"><table class="bdl-t"><thead><tr><th>#</th><th>分辨率</th><th>画质</th><th>编码</th><th>帧率</th><th>码率</th><th>预估</th></tr></thead><tbody id="bdl-vtb">${show.map(v=>`<tr data-v="${v.i}" class="${v.i===S.selV?'sel':''}"><td>${v.i}</td><td>${v.w&&v.h?v.w+'×'+v.h:'-'}</td><td><span class="bg ${bgQ(v.quality)}">${v.quality}</span></td><td><span class="bg bg-bl">${v.cn}</span></td><td>${v.fps}</td><td>${v.bw?(v.bw/1e6).toFixed(2)+' Mbps':'-'}</td><td>${v.size?fmtB(v.size):estSz(v.bw,S.duration)}</td></tr>`).join('')}</tbody></table></div>${hiVs.length&&loVs.length?`<button class="bdl-q-toggle" id="bdl-qtog">${S.showAllQ?'收起低画质 ▲':'显示全部画质 ▼ (还有'+loVs.length+'项)'}</button>`:''}</div>`;
}
if(st.as.length)h+=`<div class="bdl-s"><div class="bdl-st">🔊 音频流 <span class="bdl-st-h">— 点击选择</span></div><div class="bdl-tw"><table class="bdl-t"><thead><tr><th>#</th><th>编码</th><th>品质</th><th>码率</th><th>预估</th></tr></thead><tbody id="bdl-atb">${st.as.map(a=>`<tr data-a="${a.i}" class="${a.i===S.selA?'sel':''}"><td>${a.i}</td><td><span class="bg bg-pk">${a.cn}</span></td><td>${a.quality||'-'}</td><td>${a.kbps} kbps</td><td>${estSz(a.bw,S.duration)}</td></tr>`).join('')}</tbody></table></div></div>`;
h+=`<div class="bdl-s"><div class="bdl-st">🔗 直链</div><div id="bdl-lk"></div></div>`;
h+=`<div class="bdl-s"><div class="bdl-st">⚡ 操作</div><div class="bdl-act"><button class="bdl-btn bdl-b1" id="bdl-mg">🔀 合并下载 MP4</button>${st.vs.length?'<button class="bdl-btn bdl-b3" id="bdl-dv">🎬 仅视频</button>':''}${st.as.length?'<button class="bdl-btn bdl-b3" id="bdl-da">🔊 仅音频</button>':''}<button class="bdl-btn bdl-b3" id="bdl-dc">🖼️ 封面</button><button class="bdl-btn bdl-b3" id="bdl-cm">📋 FFmpeg</button></div></div>`;
h+=`<div class="bdl-s"><div class="bdl-st">📂 批量下载</div>`;
h+=`<div class="bdl-fg2"><div class="bdl-fc"><div class="bdl-fc-l">保存位置</div><button class="bdl-btn bdl-b3" id="bdl-seldir" style="width:100%;font-size:11px;padding:6px 10px">📁 ${S.dirHandle?S.dirHandle.name:'选择目录'}</button></div>`;
h+=`<div class="bdl-fc"><div class="bdl-fc-l">命名预设</div><select class="bdl-sel" id="bdl-npre" style="font-size:11px;padding:5px 8px"><option value="default"${S.nameFmt===NAME_PRESETS.default?' selected':''}>标题 - P集数 名称</option><option value="title_ep"${S.nameFmt===NAME_PRESETS.title_ep?' selected':''}>标题 - EP集数</option><option value="flat"${S.nameFmt===NAME_PRESETS.flat?' selected':''}>标题_名称</option><option value="sec_ep"${S.nameFmt===NAME_PRESETS.sec_ep?' selected':''}>分区 - P集数</option><option value="full"${S.nameFmt===NAME_PRESETS.full?' selected':''}>完整信息</option></select></div>`;
h+=`<div style="grid-column:1/-1"><div class="bdl-fc-l" style="margin-bottom:4px">命名变量</div><input class="bdl-name-input" id="bdl-nfmt" value="${S.nameFmt}" placeholder="输入命名模板"/></div></div>`;
h+=`<div class="bdl-name-pv" id="bdl-npv">预览: ${previewName()}</div>`;
h+=`<div class="bdl-name-vars">{title} 标题 · {ep} 集数 · {epPad} 补零集数 · {epName} 集名 · {sec} 分区 · {bvid} BV号 · {quality} 画质 · {codec} 编码</div>`;
h+=`<div class="bdl-act" style="margin-top:8px"><button class="bdl-btn bdl-b2" id="bdl-batch"${S.batchSet.size?'':' disabled'}>📦 批量下载 (${S.batchSet.size}集)</button></div></div>`;
h+=`<div class="bdl-pg" id="bdl-pg"><div class="bdl-pb-bg"><div class="bdl-pb" id="bdl-pb"></div></div><div class="bdl-pt"><span id="bdl-pc">0%</span><span id="bdl-dt"></span></div></div><div class="bdl-msg" id="bdl-msg"></div>`;
return h;
}
root.innerHTML=`<div id="bdl-ov"></div><div id="bdl-md"><div id="bdl-hd"><div class="bdl-logo">B</div><span class="bdl-hd-t">视频下载器</span><span class="bdl-hd-s">v2 · ${S.type==='bangumi'?S.bTypeName:'视频'} · 纯JS</span><button id="bdl-x" title="关闭">✕</button></div><div id="bdl-bd">${buildBody()}</div></div>`;
document.body.appendChild(root);
/* ═══════════
★★★ 关键修复: 每次都重新查询 DOM,不用缓存引用
rerender() 会销毁旧 DOM,缓存引用会指向游离节点
═══════════ */
function setMsg(t,tp){const el=$('#bdl-msg');if(el){el.textContent=t;el.className='bdl-msg'+(tp==='ok'?' ok':tp==='err'?' err':'')}}
function showP(){const pg=$('#bdl-pg'),pb=$('#bdl-pb');if(pg)pg.classList.add('on');if(pb)pb.style.width='0%'}
function hideP(){const pg=$('#bdl-pg');if(pg)pg.classList.remove('on')}
function upP(p,d){const pb=$('#bdl-pb'),pc=$('#bdl-pc'),pt=$('#bdl-dt');if(pb)pb.style.width=p+'%';if(pc)pc.textContent=p+'%';if(pt&&d)pt.textContent=d}
function buildLinks(){const st=S.str,vU=st.vs[S.selV]?.url||'',aU=st.as[S.selA]?.url||'';let h='';if(vU)h+=`<div class="bdl-lr"><span class="bdl-ll">视频</span><span class="bdl-lu" title="${vU}">${vU}</span><button class="bdl-cp" data-c="${vU}">复制</button></div>`;if(aU)h+=`<div class="bdl-lr"><span class="bdl-ll">音频</span><span class="bdl-lu" title="${aU}">${aU}</span><button class="bdl-cp" data-c="${aU}">复制</button></div>`;h+=`<div class="bdl-lr"><span class="bdl-ll">封面</span><span class="bdl-lu" title="${S.cover}">${S.cover}</span><button class="bdl-cp" data-c="${S.cover}">复制</button></div>`;return h;}
$('#bdl-lk').innerHTML=buildLinks();
function bindCopyAll(){document.querySelectorAll('.bdl-cp').forEach(btn=>{btn.onclick=async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(btn.dataset.c);btn.textContent='✓';setTimeout(()=>btn.textContent='复制',1200)}catch{setMsg('复制失败','err')}}})}
bindCopyAll();
/* ═══════════ 事件绑定 ═══════════ */
async function loadEp(gi){
const ep=getEpInfo(gi);S.curEpIdx=gi;
for(let si=0;si<S.secGroups.length;si++){if(S.secGroups[si].eps.some(e=>e.gi===gi)){S.curSecIdx=si;break;}}
setMsg('⏳ 加载: '+ep.name+'...');
try{S.play=await getPlayFor(gi);S.str=parseStreams(S.play);S.selV=0;S.selA=0;rerender();setMsg('✅ '+ep.name,'ok')}catch(er){setMsg('❌ '+er.message,'err')}
}
function updateEpGridVisual(){document.querySelectorAll('.bdl-ep-sq').forEach(sq=>{const gi=+sq.dataset.gi;sq.classList.toggle('sel',gi===S.curEpIdx);sq.classList.toggle('batch',S.batchSet.has(gi));});const c=$('#bdl-bcnt');if(c)c.textContent='已选 '+S.batchSet.size+' 集';const b=$('#bdl-batch');if(b){b.textContent='📦 批量下载 ('+S.batchSet.size+'集)';b.disabled=!S.batchSet.size;}}
function updatePreview(){const pv=$('#bdl-npv');if(pv)pv.textContent='预览: '+previewName();}
function bindAll(){
document.querySelectorAll('.bdl-sec-tab').forEach(tab=>{tab.onclick=async()=>{const si=+tab.dataset.si;if(si===S.curSecIdx)return;S.curSecIdx=si;const first=S.secGroups[si].eps[0];if(first)await loadEp(first.gi);};});
const epg=$('#bdl-epg');if(epg)epg.onclick=async e=>{const sq=e.target.closest('.bdl-ep-sq');if(!sq)return;const gi=+sq.dataset.gi;if(S.batchSet.has(gi))S.batchSet.delete(gi);else S.batchSet.add(gi);updateEpGridVisual();if(gi!==S.curEpIdx)await loadEp(gi);};
const sa=$('#bdl-selall'),sn=$('#bdl-selnone'),si=$('#bdl-selinv');
if(sa)sa.onclick=()=>{S.secGroups[S.curSecIdx].eps.forEach(ep=>S.batchSet.add(ep.gi));updateEpGridVisual();};
if(sn)sn.onclick=()=>{S.secGroups[S.curSecIdx].eps.forEach(ep=>S.batchSet.delete(ep.gi));updateEpGridVisual();};
if(si)si.onclick=()=>{S.secGroups[S.curSecIdx].eps.forEach(ep=>{if(S.batchSet.has(ep.gi))S.batchSet.delete(ep.gi);else S.batchSet.add(ep.gi);});updateEpGridVisual();};
const vt=$('#bdl-vtb');if(vt)vt.onclick=e=>{const tr=e.target.closest('tr[data-v]');if(!tr)return;vt.querySelectorAll('tr').forEach(r=>r.classList.remove('sel'));tr.classList.add('sel');S.selV=+tr.dataset.v;$('#bdl-lk').innerHTML=buildLinks();bindCopyAll();updatePreview();};
const at=$('#bdl-atb');if(at)at.onclick=e=>{const tr=e.target.closest('tr[data-a]');if(!tr)return;at.querySelectorAll('tr').forEach(r=>r.classList.remove('sel'));tr.classList.add('sel');S.selA=+tr.dataset.a;$('#bdl-lk').innerHTML=buildLinks();bindCopyAll();};
const qt=$('#bdl-qtog');if(qt)qt.onclick=()=>{S.showAllQ=!S.showAllQ;rerender();};
const np=$('#bdl-npre');if(np)np.onchange=e=>{S.nameFmt=NAME_PRESETS[e.target.value]||S.nameFmt;const ni=$('#bdl-nfmt');if(ni)ni.value=S.nameFmt;updatePreview();};
const ni=$('#bdl-nfmt');if(ni)ni.oninput=e=>{S.nameFmt=e.target.value;updatePreview();};
const sd=$('#bdl-seldir');if(sd)sd.onclick=async()=>{try{S.dirHandle=await window.showDirectoryPicker();sd.textContent='📁 '+S.dirHandle.name;setMsg('✅ 目录: '+S.dirHandle.name,'ok')}catch(e){if(e.name!=='AbortError')setMsg('目录选择不可用','err')}};
const B=(id,fn)=>{const b=$(id);if(b)b.onclick=fn};
B('#bdl-mg',doMerge);
B('#bdl-dv',()=>{const v=S.str.vs[S.selV];if(!v)return setMsg('请选择视频流','err');dlFile(v.url,san(S.title)+'_'+v.quality.replace(/\s/g,'')+'.m4s','视频')});
B('#bdl-da',()=>{const a=S.str.as[S.selA];if(!a)return setMsg('请选择音频流','err');dlFile(a.url,san(S.title)+'_audio.m4s','音频')});
B('#bdl-dc',()=>window.open(S.cover,'_blank'));
B('#bdl-cm',async()=>{const v=S.str.vs[S.selV],a=S.str.as[S.selA];const vn=san(S.title)+'_'+(v?.quality||'v').replace(/\s/g,'')+'.m4s';const an=a?san(S.title)+'_audio.m4s':'';const cmd=an?`ffmpeg -i "${vn}" -i "${an}" -c copy "${san(S.title)}.mp4"`:`ffmpeg -i "${vn}" -c copy "${san(S.title)}.mp4"`;try{await navigator.clipboard.writeText(cmd);setMsg('📋 已复制','ok')}catch{prompt('复制:',cmd)}});
B('#bdl-batch',batchDownload);
}
bindCopyAll();bindAll();
function rerender(){
$('#bdl-bd').innerHTML=buildBody();
$('#bdl-lk').innerHTML=buildLinks();
bindCopyAll();
bindAll();
}
/* ═══════════ 下载 ═══════════ */
function saveBlob(b,n){const a=Object.assign(document.createElement('a'),{href:URL.createObjectURL(b),download:n});document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),60000)}
async function saveToFile(blob,filename){if(S.dirHandle){try{const fh=await S.dirHandle.getFileHandle(filename,{create:true});const w=await fh.createWritable();await w.write(blob);await w.close();return;}catch(e){log('目录写入失败: '+e.message);}}saveBlob(blob,filename);}
async function dlFile(url,name,label){showP();setMsg('⬇️ '+label);try{const r=await fetch(url);if(!r.ok)throw new Error('HTTP '+r.status);const total=parseInt(r.headers.get('content-length'))||0,rd=r.body.getReader(),ch=[];let loaded=0,lp=-1,t0=Date.now();for(;;){const{done,value}=await rd.read();if(done)break;ch.push(value);loaded+=value.length;if(total){const p=Math.floor(loaded*100/total);if(p!==lp){lp=p;const sp=loaded/((Date.now()-t0)/1000||1);const eta=total>loaded?((total-loaded)/sp)|0:0;upP(p,fmtB(loaded)+'/'+fmtB(total)+' '+fmtB(sp)+'/s 剩余'+fmtD(eta))}}}const blob=new Blob(ch);await saveToFile(blob,name);upP(100,fmtB(blob.size));setMsg('✅ '+name+' ('+fmtB(blob.size)+')','ok');setTimeout(hideP,2500);return blob}catch(er){setMsg('❌ '+er.message,'err');hideP();return null}}
async function dlBuf(url,cb){const r=await fetch(url);if(!r.ok)throw new Error('HTTP '+r.status);const total=parseInt(r.headers.get('content-length'))||0,rd=r.body.getReader(),ch=[];let loaded=0,lp=-1;for(;;){const{done,value}=await rd.read();if(done)break;ch.push(value);loaded+=value.length;if(total){const p=Math.floor(loaded*100/total);if(p!==lp){lp=p;cb(p,loaded,total)}}}return new Blob(ch).arrayBuffer();}
async function dlBufQ(url){const r=await fetch(url);if(!r.ok)throw new Error('HTTP '+r.status);const rd=r.body.getReader(),ch=[];for(;;){const{done,value}=await rd.read();if(done)break;ch.push(value);}return new Blob(ch).arrayBuffer();}
async function doMerge(){
const v=S.str.vs[S.selV],a=S.str.as[S.selA];if(!v)return setMsg('请选择视频流','err');
if(!a){return dlFile(v.url,san(S.title)+'.m4s','视频')}
try{showP();setMsg('⬇️ 下载视频流...');upP(0,'开始...');const vBuf=await dlBuf(v.url,(p,ld,tot)=>upP(Math.round(p*.4),'视频 '+fmtB(ld)+'/'+fmtB(tot)));upP(40,'视频完成');setMsg('⬇️ 下载音频流...');await sleep(200);const aBuf=await dlBuf(a.url,(p,ld,tot)=>upP(40+Math.round(p*.3),'音频 '+fmtB(ld)+'/'+fmtB(tot)));upP(72,'音频完成');setMsg('🔀 合并中...');await sleep(100);upP(80,'解析 fMP4...');const blob=mergefMP4(vBuf,aBuf);const outName=fillName(getEpInfo(S.curEpIdx));await saveToFile(blob,outName);upP(100,fmtB(blob.size));setMsg('✅ '+outName+' ('+fmtB(blob.size)+')','ok');setTimeout(hideP,3000);}catch(er){setMsg('❌ '+er.message,'err');console.error('[BiliDL]',er)}
}
/* ═══════════ 批量下载 ═══════════ */
async function batchDownload(){
const selected=[...S.batchSet].sort((a,b)=>a-b);if(!selected.length)return setMsg('请先选择集数','err');
const total=selected.length;let ok=0,fail=0;
const savedCid=S.cid,savedAvid=S.avid,savedBvid=S.bvid,savedEpId=S.epId;
showP();
for(let i=0;i<total;i++){
const gi=selected[i],ep=getEpInfo(gi);
upP(Math.round((i/total)*100),`[${i+1}/${total}] ${ep.name}`);
setMsg(`📦 [${i+1}/${total}] ${ep.name}...`);
try{
const play=await getPlayFor(gi);const str=parseStreams(play);
const v=str.vs[S.selV]||str.vs[0];const a=str.as[S.selA]||str.as[0];
if(!v)throw new Error('无视频流');
const filename=fillName(ep);
if(a&&str.fmt==='DASH'){const vBuf=await dlBufQ(v.url);const aBuf=await dlBufQ(a.url);const blob=mergefMP4(vBuf,aBuf);await saveToFile(blob,filename);}
else{const r=await fetch(v.url);const blob=await r.blob();await saveToFile(blob,filename);}
ok++;log(`✅ [${i+1}/${total}] ${filename}`);
}catch(er){fail++;log(`❌ [${i+1}/${total}] ${ep.name}: ${er.message}`);}
await sleep(300);
}
Object.assign(S,{cid:savedCid,avid:savedAvid,bvid:savedBvid,epId:savedEpId});
upP(100,`完成: ${ok}成功 ${fail}失败`);
setMsg(`🎉 批量完成: ${ok}成功${fail?', '+fail+'失败':''}`,ok>0?'ok':'err');
setTimeout(hideP,4000);
}
/* ═══════════ 关闭 & 拖拽 ═══════════ */
const close=()=>root.remove();$('#bdl-x').onclick=close;$('#bdl-ov').onclick=close;
document.addEventListener('keydown',function esc(e){if(e.key==='Escape'){close();document.removeEventListener('keydown',esc)}});
let dr=false,ox,oy,sx,sy;const hd=$('#bdl-hd'),md=$('#bdl-md');
hd.addEventListener('mousedown',e=>{if(e.target.id==='bdl-x')return;dr=true;ox=e.clientX;oy=e.clientY;const r=md.getBoundingClientRect();sx=r.left;sy=r.top;md.style.transition='none';e.preventDefault()});
document.addEventListener('mousemove',e=>{if(!dr)return;md.style.position='fixed';md.style.left=(sx+e.clientX-ox)+'px';md.style.top=(sy+e.clientY-oy)+'px';md.style.margin='0'});
document.addEventListener('mouseup',()=>{dr=false;md.style.transition=''});
log('✅ v2 已加载 · '+S.str.vs.length+' 视频流 · '+S.str.as.length+' 音频流 · '+S.secGroups.length+' 分区');
})();哔哩哔哩浏览器控制台最高画质下载电影番剧视频
(async function bilibiliDownloader() {
'use strict';
document.getElementById('bdl-root')?.remove();
document.getElementById('bdl-css')?.remove();
/*