// ==UserScript== // @name 豆瓣下载助手 // @namespace https://github.com/Shshuaicc/douban-download-master // @version 5.3.1 // @description 豆瓣电影详情页下载/网盘/在线 三大类资源聚合搜索 v5.3.1 - 站点扩容·实测分层 // @author Shshuaicc // @match *://movie.douban.com/subject/* // @connect * // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_setClipboard // @grant GM_registerMenuCommand // @run-at document-start // @license MIT // ==/UserScript== /* * ============================================================ * 豆瓣下载助手 v5.3.1 (原名「豆瓣电影下载大师」) * 更新日志: * v5.3.1 - 站点扩容(三类各新增实测可用站,按实测延迟分层) * 【采集】新增候选 2,292 个域名:FMHY 国际清单(video/torrenting/downloading 三页) * + 14 篇中文影视/网盘/磁力推荐榜单,全部来自可抓取来源,不凭记忆编造网址 * 【验证】可达性初筛 → 首页指纹(maccms/WordPress/Next/表单 action)→ 定向搜索端点 * → 真实片名请求 + 结果页特征判定,两轮(严格 / 前端渲染宽松)互补 * 【相关性】结果页须含发布标记(1080p/BluRay/x264/磁力)或影视/网盘字样, * 并剔除过期域名被抢注后的跳转站(丹麦高校、工具箱、Object Cache 等 8 例) * 【新增】下载 +27 · 网盘 +22 · 在线 +30 = 共 79 站, * 每类内部按实测延迟三分位划 快速/中速/慢速;站点总数 81 → 160 * 【说明】在线类目标 30 已达成;下载 27、网盘 22 为「实测可出结果」的上限—— * 中文网盘搜索生态可枚举站点已基本收齐,其余候选已停放/失效/需代理 * * v5.3.0 - 更名·站点实测重排·代码精简 * 【更名】程序名统一为「豆瓣下载助手」:脚本名、面板标题、启动标记、日志前缀 * 【站点校准】246 站全量实测(真实关键词请求 ×3 轮重试 + 根域二次探测 + 响应体特征判定): * - 删除 165 个连不上的站点:域名解析失败 29、连接超时 51、拒绝连接 8、 * HTTP 4xx/5xx 40、Cloudflare 挑战页 8,以及返回「域名出售/跳转/人机验证」 * 空壳却仍被标绿的站点(人人影视·深影字幕组·MK影视·YaPan·云盘精灵 等) * - 修复 9 个「站点存活但搜索路径写错」的网盘站端点(搜百度盘·影猫导航·夸克资源· * 天天云搜·海搜·知了搜·盘友圈·盘库吧·盘搜搜·云盘4K,原 /search?keyword= 全为 404) * - 新增 6 个实测可用站:蓝光电影(下载)·盘搜Pro·天天搜索·皮卡搜索·夸父资源社(网盘)· * 影猫仓库(在线)·东京图书馆(国际BT) * - 国际站按类收敛:下载 38→7、在线 46→17,仅保留实测可达者 * - 结果:246 → 81 站,全部复测可达;快速层实测可用 12 → 43 * (下载快速 0→4、在线快速 0→10、网盘快速 12→29) * - 梯队按实测延迟重排:快 <1s · 中 1-3s · 慢=国际站(需代理); * 原快速层中已连不上的站点全部剔除而非降级 * 【代码精简】界面元素与 DOM 结构逐节点校验为完全一致: * - 移除 GBK 码表模块(仅服务于已失效的 4 个 GBK-POST 站点)与 POST 检测分支 * - 梯队可见性由「每个站点完成后全量重扫」改为增量计数,消除 O(n²) 扫描 * - 删除未被消费的字段(导演/主演/片长/类型、noResult、badgeEl、useImdb)、 * 站点专用解析器表(10 项对应站点已全部下线)、旧版 GM.xmlHttpRequest 兼容分支 * - 合并三段同构的分区初始化、面板卸载与缓存清理逻辑;定时器 5 路收敛为 1 路轮询 * - 修正 v5.2.1 遗留并发缺陷:pool 的 done 回调在同帧多站点收尾时会重复触发, * 导致下一梯队被并发重复启动、可用站点计数翻倍(如显示 133/87) * - 体积 1927 行/114KB → 1578 行/89KB(-22%) * v5.2.1 - 界面精简(删除「复制全部可用链接」按钮) * v5.2.0 - 纯白实底化·结果纯化·国际站全自动 * v5.1.0 - 配色可读性重构·去荧光·全量淡字强化 * v5.0.0 - 视觉降噪重构·信息提纯·双模适配 * v4.x - 分层加载 / FMHY 国际站集成 / 加权评分 / iOS 毛玻璃 * 注:CSS 类名与存储键沿用 ddm- 前缀(内部标识,不显示于界面)。 * ============================================================ */ (function() { var NAME = '豆瓣下载助手', VER = '5.31'; // ============================================================ // 【0号优先级】立即注入可见标记 // ============================================================ try { var _ddm_mark = document.createElement('div'); _ddm_mark.id = 'ddm-boot-mark'; _ddm_mark.innerHTML = '' + NAME + ' ' + VER; _ddm_mark.style.cssText = 'position:fixed;top:14px;left:14px;z-index:2147483647;background:#ffffff;color:#1c1c1e;font-size:12px;font-weight:600;padding:8px 16px;border-radius:18px;box-shadow:0 4px 16px rgba(0,0,0,0.08),inset 0 0 0 1px rgba(120,120,128,0.16);cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang SC",sans-serif;transition:all .35s cubic-bezier(0.34,1.56,0.64,1);letter-spacing:0.2px;display:flex;align-items:center;'; _ddm_mark.title = NAME + ' v' + VER + ' 已加载 - 点击关闭'; _ddm_mark.onclick = function() { if (_ddm_mark.parentNode) _ddm_mark.parentNode.removeChild(_ddm_mark); }; function _ddm_addMark() { if (document.getElementById('ddm-boot-mark')) return; if (!document.body) { setTimeout(_ddm_addMark, 50); return; } document.body.appendChild(_ddm_mark); setTimeout(function() { if (_ddm_mark.parentNode) { _ddm_mark.style.background = 'rgba(26,127,55,0.07)'; _ddm_mark.innerHTML = '' + NAME + ' ' + VER + ' ✓'; } }, 1500); } _ddm_addMark(); console.log('%c[' + NAME + ' v' + VER + '] 已启动', 'color:#1c1c1e;font-size:14px;font-weight:bold;'); } catch(e) { try { alert(NAME + ' v' + VER + '启动: ' + e.message); } catch(e2) {} } // ============================================================ // 工具函数 (纯ES5) // ============================================================ var LOGP = '[DDA' + VER + ']'; function log() { try { var a=[LOGP]; for(var i=0;i/g,'>').replace(/"/g,'"').replace(/'/g,'''); } function randomInt(a,b) { return Math.floor(Math.random()*(b-a+1))+a; } function randomChoice(arr) { return arr[Math.floor(Math.random()*arr.length)]; } function addCSS(css) { try { if (typeof GM_addStyle !== 'undefined') { GM_addStyle(css); return; } } catch(e){} try { var s=document.createElement('style'); s.type='text/css'; s.textContent=css; (document.head||document.documentElement).appendChild(s); } catch(e){} } function copyText(text) { try { if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); return true; } } catch(e){} try { var ta=document.createElement('textarea'); ta.value=text; ta.style.position='fixed'; ta.style.left='-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); return true; } catch(e){ return false; } } // ============================================================ // v4.9.0 UI辅助: 展开动画 / Toast / Tooltip // ============================================================ function animateBodyIn(body) { if (!body) return; body.classList.remove('ddm-body-in'); void body.offsetWidth; body.classList.add('ddm-body-in'); } function showToast(msg) { try { var old = document.getElementById('ddm-toast'); if (old && old.parentNode) old.parentNode.removeChild(old); var t = document.createElement('div'); t.id = 'ddm-toast'; t.className = 'ddm-toast'; t.textContent = msg; document.body.appendChild(t); setTimeout(function() { if (t.parentNode) t.classList.add('ddm-toast-hide'); }, 2700); setTimeout(function() { if (t.parentNode) t.parentNode.removeChild(t); }, 3150); } catch(e) { logErr('showToast error', e); } } function showTooltipFor(a, ev) { hideTooltip(); try { var tip = document.createElement('div'); tip.id = 'ddm-tip'; tip.className = 'ddm-tip'; var nm = a.textContent || ''; var desc = a.getAttribute('data-desc') || ''; var tier = a.getAttribute('data-tier') || ''; var html = '
' + esc(nm) + '
'; if (desc) html += '
' + esc(desc) + '
'; if (tier) html += '' + esc(tier) + ''; tip.innerHTML = html; document.body.appendChild(tip); var x = ev.clientX, y = ev.clientY; var tw = tip.offsetWidth || 100, th = tip.offsetHeight || 40; var nx = Math.max(8, Math.min(x - Math.round(tw / 2), window.innerWidth - tw - 8)); var ny = y - th - 14; if (ny < 8) ny = y + 20; tip.style.left = nx + 'px'; tip.style.top = ny + 'px'; } catch(e) { logErr('showTooltip error', e); } } function hideTooltip() { try { var tip = document.getElementById('ddm-tip'); if (tip && tip.parentNode) tip.parentNode.removeChild(tip); } catch(e) {} } // ============================================================ // 反爬模块 // ============================================================ var UA_POOL = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' ]; var REFERER_POOL = ['https://www.baidu.com/','https://www.google.com/','https://www.bing.com/','https://movie.douban.com/','']; var ACCEPT_LANG_POOL = ['zh-CN,zh;q=0.9,en;q=0.8','zh-CN,zh;q=0.9']; function getRandomHeaders() { var h = { 'User-Agent':randomChoice(UA_POOL), 'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language':randomChoice(ACCEPT_LANG_POOL), 'Accept-Encoding':'gzip, deflate', 'Cache-Control':'max-age=0', 'DNT':'1' }; var r = randomChoice(REFERER_POOL); if (r) h['Referer']=r; return h; } // ============================================================ // XHR封装 // ============================================================ var _xhrCache = {}; var _requestLastTime = {}; var MIN_REQUEST_INTERVAL = 200; function gmXhr(opts, cb) { var url = opts.url; var headers = opts.headers || {}; var timeout = opts.timeout || 8000; var maxRetries = opts.retries || 1; var retryCount = 0; var cacheKey = url; if (!opts.noCache && _xhrCache[cacheKey] && (Date.now()-_xhrCache[cacheKey].t < 300000)) { setTimeout(function(){cb(null,_xhrCache[cacheKey].r);},0); return; } function doRequest() { try { var fh = getRandomHeaders(); for (var k in headers) { if (hasOwn(headers,k)) fh[k]=headers[k]; } var host = url.replace(/^https?:\/\//,'').split('/')[0]; var now = Date.now(); var wt = Math.max(0, MIN_REQUEST_INTERVAL-(now-(_requestLastTime[host]||0))); _requestLastTime[host] = now+wt; function retry(msg) { if (retryCount++ < maxRetries) { setTimeout(doRequest, 500*Math.pow(2,retryCount-1)+randomInt(100,300)); return; } cb(new Error(msg)); } function exec() { if (typeof GM_xmlhttpRequest === 'undefined') { cb(new Error('no GM_xmlhttpRequest')); return; } GM_xmlhttpRequest({ method:'GET', url:url, headers:fh, timeout:timeout, onload: function(res) { if(!opts.noCache) _xhrCache[cacheKey]={r:res,t:Date.now()}; cb(null,res); }, onerror: function() { retry('network error'); }, ontimeout: function() { retry('timeout'); } }); } wt > 0 ? setTimeout(exec,wt) : exec(); } catch(e) { cb(e); } } doRequest(); } // ============================================================ // 中文数字转换 // ============================================================ function cn2num(s) { if(/^\d+$/.test(s))return s; var m={'零':0,'一':1,'二':2,'两':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10}; if(s.length===1)return String(m[s]||0); if(s.charAt(0)==='十')return String(10+(m[s.charAt(1)]||0)); if(s.charAt(s.length-1)==='十')return String((m[s.charAt(0)]||0)*10); if(s.indexOf('十')!==-1){var p=s.split('十');return String((m[p[0]]||0)*10+(m[p[1]]||0));} return s; } function pad2(n){n=String(n);return n.length<2?'0'+n:n;} // ============================================================ // 提取电影信息 // ============================================================ function getMovieInfo() { var info = {cnTitle:'',enTitle:'',year:'',imdbId:'',isTV:false,season:'',episode:'',kw:'',kwEn:'',kwCandidates:[]}; try { var h1 = document.querySelector('h1 span[property="v:itemreviewed"]') || document.querySelector('h1'); if (!h1) return info; var rawTitle = (h1.textContent||'').trim(); var yearEl = document.querySelector('h1 .year'); if (yearEl) { var ym=(yearEl.textContent||'').match(/\((\d{4})\)/); if(ym)info.year=ym[1]; } if (!info.year) { var tym=rawTitle.match(/\((\d{4})\)/); if(tym){info.year=tym[1]; rawTitle=rawTitle.replace(/\s*\(\d{4}\)\s*$/,'').trim();} } // 韩文/日文/西里尔/泰文等外语字种出现处截断,仅保留其前的中文片名 var fm = rawTitle.match(/[\u3040-\u30ff\uac00-\ud7af\u0400-\u04ff\u0e00-\u0e7f]/); var cnTitle = fm ? rawTitle.slice(0, fm.index) : rawTitle; if (cnTitle !== rawTitle && cnTitle.trim()) { info.cnTitle = cnTitle.trim(); } else { var t1 = rawTitle.match(/^([\u4e00-\u9fa5][\s\S]*?)\s+([A-Za-z][\s\S]*)$/); if (t1) { info.cnTitle=t1[1].trim(); info.enTitle=t1[2].trim(); } else { info.cnTitle=rawTitle; } } if (!info.enTitle) { var enMatch = rawTitle.match(/\s+([A-Za-z][A-Za-z0-9\s\-\.:,'&]*)$/); if (enMatch) info.enTitle = enMatch[1].trim(); } var infoEl = document.querySelector('#info'); if (infoEl) { var infoText = infoEl.textContent||''; var im = infoText.match(/IMDb:\s*(tt\d+)/i); if(im)info.imdbId=im[1]; if (/集数|首播|电视台/.test(infoText)) info.isTV = true; if (!info.enTitle) { var ak=infoText.match(/又名:\s*([^\n]+)/); if(ak){var akas=ak[1].split(/[\/,,]/); for(var ai=0;ai 0)) return; _respTimes.push(elapsed); if (_respTimes.length > 30) _respTimes.shift(); if (_respTimes.length < 8) return; var sum = 0; for (var i = 0; i < _respTimes.length; i++) sum += _respTimes[i]; var avg = sum / _respTimes.length; var prev = CONCUR.current; if (avg < 1200) { CONCUR.current = Math.min(CONCUR.max, CONCUR.current + 2); } else if (avg > 3500) { CONCUR.current = Math.max(CONCUR.min, CONCUR.current - 1); } if (CONCUR.current !== prev) log('concurrency adjusted: ' + prev + ' -> ' + CONCUR.current + ' (avg=' + Math.round(avg) + 'ms)'); } function pool(items, limit, worker, progressCb, done) { var results = new Array(items.length); var cursor = 0, running = 0, finished = 0, doneFired = false; function getLim() { return (limit > 0) ? limit : getConcurrency(); } function next() { // 多个 worker 同帧收尾时 next() 会被重复调度,done 只允许触发一次, // 否则下一梯队被并发重复启动、可用站点计数翻倍(v5.2.1 遗留缺陷) if (finished >= items.length) { if (done && !doneFired) { doneFired = true; try { done(results); } catch(e) { logErr('pool done error', e); } } return; } while (running < getLim() && cursor < items.length) { var idx = cursor++; running++; (function(i) { try { worker(items[i], i, function(err, r) { results[i] = err ? null : r; running--; finished++; if (progressCb) try { progressCb(finished, items.length); } catch(e) {} setTimeout(next, randomInt(10, 50)); }); } catch(e) { logErr('pool worker error', e); running--; finished++; results[i] = null; if (progressCb) try { progressCb(finished, items.length); } catch(e2) {} setTimeout(next, 10); } })(idx); } } if (progressCb) try { progressCb(0, items.length); } catch(e) {} next(); } // ============================================================ // 网盘API搜索 // ============================================================ function searchPanApi(kw, cb) { var apiUrl = PAN_API_URL + encodeURIComponent(kw); gmXhr({url:apiUrl, timeout:10000, retries:2}, function(err, res) { if (err || !res || !res.responseText) { cb([]); return; } try { var json = JSON.parse(res.responseText); var items = []; var seen = {}; if (json.code === 0 || json.code === '0') { var data = json.data || {}; var groups = data.merged_by_type || data.by_type || data; var tagMap = {ali:'ali',aliyun:'ali',quark:'quark',baidu:'baidu',baidupan:'baidu',xunlei:'xunlei',thunder:'xunlei'}; var tagName = {ali:'阿里',quark:'夸克',baidu:'百度',xunlei:'迅雷',other:'网盘'}; var totalAdded = 0; var MAX_TOTAL = 20; var PER_GROUP = 6; for (var type in groups) { if (!hasOwn(groups, type)) continue; if (totalAdded >= MAX_TOTAL) break; var arr = groups[type]; var t = tagMap[type.toLowerCase()] || 'other'; if (Object.prototype.toString.call(arr) === '[object Array]') { var groupCount = 0; for (var i = 0; i < arr.length && groupCount < PER_GROUP && totalAdded < MAX_TOTAL; i++) { var it = arr[i]; var name = it.note||it.name||it.title||it.filename||'资源'; var url = it.url||it.link||it.share_url||''; var pwd = it.password||it.pwd||it.code||it.extract_code||''; if (url) { var normUrl = url.replace(/[?&#].*$/, '').toLowerCase(); if (seen[normUrl]) continue; seen[normUrl] = true; items.push({name:name, url:url, pwd:pwd, tag:t, tagLabel:tagName[t]}); groupCount++; totalAdded++; } } } } } cb(items); } catch(e) { logErr('pan api parse error', e); cb([]); } }); } // ============================================================ // 站点检测 // ============================================================ // v4.7.0: 站点健康度持久化缓存 (GM_setValue/GM_getValue) var HEALTH_KEY_PREFIX = 'ddm_health_'; var HEALTH_TTL = 24 * 60 * 60 * 1000; // 24小时 function getHealth(name) { try { if (typeof GM_getValue === 'undefined') return null; var v = GM_getValue(HEALTH_KEY_PREFIX + name, ''); if (!v) return null; var o = JSON.parse(v); if (o && typeof o.a !== 'undefined' && typeof o.t !== 'undefined') return o; } catch(e) {} return null; } function setHealth(name, available) { try { if (typeof GM_setValue === 'undefined') return; GM_setValue(HEALTH_KEY_PREFIX + name, JSON.stringify({ a: available ? 1 : 0, t: Date.now() })); } catch(e) {} } // 24h 内标记为不可用的站点 → 直接标灰跳过请求 function isHealthSkipped(name) { var h = getHealth(name); return !!(h && h.a === 0 && (Date.now() - h.t) < HEALTH_TTL); } // ============================================================ // v4.8.0 站点类型标记 (智能关键词选择用) // ============================================================ function tagSiteCategories() { var i; for (i = 0; i < DOWNLOAD_SITES.length; i++) { var s = DOWNLOAD_SITES[i]; s._cat = (s.tier === 3) ? 'bt_en' : ((s.tier === 1) ? 'bt_zh' : 'dl_zh'); } for (i = 0; i < PAN_SEARCH_SITES.length; i++) PAN_SEARCH_SITES[i]._cat = 'pan'; for (i = 0; i < ONLINE_SITES.length; i++) ONLINE_SITES[i]._cat = 'online'; } // ============================================================ // v4.8.0 反拦截检测 (Cloudflare 挑战页) // ============================================================ function isBlocked(text) { if (!text) return false; var low = text.toLowerCase(); if (low.indexOf('cf-browser-verification') !== -1) return true; if (low.indexOf('challenge-platform') !== -1) return true; if (low.indexOf('attention required') !== -1) return true; if (text.length < 3000 && (low.indexOf('just a moment') !== -1 || low.indexOf('checking your browser') !== -1)) return true; return false; } // ============================================================ // v4.8.0 加权评分算法 // ============================================================ var NEGATIVE_KW = ['没有找到','暂无相关','未找到','搜索不到','无结果','没有搜索到','验证码','captcha','404','页面不存在','page not found','no results','nothing found','0 results','0个结果','0 条结果','该关键词没有找到']; function scoreResult(text, movie) { var score = 0; var low = text.toLowerCase(); var i; // 负面信号 -3 for (i = 0; i < NEGATIVE_KW.length; i++) { if (low.indexOf(NEGATIVE_KW[i].toLowerCase()) !== -1) { score -= 3; break; } } // 页面过短 -5 if (text.length < 800) score -= 5; // 强信号 +3 if (low.indexOf('magnet:?xt=') !== -1) score += 3; if (/\.torrent/i.test(low)) score += 3; if (/(pan\.baidu\.com|aliyundrive|alipan\.com|quark\.cn|cloud\.189\.cn|tianyi|weiyun|pan\.quark\.cn)/i.test(low)) score += 3; // 标题强匹配 +4 if (movie.cnTitle && text.indexOf(movie.cnTitle) !== -1) score += 4; if (movie.enTitle && low.indexOf(movie.enTitle.toLowerCase()) !== -1) score += 4; // 标题前4字部分匹配 +2 var pre4 = (movie.cnTitle || '').substring(0, 4); if (pre4.length >= 2 && text.indexOf(pre4) !== -1) score += 2; var pre4en = (movie.enTitle || '').substring(0, 4).toLowerCase(); if (pre4en.length >= 3 && low.indexOf(pre4en) !== -1) score += 2; // 年份出现 +1 if (movie.year && low.indexOf(movie.year) !== -1) score += 1; // 结果列表HTML结构 +1~2 if (/]+href="[^"]+\.(html|htm|mp4|mkv|torrent|ram|iso|m3u8|rmvb)/i.test(low)) score += 1; if (/class="[^"]*(item|result|torrent|list|post|article|entry|search-item|movie-item|resource|share|search-list|box-list|video-list)/i.test(low)) score += 1; if (text.length > 15000 && /]*>.*<\/li>/i.test(text)) score += 1; // 下载/播放词汇 +1 if (/(下载|磁力|迅雷|thunder|bt下载|在线观看|播放|watch|play|stream|ed2k|bt种子)/i.test(low)) score += 1; return score; } // ============================================================ // v4.8.0 智能关键词选择 (按站点类型选最优候选, 最多3个) // ============================================================ function pickCandidates(site, movie) { var base = movie.kwCandidates || []; var list = []; function push(s) { if (s && list.indexOf(s) === -1 && base.indexOf(s) !== -1) list.push(s); } var cat = site._cat || 'online'; if (site.useEn || cat === 'bt_en') { // 磁力/BT国际站:优先英文 push(movie.enTitle); push(movie.enTitle ? (movie.enTitle + (movie.year ? ' ' + movie.year : '')) : ''); push(movie.cnTitle); push(movie.cnTitle ? (movie.cnTitle + (movie.year ? ' ' + movie.year : '')) : ''); } else if (cat === 'dl_zh') { // 中文下载站:中文标题+年份优先 push(movie.cnTitle ? (movie.cnTitle + (movie.year ? ' ' + movie.year : '')) : ''); push(movie.cnTitle); var mp = movie.cnTitle ? movie.cnTitle.split(/[::]/)[0].trim() : ''; if (mp && mp !== movie.cnTitle) push(mp); } else if (cat === 'pan') { // 网盘搜索站:纯中文标题不带年份 push(movie.cnTitle); var mp2 = movie.cnTitle ? movie.cnTitle.split(/[::]/)[0].trim() : ''; if (mp2 && mp2 !== movie.cnTitle) push(mp2); push(movie.cnTitle ? (movie.cnTitle + (movie.year ? ' ' + movie.year : '')) : ''); } else { // 在线影视站 / 默认:中文标题 push(movie.cnTitle); var mp3 = movie.cnTitle ? movie.cnTitle.split(/[::]/)[0].trim() : ''; if (mp3 && mp3 !== movie.cnTitle) push(mp3); push(movie.cnTitle ? (movie.cnTitle + (movie.year ? ' ' + movie.year : '')) : ''); } // 剧集季数拼接补充 if (movie.season && movie.cnTitle) push(movie.cnTitle + ' ' + movie.season); var out = []; for (var i = 0; i < list.length && out.length < 3; i++) { if (list[i]) out.push(list[i]); } return out; } // ============================================================ // v4.8.0 多关键词检测 (无结果自动切换, 最多3个) // ============================================================ function testSite(site, movie, cb) { // v4.7.0: 健康度缓存命中(24h 内不可用)直接跳过 if (isHealthSkipped(site.name)) { cb({accessible:false, time:0, available:false, cachedSkip:true}); return; } var candidates = pickCandidates(site, movie); if (!candidates || candidates.length === 0) { cb({accessible:false, time:999999, available:false, skip:true}); return; } tryKeywords(site, movie, candidates, 0, cb); } function tryKeywords(site, movie, cands, idx, cb) { var kw = cands[idx]; var startTime = Date.now(); var timeout = 8000; function finish(extra) { extra = extra || {}; var r = { accessible:true, time: Date.now() - startTime, available:false }; for (var k in extra) { if (hasOwn(extra, k)) r[k] = extra[k]; } cb(r); } function nextOrFail() { if (idx + 1 < cands.length) { setTimeout(function() { tryKeywords(site, movie, cands, idx + 1, cb); }, 20); } else { setHealth(site.name, false); finish(); } } var url = site.url + encodeURIComponent(kw) + (site.suffix || ''); gmXhr({url:url, timeout:timeout, retries:1, noCache:true}, function(err, res) { if (err || !res || !res.responseText) { setHealth(site.name, false); finish({accessible:false, time:999999, error:true}); return; } var text = res.responseText; try { if (isBlocked(text)) { finish({ blocked:true }); return; } if (!site.detect) { // 跳转站 / 前端渲染站:仅做可访问性判定 if (text.length < 500 || (res.status && res.status >= 400)) { nextOrFail(); return; } setHealth(site.name, true); finish({ available:true }); return; } if (scoreResult(text, movie) >= 3) { setHealth(site.name, true); finish({ available:true }); return; } nextOrFail(); } catch(e) { logErr('tryKeywords parse error', e, site.name); setHealth(site.name, true); finish({ available:true, error:true }); } }); } // ============================================================ // v4.7.0 分区渲染 (支持子区域定位);v5.2.0 Tier3 全自动检测 // ============================================================ function renderSectionTiered(opts) { var sitesGroups = opts.sitesGroups; var movie = opts.movie; var sectionId = opts.sectionId; var section = document.getElementById(sectionId); if (!section) return; var totalSites = (sitesGroups[1]||[]).length + (sitesGroups[2]||[]).length + (sitesGroups[3]||[]).length; var totalFound = 0; var totalDone = 0; var allResults = {}; var sectionExpanded = false; var toastShown = false; // v4.7.0: 支持通过 bodyId/arrowId 精确定位子区域(如「更多网盘搜索站」) var sectionBody = opts.bodyId ? document.getElementById(opts.bodyId) : section.querySelector('.ddm-section-body'); var sectionArrow = opts.arrowId ? document.getElementById(opts.arrowId) : section.querySelector('.ddm-section-header .ddm-arrow'); // v5.3.0: 无资源站点检测完成即隐藏,仅显示可用站点; // 梯队可见性用增量计数维护(替代 v5.2.1 每次完成后的全量重扫) var tierState = {}; function initTierWraps() { for (var t = 1; t <= 3; t++) { var cont = document.getElementById(opts.containerPrefix + '-t' + t + '-links'); if (!cont) continue; var wrap = cont.parentNode; if (!wrap || wrap.className.indexOf('ddm-tier-block') === -1) continue; tierState[t] = { wrap: wrap, pending: 0, avail: 0 }; wrap.style.display = 'none'; } } function reportTier(tier) { var st = tierState[tier]; if (st) st.wrap.style.display = (st.pending > 0 || st.avail > 0) ? '' : 'none'; } initTierWraps(); function autoExpandSection() { if (sectionExpanded) return; sectionExpanded = true; if (sectionBody) { sectionBody.style.display = 'block'; // v4.9.0: 展开内容区淡入动画 animateBodyIn(sectionBody); } if (sectionArrow) sectionArrow.textContent = '▾'; updateSummaryCount(); } function updateSummaryCount() { var summaryEl = document.getElementById('ddm-summary-' + opts.summaryKey); if (summaryEl) { summaryEl.textContent = totalFound; if (totalFound > 0) summaryEl.className = 'ddm-summary-num has-val'; } } function updateProgress() { if (opts.barEl) opts.barEl.style.width = Math.round((totalDone/totalSites)*100) + '%'; if (opts.textEl) opts.textEl.textContent = totalDone + '/' + totalSites; if (opts.countEl) { opts.countEl.textContent = totalFound + '/' + totalSites; if (totalFound > 0) opts.countEl.className = 'ddm-section-count has-result'; } updateSummaryCount(); } function finalizeSection() { if (opts.barEl) { opts.barEl.style.width = '100%'; opts.barEl.classList.add('done'); } if (opts.textEl) opts.textEl.textContent = totalDone + '/' + totalSites; if (opts.countEl) { opts.countEl.textContent = totalFound + '/' + totalSites; if (totalFound > 0) opts.countEl.className = 'ddm-section-count has-result'; } if (section) { section.classList.add('ddm-section-completed'); if (totalFound > 0) section.classList.add('ddm-section-has-result'); } // v5.0.0: 无结果 → 折叠分区为一行居中次级文案「暂未找到相关资源」 var emptyTag = opts.emptyElId ? document.getElementById(opts.emptyElId) : null; if (totalFound === 0) { if (sectionBody) sectionBody.style.display = 'none'; if (sectionArrow) sectionArrow.textContent = '▸'; if (emptyTag) { emptyTag.style.display = 'inline'; var hdrTitle = section ? section.querySelector('.ddm-section-title') : null; var hdrCount = section ? section.querySelector('.ddm-section-count') : null; if (hdrTitle) hdrTitle.style.display = 'none'; if (hdrCount) hdrCount.style.display = 'none'; } } else { if (emptyTag) emptyTag.style.display = 'none'; } for (var t = 1; t <= 3; t++) reportTier(t); updateSummaryCount(); // v4.9.0: 完成有结果 → Toast 通知 if (totalFound > 0 && !toastShown && opts.toastMsg) { toastShown = true; showToast(opts.toastMsg + ':找到 ' + totalFound + ' 个可用站点'); } for (var t in allResults) { if (!hasOwn(allResults, t)) continue; var arr = allResults[t]; arr.sort(function(a, b) { var ra = a.result || {}, rb = b.result || {}; if (ra.available && !rb.available) return -1; if (!ra.available && rb.available) return 1; if (ra.accessible && !rb.accessible) return -1; if (!ra.accessible && rb.accessible) return 1; return ra.time - rb.time; }); var cont = document.getElementById(opts.containerPrefix + '-t' + t + '-links'); if (cont) { for (var i = 0; i < arr.length; i++) cont.appendChild(arr[i].a); } } } function runTier(tier) { var sites = sitesGroups[tier] || []; var containerId = opts.containerPrefix + '-t' + tier + '-links'; var container = document.getElementById(containerId); if (!container || sites.length === 0) { if (tier < 3) runTier(tier + 1); else finalizeSection(); return; } container.innerHTML = ''; var tierResults = []; allResults[tier] = tierResults; var linkItems = []; for (var i = 0; i < sites.length; i++) { var site = sites[i]; var kw = site.useEn ? movie.kwEn : movie.kw; var url = kw ? (site.url + encodeURIComponent(kw) + (site.suffix || '')) : 'javascript:void(0)'; var a = document.createElement('a'); a.href = url; a.target = '_blank'; a.rel = 'noopener'; a.textContent = site.name; if (site.desc) a.title = site.desc; else if (!kw) a.title = site.name; a.className = 'checking'; // v4.9.0: 逐个弹入瀑布动画 (30ms 递增延迟) a.style.animation = 'ddm-link-pop .3s cubic-bezier(0.34,1.56,0.64,1) both'; a.style.animationDelay = (i * 30) + 'ms'; // v4.9.0: Tooltip 数据 if (site.desc) a.setAttribute('data-desc', site.desc); a.setAttribute('data-tier', TIER_CONFIG[site.tier || 2].label); (function(link) { link.addEventListener('mouseenter', function(ev) { showTooltipFor(link, ev); }, false); link.addEventListener('mouseleave', hideTooltip, false); link.addEventListener('click', hideTooltip, false); })(a); container.appendChild(a); linkItems.push({ site:site, a:a, kw:kw, result:null }); } // Tier3 国际站与普通站点同样自动检测,仅按速度顺位最后执行 var _token = container._ddmToken || (container._ddmToken = '_ddm_token_' + Math.random().toString(36).slice(2, 9)); var tierFound = 0; var st = tierState[tier]; if (st) { st.pending = linkItems.length; reportTier(tier); } pool(linkItems, 0, function(item, idx, workerDone) { setTimeout(function() { testSite(item.site, movie, function(r) { if (container._ddmToken !== _token) { workerDone(); return; } item.result = r; item.a.textContent = item.site.name; // v4.7.0: 记录响应时间用于动态并发调节 if (r.time > 0) updateConcurrency(r.time); if (r.available) { item.a.className = 'available'; item.a.title = item.site.name + ' ✅ 有资源 (' + r.time + 'ms)'; if (item.site.desc) item.a.title += ' - ' + item.site.desc; tierFound++; totalFound++; autoExpandSection(); if (opts.countEl) { opts.countEl.textContent = totalFound + '/' + totalSites; opts.countEl.className = 'ddm-section-count has-result'; } } else if (r.cachedSkip) { item.a.className = 'skipped'; item.a.title = item.site.name + ' ⏭ 24h内检测不可用,已跳过'; } else if (r.blocked) { item.a.className = 'blocked'; item.a.title = item.site.name + ' 🚧 被拦截 (CF挑战/验证码)'; } else if (r.accessible) { if (r.error) { item.a.className = 'error'; item.a.title = item.site.name + ' ⚠ 解析异常 (' + r.time + 'ms)'; } else { item.a.className = 'unavailable'; item.a.title = item.site.name + ' ❌ 无资源 (' + r.time + 'ms)'; } } else { item.a.className = 'inaccessible'; item.a.title = item.site.name + ' 🚫 无法访问'; } tierResults.push(item); if (st) { st.pending--; if (r.available) st.avail++; else item.a.classList.add('ddm-filtered'); reportTier(tier); } workerDone(); }); }, randomInt(20, 150)); }, function(finished, total) { if (container._ddmToken !== _token) return; var done = 0; for (var t = 1; t <= 3; t++) { if (t < tier) done += (sitesGroups[t]||[]).length; else if (t === tier) done += finished; } totalDone = done; updateProgress(); }, function() { if (container._ddmToken !== _token) return; totalDone = 0; for (var t = 1; t <= tier; t++) totalDone += (sitesGroups[t]||[]).length; updateProgress(); log('tier ' + tier + ' done, found=' + tierFound + ', section=' + sectionId); if (tier < 3) { setTimeout(function() { runTier(tier + 1); }, 80); } else { finalizeSection(); } }); } runTier(1); } // ============================================================ // v5.0.0 构建面板 — 三大类顺序:下载→网盘→在线 // ============================================================ function removePanel() { var p = document.getElementById('ddm-panel'); if (!p || !p.parentNode) return; var wrap = p.parentNode; if (wrap.id === 'ddm-float-wrap') wrap.parentNode.removeChild(wrap); else wrap.removeChild(p); } function resetCaches() { _xhrCache = {}; _requestLastTime = {}; } function buildPanel(movie) { log('buildPanel v'+VER); var target = document.querySelector('.aside'); if (!target) target = document.body; if (document.getElementById('ddm-panel')) return document.getElementById('ddm-panel'); var dlN = DOWNLOAD_SITES.length; var panN = PAN_SEARCH_SITES.length; var onlineN = ONLINE_SITES.length; var tDl = countTier(DOWNLOAD_SITES); var tPan = countTier(PAN_SEARCH_SITES); var tOnline = countTier(ONLINE_SITES); var t1Total = tPan[1] + tOnline[1] + tDl[1]; var t2Total = tPan[2] + tOnline[2] + tDl[2]; var t3Total = tPan[3] + tOnline[3] + tDl[3]; var totalAll = t1Total + t2Total + t3Total; function tierBlock(prefix, tier, count) { if (count === 0) return ''; return '
' + '' + TIER_CONFIG[tier].label + ' · ' + TIER_CONFIG[tier].desc + '' + '' + '
'; } var panel = document.createElement('div'); panel.id = 'ddm-panel'; panel.className = 'ddm-panel'; panel.innerHTML = '
' + '

⬇ ' + NAME + ' v' + VER + '

' + '' + '
' + '
' + '
0下载
' + '
0网盘直链
' + '
0网盘搜索
' + '
0在线
' + '
' + // v4.9.0: 实时过滤搜索框 '' + // 1. 下载资源(首位) '
' + '
' + '▸' + '🧲' + '下载资源' + '' + '0/' + dlN + '' + '
' + '
' + '
' + '0/' + dlN + '' + '
' + '' + '
' + // 2. 网盘资源(含API+搜索站) '
' + '
' + '▾' + '☁️' + '网盘资源' + '加载中' + '
' + '
搜索中
' + '
' + '
正在搜索网盘资源...
' + '
' + '
' + '▸' + '🔍' + '更多网盘搜索站' + '' + '0/' + panN + '' + '
' + '
' + '
' + '0/' + panN + '' + '
' + '' + '
' + // 3. 在线观看(末位) '
' + '
' + '▸' + '📺' + '在线观看' + '' + '0/' + onlineN + '' + '
' + '
' + '
' + '0/' + onlineN + '' + '
' + '' + '
' + '
' + '总计 ' + totalAll + ' 站点' + '快 ' + t1Total + '' + '中 ' + t2Total + '' + '慢 ' + t3Total + '' + '
'; try { if (target === document.body) { var fw = document.createElement('div'); fw.id = 'ddm-float-wrap'; fw.style.cssText = 'position:fixed;right:18px;top:78px;width:384px;max-height:86vh;overflow-y:auto;z-index:99998;background:transparent;border-radius:20px;'; fw.appendChild(panel); document.body.appendChild(fw); } else { target.insertBefore(panel, target.firstChild); } log('panel injected v'+VER); } catch(e) { logErr('panel inject failed', e); try { document.body.appendChild(panel); } catch(e2) {} } var headers = panel.querySelectorAll('.ddm-section-header[data-sec]'); for (var hi = 0; hi < headers.length; hi++) { headers[hi].addEventListener('click', function(e) { var key = this.getAttribute('data-sec'); var body = document.getElementById('ddm-' + key + '-body'); var arrow = this.querySelector('.ddm-arrow'); if (!body || !arrow) return; var hid = body.style.display === 'none'; body.style.display = hid ? 'block' : 'none'; arrow.textContent = hid ? '▾' : '▸'; // v4.9.0: 展开时内容区淡入动画 if (hid) { animateBodyIn(body); // v5.0.0: 展开后隐藏空结果提示并恢复标题/计数 var et = document.getElementById('ddm-' + key + '-empty'); if (et) et.style.display = 'none'; var ht = this.querySelector('.ddm-section-title'); if (ht) ht.style.display = ''; var hc = this.querySelector('.ddm-section-count'); if (hc) hc.style.display = ''; } }, false); } // v4.9.0: 实时过滤搜索框 var searchInput = document.getElementById('ddm-search'); if (searchInput) { searchInput.addEventListener('input', function() { var q = (this.value || '').trim().toLowerCase(); var links = panel.querySelectorAll('.ddm-links a'); for (var li = 0; li < links.length; li++) { var nm = (links[li].textContent || '').toLowerCase(); var show = !q || nm.indexOf(q) !== -1; if (show) links[li].classList.remove('ddm-search-hide'); else links[li].classList.add('ddm-search-hide'); } }, false); } var reloadBtn = document.getElementById('ddm-reload'); if (reloadBtn) { reloadBtn.addEventListener('click', function(e) { e.preventDefault(); removePanel(); resetCaches(); setTimeout(function(){ initAll(movie); }, 100); }, false); } return panel; } // ============================================================ // v4.7.0 DNS预解析 (Tier1 站点域名, 最多6个) // ============================================================ function addDnsPrefetch() { try { var head = document.head || document.documentElement; if (!head) return; var seen = {}, domains = []; var all = DOWNLOAD_SITES.concat(PAN_SEARCH_SITES, ONLINE_SITES); for (var i = 0; i < all.length && domains.length < 6; i++) { if (all[i].tier !== 1) continue; var host = (all[i].url || '').replace(/^https?:\/\//, '').split('/')[0]; if (!host || seen[host]) continue; seen[host] = 1; domains.push(host); var link = document.createElement('link'); link.rel = 'dns-prefetch'; link.href = '//' + host; head.appendChild(link); } if (domains.length > 0) log('dns-prefetch: ' + domains.join(', ')); } catch(e) { logErr('addDnsPrefetch error', e); } } // ============================================================ // 初始化 // ============================================================ function initAll(movie) { if (window._ddm_initialized) return; if (document.getElementById('ddm-panel')) return; window._ddm_initialized = true; try { addCSS(CSS); addDnsPrefetch(); tagSiteCategories(); var panel = buildPanel(movie); if (!panel) { window._ddm_initialized = false; return; } // 分区检测:三大类共用同一渲染流程(网盘直链 API 与「更多网盘搜索站」同属第二分区) function runSection(key, sites, toast, sectionId, bodyId, arrowId) { var pre = 'ddm-' + key; renderSectionTiered({ sitesGroups: groupByTier(sites), movie: movie, sectionId: sectionId || 'ddm-sec-' + key, containerPrefix: pre, bodyId: bodyId, arrowId: arrowId, summaryKey: key, toastMsg: toast, emptyElId: pre + '-empty', barEl: document.getElementById(pre + '-bar'), textEl: document.getElementById(pre + '-prog'), countEl: document.getElementById(pre + '-cnt') }); } runSection('dl', DOWNLOAD_SITES, '🧲 下载资源'); // 网盘直链 API(第二分区上半部) var panBody = document.getElementById('ddm-pan-body'); var panBadge = document.getElementById('ddm-pan-badge'); var panCnt = document.getElementById('ddm-pan-cnt'); var panSec = document.getElementById('ddm-sec-pan'); if (panBody) { searchPanApi(movie.kw, function(items) { if (!panBody) return; if (items && items.length > 0) { var html = '
'; for (var i = 0; i < items.length; i++) { var it = items[i]; html += ''; html += '' + esc(it.tagLabel) + ''; html += '' + esc(it.name) + ''; if (it.pwd) html += '🔑' + esc(it.pwd) + ''; html += ''; } html += '
'; panBody.innerHTML = html; if (panCnt) { panCnt.textContent = items.length + '条'; panCnt.className = 'ddm-section-count has-result'; } if (panBadge) { panBadge.className='ddm-progress-badge done'; panBadge.textContent='✓ 找到' + items.length + '条'; } if (panSec) { panSec.classList.add('ddm-section-completed'); panSec.classList.add('ddm-section-has-result'); } // v4.9.0: 网盘直链 Toast showToast('☁️ 网盘直链:找到 ' + items.length + ' 条资源'); var summaryEl = document.getElementById('ddm-summary-pan'); if (summaryEl) { summaryEl.textContent = items.length; summaryEl.className = 'ddm-summary-num has-val'; } var pwds = panBody.querySelectorAll('.ddm-pan-pwd'); for (var p = 0; p < pwds.length; p++) { pwds[p].addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); var pwd = this.getAttribute('data-pwd'); var ok = copyText(pwd); var old = this.innerHTML; var self = this; this.className = 'ddm-pan-pwd copied'; this.innerHTML = ok ? '✅已复制' : '失败'; setTimeout(function(){ self.className='ddm-pan-pwd'; self.innerHTML = old; }, 1500); }, false); } } else { panBody.innerHTML = '
未找到网盘资源,可查看下方搜索站
'; if (panBadge) { panBadge.className='ddm-progress-badge done-none'; panBadge.textContent='无结果'; } if (panSec) panSec.classList.add('ddm-section-completed'); if (panCnt) panCnt.textContent = '0条'; } }); } runSection('pansearch', PAN_SEARCH_SITES, '☁️ 网盘搜索', 'ddm-sec-pan', 'ddm-pansearch-body', 'ddm-pansearch-arrow'); runSection('online', ONLINE_SITES, '📺 在线观看'); // IMDb评分注入 if (movie.imdbId) { try { gmXhr({url:'https://www.imdb.com/title/'+movie.imdbId+'/',timeout:12000,retries:2,headers:{'Accept-Language':'en-US,en;q=0.9','Referer':'https://www.google.com/'}}, function(err, res) { if (err || !res || !res.responseText) return; try { var m = res.responseText.match(/"ratingValue"\s*:\s*"?(\d+\.?\d*)"?/); if (m) { var rating = m[1]; var sectl = document.querySelector('#interest_sectl'); if (sectl && !document.getElementById('ddm-imdb')) { var wrap = document.createElement('div'); wrap.id = 'ddm-imdb'; wrap.className = 'rating_wrap ddm-imdb'; wrap.innerHTML = '
' + esc(rating) + '星
查看IMDb页面'; try { sectl.appendChild(wrap); } catch(e){} } } } catch(e) { logErr('imdb parse error', e); } }); } catch(e) {} } var bm = document.getElementById('ddm-boot-mark'); if (bm && bm.parentNode) setTimeout(function(){ if(bm.parentNode)bm.parentNode.removeChild(bm); }, 800); log('initAll done v'+VER); } catch(e) { logErr('initAll error', e); window._ddm_initialized = false; } } // ============================================================ // 主入口 // ============================================================ function main() { log('main() v'+VER); try { var movie = getMovieInfo(); if (!movie.cnTitle) { var bm = document.getElementById('ddm-boot-mark'); if(bm)bm.innerHTML=NAME + ' - 非详情页?'; return; } initAll(movie); } catch(e) { logErr('main error', e); } } function startWhenReady() { if (document.body && document.querySelector('h1')) { if (document.querySelector('.aside') || document.readyState === 'complete') { setTimeout(main, 300); } else { setTimeout(startWhenReady, 200); } } else { setTimeout(startWhenReady, 200); } } try { if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('🔄 重新加载面板', function() { removePanel(); resetCaches(); window._ddm_initialized = false; main(); }); GM_registerMenuCommand('🧹 清空缓存', function() { resetCaches(); try { if (typeof GM_listValues !== 'undefined') { var ks=GM_listValues(); for (var i=0;i