// ==UserScript== // @name 豆瓣影视移动端适配(TV/Explore 适配 | 自动加载更多·修复版) // @namespace https://scriptcat.org/zh-CN/ // @version 5.8 // @description 修复自动加载失效问题;优化无数据时的闪屏;增强按钮查找兼容性。 // @match *://movie.douban.com/tv* // @match *://movie.douban.com/explore* // @match *://movie.douban.com/subject/* // @match *://movie.douban.com/ // @match *://m.douban.com/tv* // @match *://m.douban.com/movie* // @match *://search.douban.com/movie/subject_search* // @match *://www.douban.com/doubanapp/dispatch* // @grant none // @run-at document-start // @author 失辛向南 // @license MIT // ==/UserScript== (function() { 'use strict'; // ========== 调试开关(可改为 true 查看控制台日志) ========== const DEBUG = true; // 调试完成后可改回 false function log(...args) { if (DEBUG) console.log('[豆瓣适配]', ...args); } // ========== 最高优先级:doubanapp 直接跳转 ========== (function() { const url = window.location.href; const hostname = window.location.hostname; const pathname = window.location.pathname; if (hostname === 'www.douban.com' && pathname === '/doubanapp/dispatch') { try { const params = new URLSearchParams(window.location.search); const uri = params.get('uri'); if (uri && (uri.startsWith('/tv/') || uri.startsWith('/movie/'))) { const idMatch = uri.match(/\/(\d+)/); if (idMatch) { if (window.stop) window.stop(); document.documentElement.innerHTML = ''; const targetUrl = `https://m.douban.com/movie/subject/${idMatch[1]}/`; window.location.replace(targetUrl); return; } } } catch(e) { console.error(e); } } })(); // ========== 配置 ========== const CONFIG = { DESKTOP_UA: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", SUBJECT_REDIRECT: { enable: true, minWidthForDesktop: 769, originHost: "movie.douban.com", targetHost: "m.douban.com", pathPrefix: "/movie" }, M_SUBJECT_REDIRECT: { enable: true, minWidthForMobile: 769, originHost: "m.douban.com", targetHost: "movie.douban.com", pathPrefixRemove: "/movie" }, SEARCH_REDIRECT: { enable: true, originHost: "search.douban.com", originPath: "/movie/subject_search", targetHost: "m.douban.com", targetPath: "/search/", searchParam: "search_text", targetParam: "query" }, AUTO_LOAD: { enable: true, triggerDistance: 300, throttleDelay: 150, lockResetTime: 3000, retryBaseDelay: 800, maxRetryDelay: 5000, maxRetryCount: 6 // 最多重试6次后认为无数据 }, ADAPT_ENABLE: true, M_TO_MOVIE_REDIRECT: { enable: true, originHost: "m.douban.com", targetHost: "movie.douban.com", targetPath: "/tv", excludePaths: ["/subject/", "/celebrity/"], matchListPaths: ["/tv", "/movie"] } }; const currentLocation = window.location; const currentHostname = currentLocation.hostname; const currentPathname = currentLocation.pathname; const currentHref = currentLocation.href; const isMovieDoubanDomain = currentHostname === CONFIG.SUBJECT_REDIRECT.originHost; const isMDoubanDomain = currentHostname === CONFIG.M_SUBJECT_REDIRECT.originHost; const isTvPage = isMovieDoubanDomain && (currentPathname.startsWith('/tv') || currentPathname === '/tv'); const isExplorePage = isMovieDoubanDomain && (currentPathname.startsWith('/explore') || currentPathname === '/explore'); const isHomePage = isMovieDoubanDomain && (currentPathname === '/' || currentPathname === ''); const needAdaptPage = isTvPage || isExplorePage; if (isHomePage) return; const getViewportWidth = () => window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; // ========== 手机版详情页 -> 电脑版 ========== const isMSubjectPage = isMDoubanDomain && currentPathname.startsWith('/movie/subject/'); if (CONFIG.M_SUBJECT_REDIRECT.enable && isMSubjectPage) { const screenWidth = getViewportWidth(); if (screenWidth >= CONFIG.M_SUBJECT_REDIRECT.minWidthForMobile) { try { const newPathname = currentPathname.replace(CONFIG.M_SUBJECT_REDIRECT.pathPrefixRemove, ''); const targetUrl = new URL(currentHref); targetUrl.hostname = CONFIG.M_SUBJECT_REDIRECT.targetHost; targetUrl.pathname = newPathname; if (targetUrl.href !== currentHref) { log(`宽屏检测:手机版详情页跳转电脑版 ${targetUrl.href}`); window.location.replace(targetUrl.href); return; } } catch (e) { console.error('手机版详情页跳转电脑版失败:', e); } } } if (isTvPage && currentLocation.hash !== '#douban-desktop-adapt') { window.location.hash = 'douban-desktop-adapt'; } // UA 伪装 if (needAdaptPage) { const rewriteNavigatorProp = (prop, value) => { Object.defineProperty(navigator, prop, { get: () => value, configurable: true, enumerable: true }); }; rewriteNavigatorProp('userAgent', CONFIG.DESKTOP_UA); rewriteNavigatorProp('appVersion', CONFIG.DESKTOP_UA.replace('Mozilla/', '')); rewriteNavigatorProp('platform', 'Win64'); rewriteNavigatorProp('oscpu', 'Windows NT 10.0; Win64; Win64; x64'); Object.defineProperty(window.screen, 'width', { get: () => 1920, configurable: true }); Object.defineProperty(window.screen, 'height', { get: () => 1080, configurable: true }); Object.defineProperty(window.screen, 'availWidth', { get: () => 1920, configurable: true }); Object.defineProperty(window.screen, 'availHeight', { get: () => 1040, configurable: true }); } // viewport (function() { if (isTvPage || isExplorePage) { const meta = document.createElement('meta'); meta.name = 'viewport'; meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes'; if (document.head) document.head.appendChild(meta); else document.write(''); } })(); // 搜索页跳转 const isSearchPage = currentHostname === CONFIG.SEARCH_REDIRECT.originHost && currentPathname.startsWith(CONFIG.SEARCH_REDIRECT.originPath); if (CONFIG.SEARCH_REDIRECT.enable && isSearchPage) { try { const urlParams = new URLSearchParams(currentLocation.search); const searchKeyword = urlParams.get(CONFIG.SEARCH_REDIRECT.searchParam); if (searchKeyword) { const targetUrl = new URL(currentLocation.origin + CONFIG.SEARCH_REDIRECT.targetPath); targetUrl.hostname = CONFIG.SEARCH_REDIRECT.targetHost; targetUrl.searchParams.set(CONFIG.SEARCH_REDIRECT.targetParam, searchKeyword); if (targetUrl.href !== currentHref) window.location.replace(targetUrl.href); return; } } catch (e) { console.error(e); } } // 详情页跳转手机版 const isSubjectPage = isMovieDoubanDomain && currentPathname.startsWith('/subject/'); if (CONFIG.SUBJECT_REDIRECT.enable && isSubjectPage) { const screenWidth = getViewportWidth(); if (screenWidth < CONFIG.SUBJECT_REDIRECT.minWidthForDesktop) { try { const targetUrl = new URL(currentHref); targetUrl.hostname = CONFIG.SUBJECT_REDIRECT.targetHost; targetUrl.pathname = CONFIG.SUBJECT_REDIRECT.pathPrefix + targetUrl.pathname; if (targetUrl.href !== currentHref) window.location.replace(targetUrl.href); return; } catch (e) { console.error(e); } } } // m站列表页跳回电脑版 const isMDoubanListPage = isMDoubanDomain && !currentPathname.includes('/subject/') && !currentPathname.includes('/celebrity/') && (currentPathname.startsWith('/tv') || currentPathname.startsWith('/movie')); if (CONFIG.M_TO_MOVIE_REDIRECT.enable && isMDoubanListPage) { try { const targetUrl = new URL(currentHref); targetUrl.hostname = CONFIG.M_TO_MOVIE_REDIRECT.targetHost; targetUrl.pathname = CONFIG.M_TO_MOVIE_REDIRECT.targetPath; targetUrl.hash = 'douban-desktop-adapt'; if (targetUrl.href !== currentHref) window.location.replace(targetUrl.href); return; } catch (e) { console.error(e); } } // ========== 样式注入(与原版相同) ========== function injectAdaptStyle() { const style = document.createElement('style'); style.textContent = ` html, body, #wrapper, .grid-16-8, .article { overflow-x: hidden !important; max-width: 100% !important; } #db-global-nav, #db-nav-movie .nav-primary, #db-nav-movie .nav-secondary, .global-nav, .top-nav-info, .site-nav, .nav-search, .movieannual, .nav-logo, .nav-items, #footer, .aside, .extra, #recommend-groups, .dale_movie_tv_bottom_banner, #dale_movie_tv_bottom_banner, .grid-16-8 .aside, .grid-16-8 .extra { display: none !important; } .rating-range, .score-range, .filter-checkbox, .drc-checkbox, .explore-all-filter, .rating-range-container, [class*="rating-range"], [class*="score-range"] { display: none !important; } .explore-all-selectors, .explore-all-selectors-main, .selector-item, .explore-menu, .explore-menu-second, .tag-group-list, .base-selector, .tag-group { display: flex !important; flex-direction: row !important; flex-wrap: wrap !important; align-items: center !important; visibility: visible !important; position: relative !important; z-index: 20 !important; background: white !important; } .selector-item, .tag-group, .base-selector { margin-right: 15px !important; white-space: nowrap !important; } .tag-group .base-selector-main, .base-selector .base-selector-main { overflow: visible !important; position: absolute !important; background: white !important; z-index: 30 !important; } .grid-16-8 .article, #content .article { float: none !important; width: 100% !important; margin: 0 !important; padding: 0 12px !important; box-sizing: border-box; } #content > h1 { margin: 20px 0 10px 0; font-size: 24px; font-weight: 500; } .explore-menu { display: flex !important; margin-bottom: 20px; } .subject-list-list, .grid-view .items, .list-view .items { display: grid !important; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)) !important; gap: 20px 12px !important; margin: 20px 0 0 0 !important; padding: 0 !important; } @media (max-width: 560px) { .subject-list-list, .grid-view .items, .list-view .items { grid-template-columns: repeat(3, 1fr) !important; gap: 16px 8px !important; } } .subject-list-list li, .grid-view .item, .list-view .item { width: auto !important; margin: 0 !important; padding: 0 !important; list-style: none; } .drc-subject-card, .grid-view .item .pic, .list-view .item .pic { display: flex !important; flex-direction: column !important; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.08); height: 100%; } .drc-subject-card-cover, .grid-view .item .pic a, .list-view .item .pic a { width: 100% !important; margin-bottom: 8px; border-radius: 12px; overflow: hidden; background: #f0f0f0; display: block; } .drc-subject-card-cover img, .grid-view .item .pic img, .list-view .item .pic img { width: 100% !important; height: auto !important; aspect-ratio: 9 / 16 !important; object-fit: cover !important; display: block; } .drc-subject-card-main, .grid-view .item .info, .list-view .item .info { padding: 0 6px 10px 6px; text-align: left; } .drc-subject-info-title, .grid-view .item .title a, .list-view .item .title a { font-size: 14px !important; font-weight: 600 !important; line-height: 1.3 !important; margin: 0 0 6px 0 !important; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; } .drc-rating, .grid-view .item .rating, .list-view .item .rating { display: flex !important; align-items: center; gap: 4px; } .drc-rating-stars { display: inline-block; } .drc-rating-num, .grid-view .item .rating .rating_num, .list-view .item .rating .rating_num { font-size: 12px; font-weight: 500; color: #ffac2c; } .drc-rating.no-rating, .grid-view .item .rating:empty, .list-view .item .rating:empty { display: none !important; } .drc-subject-info-subtitle, .drc-subject-info-year, .drc-subject-info-info, .grid-view .item .year, .list-view .item .year, .grid-view .item .abstract, .list-view .item .abstract { display: none !important; } .subject-list-more { margin: 40px 0 30px; text-align: center; } .subject-list-more button, .subject-list-more .drc-button { border-radius: 40px; padding: 8px 32px; background-color: #f0f2f5; border: none; color: #1a1a1a; font-weight: 500; cursor: pointer; } `; document.head.appendChild(style); } function cleanPage() { const hideSelectors = ['.rating-range', '.score-range', '.filter-checkbox', '.drc-checkbox', '.explore-all-filter', '.rating-range-container', '[class*="rating-range"]', '[class*="score-range"]']; hideSelectors.forEach(sel => document.querySelectorAll(sel).forEach(el => el && (el.style.display = 'none'))); document.querySelectorAll('div, span, li').forEach(el => { const txt = el.innerText || ''; if ((txt.includes('评分区间') || txt.includes('未看过') || txt.includes('可播放')) && !el.querySelector('a, .selector-item, .tag-group, button')) el.style.display = 'none'; }); } // ========== 自动加载逻辑(修复版) ========== let isLoading = false; let retryTimer = null; let contentObserver = null; let lastContentHeight = 0; let retryCount = 0; let noMoreData = false; // 无更多数据标志 let lastLoadTime = 0; const LOAD_COOLDOWN = 2000; // 增强版按钮查找(兼容豆瓣实际类名) function getLoadMoreBtn() { if (noMoreData) return null; const isValidBtn = (el) => { if (!el) return false; if (el.disabled) return false; const rect = el.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; const text = (el.textContent || '').trim(); return text.includes('加载更多') || text.includes('Load more') || text.includes('更多'); }; // 豆瓣 TV/Explore 常见的加载更多按钮选择器 const selectors = [ '.subject-list-more button', // 最常见 '.subject-list-more .drc-button', '.subject-list-more a', '.more a', '.load-more a', '.list-more a', '.next a', '.load-more-btn', '.btn-more', '.more-btn', 'a.more', 'button.more', '[class*="load-more"]', '[class*="loadmore"]', '#load-more', '.load-more-button', '.more-link', '.next-page', '.paginator .next a' // 部分分页样式 ]; for (const selector of selectors) { try { const elements = document.querySelectorAll(selector); for (const el of elements) { if (isValidBtn(el)) { log(`找到加载按钮: ${selector}`, el); return el; } } } catch(e) {} } // XPath 查找包含“加载更多”文本的元素 try { const xpath = "//*[contains(text(),'加载更多') or contains(text(),'Load more')]"; const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let node = result.singleNodeValue; while (node) { if (isValidBtn(node)) return node; let parent = node.parentElement; while (parent && parent !== document.body) { if (isValidBtn(parent) || parent.tagName === 'A' || parent.tagName === 'BUTTON') { if (isValidBtn(parent)) return parent; } parent = parent.parentElement; } node = node.parentElement; } } catch(e) {} // 全局扫描所有 a / button const all = document.querySelectorAll('a, button, .more, .load-more, [role="button"]'); for (const el of all) { if (isValidBtn(el)) return el; } return null; } // 判断是否真正没有更多数据(不依赖按钮存在性,而是检查禁用状态或提示文本) function isReallyNoMoreData() { // 1. 检查是否存在“没有更多”的文本 const bodyText = document.body.innerText; if (bodyText.includes('没有更多') || bodyText.includes('已经到底') || bodyText.includes('No more') || bodyText.includes('End of list')) { log("检测到无更多文本"); return true; } // 2. 检查按钮是否存在且被禁用 const btn = getLoadMoreBtn(); if (btn && btn.disabled) { log("加载按钮被禁用"); return true; } // 3. 如果重试次数已达上限且仍然找不到按钮,认为无数据 if (retryCount >= CONFIG.AUTO_LOAD.maxRetryCount && !btn) { log(`重试次数已达上限(${retryCount}),且无按钮,认为无更多数据`); return true; } return false; } function getScrollInfo() { const docEl = document.documentElement; const scrollTop = window.pageYOffset || docEl.scrollTop || 0; const windowHeight = window.innerHeight || docEl.clientHeight || 0; const documentHeight = Math.max(docEl.scrollHeight, docEl.offsetHeight, document.body.scrollHeight); return { scrollTop, windowHeight, documentHeight }; } function resetLoadingLock() { if (isLoading) { log('重置加载锁'); isLoading = false; } if (window._loadLockTimer) { clearTimeout(window._loadLockTimer); window._loadLockTimer = null; } } function onContentChanged() { const currentHeight = getScrollInfo().documentHeight; if (currentHeight !== lastContentHeight) { lastContentHeight = currentHeight; log('内容高度变化,重置锁并重新检测'); resetLoadingLock(); // 如果新内容加载后出现了按钮,则重置无数据标志 if (noMoreData && getLoadMoreBtn()) { noMoreData = false; retryCount = 0; log("检测到新的加载按钮,重置无数据标志"); } setTimeout(() => handleAutoLoadMore(), 200); } } function setupContentMonitor() { if (contentObserver) contentObserver.disconnect(); contentObserver = new MutationObserver(() => onContentChanged()); contentObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); lastContentHeight = getScrollInfo().documentHeight; log('内容监听已启动'); } function handleAutoLoadMore() { if (!CONFIG.AUTO_LOAD.enable) return; if (isLoading) { log("正在加载中,跳过"); return; } if (noMoreData) { log("已无更多数据,停止自动加载"); return; } // 冷却检查 const now = Date.now(); if (now - lastLoadTime < LOAD_COOLDOWN) { log(`加载冷却中,距上次加载 ${now - lastLoadTime}ms`); return; } const { scrollTop, windowHeight, documentHeight } = getScrollInfo(); const distanceToBottom = documentHeight - (scrollTop + windowHeight); log(`滚动位置: scrollTop=${scrollTop}, winH=${windowHeight}, docH=${documentHeight}, distance=${distanceToBottom}`); if (distanceToBottom <= CONFIG.AUTO_LOAD.triggerDistance) { // 先检查是否真的无数据 if (isReallyNoMoreData()) { noMoreData = true; log("确认为无更多数据,停止自动加载"); return; } const loadBtn = getLoadMoreBtn(); if (loadBtn) { log('✅ 触发自动加载,点击加载更多按钮'); isLoading = true; lastLoadTime = now; loadBtn.click(); window._loadLockTimer = setTimeout(() => { log('后备定时器触发,强制重置加载锁'); resetLoadingLock(); }, CONFIG.AUTO_LOAD.lockResetTime); retryCount = 0; if (retryTimer) clearTimeout(retryTimer); } else { // 没有找到按钮,增加重试计数 if (retryCount < CONFIG.AUTO_LOAD.maxRetryCount) { const delay = Math.min(CONFIG.AUTO_LOAD.retryBaseDelay * Math.pow(1.5, retryCount), CONFIG.AUTO_LOAD.maxRetryDelay); retryCount++; log(`⚠️ 未找到加载按钮,第 ${retryCount}/${CONFIG.AUTO_LOAD.maxRetryCount} 次重试,${delay}ms 后重试`); if (retryTimer) clearTimeout(retryTimer); retryTimer = setTimeout(() => { if (!isLoading) handleAutoLoadMore(); }, delay); } else { // 重试次数用尽且没有按钮,标记为无更多数据 noMoreData = true; log("重试次数用尽且未找到按钮,停止自动加载"); } } } else { // 用户向上滚动,重置重试计数 if (retryCount > 0) { retryCount = 0; if (retryTimer) clearTimeout(retryTimer); log("滚动离开底部,重置重试计数"); } } } function throttle(fn, delay) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= delay) { lastTime = now; fn.apply(this, args); } }; } // ========== 初始化 ========== function initAfterDOMLoad() { if (needAdaptPage) { injectAdaptStyle(); cleanPage(); const observer = new MutationObserver(() => cleanPage()); observer.observe(document.body, { childList: true, subtree: true }); } if (CONFIG.AUTO_LOAD.enable && needAdaptPage) { setupContentMonitor(); window.addEventListener('scroll', throttle(handleAutoLoadMore, CONFIG.AUTO_LOAD.throttleDelay), { passive: true }); window.addEventListener('resize', throttle(handleAutoLoadMore, 150), { passive: true }); window.addEventListener('load', () => { setTimeout(handleAutoLoadMore, 500); }); setTimeout(handleAutoLoadMore, 300); // 兜底检查(但不再强制触发,避免闪屏) setInterval(() => { if (!isLoading && !noMoreData && needAdaptPage) { const { scrollTop, windowHeight, documentHeight } = getScrollInfo(); if (scrollTop + windowHeight >= documentHeight - CONFIG.AUTO_LOAD.triggerDistance) { handleAutoLoadMore(); } } }, 5000); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAfterDOMLoad); } else { initAfterDOMLoad(); } })();