// ==UserScript== // @name 豆瓣影视移动端适配 // @namespace https://scriptcat.org/zh-CN/ // @version 2.3 // @description 让豆瓣影视在移动端浏览更舒适:电视/探索页强制显示电脑版(避免跳转手机版),并注入移动端适配样式(网格布局、隐藏侧边栏);详情页自动跳转手机版方便阅读;电视页禁止左右滑动、滚动到底部时自动点击“加载更多”按钮(增强版按钮识别与锁机制);手机版影视列表自动跳回电脑版;首页隐藏右侧榜单,只留推荐内容;搜索自动转手机版;修复演员页被误跳转的问题。支持自动加载重试与内容变化监听,提升滚动加载稳定性。 // @author 失辛向南 // @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 // @license MIT // @supportURL https://scriptcat.org/zh-CN/script-show-page/5802/issue // ==/UserScript== (function() { 'use strict'; // ========== 调试开关(控制台可修改) ========== const DEBUG = false; // 改为 true 可输出详细日志 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); } } })(); let isLoading = false; let loadObserver = null; let retryTimer = null; let contentObserver = null; // 用于监听内容增加,重置锁 let lastContentHeight = 0; // 记录上次内容高度,检测新增 let retryCount = 0; const MAX_RETRY = 8; // 最大重试次数,避免无限轮询 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, originHost: "movie.douban.com", targetHost: "m.douban.com", pathPrefix: "/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: 200, // 缩小触发距离,更灵敏 throttleDelay: 80, // 减小节流延迟 lockResetTime: 3000, // 后备重置时间(3秒) retryBaseDelay: 800, // 重试基础延迟 maxRetryDelay: 5000 }, 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 isTvPage = isMovieDoubanDomain && (currentPathname.startsWith('/tv') || currentPathname === '/tv'); const isExplorePage = isMovieDoubanDomain && (currentPathname.startsWith('/explore') || currentPathname === '/explore'); const needAdaptPage = isTvPage || isExplorePage; if (isTvPage && currentLocation.hash !== '#douban-desktop-adapt') { window.location.hash = 'douban-desktop-adapt'; } 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 }); } // 搜索页跳转 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) { 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('subject页面跳转失败:', e); } } // m站列表页跳回电脑版 const isMDoubanDomain = currentHostname === CONFIG.M_TO_MOVIE_REDIRECT.originHost; const isExcludePage = CONFIG.M_TO_MOVIE_REDIRECT.excludePaths.some(p => currentPathname.includes(p)); const isMatchListPage = CONFIG.M_TO_MOVIE_REDIRECT.matchListPaths.some(path => currentPathname.startsWith(path) || currentPathname === path ); if (CONFIG.M_TO_MOVIE_REDIRECT.enable && isMDoubanDomain && isMatchListPage && !isExcludePage) { 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('m站列表页跳转失败:', e); } } // ========== 增强的加载按钮查找 ========== function getLoadMoreBtn() { // 检查元素是否有效(可见、未禁用、包含文本) const isValidBtn = (el) => { if (!el) return false; if (el.disabled) return false; // 检查可见性(offsetParent 为 null 不一定不可见,需综合判断) 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('更多'); }; // 1. 常用选择器列表(扩充) const selectors = [ '.more a', '.list-more a', '.load-more a', '.paginator .next a', '.next a', '.load-more-btn', '.btn-more', '.more-btn', 'a.more', 'button.more', '[class*="load-more"]', '[class*="loadmore"]', '[class*="load_more"]', '#load-more', '.load-more-button', '.more-link', '.next-page', '.pagination .next' ]; 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) {} } // 2. 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)) { log('通过XPath找到按钮:', node); return node; } // 向上查找可点击元素(a, button, 带click事件的元素) let parent = node.parentElement; while (parent && parent !== document.body) { if (isValidBtn(parent) || parent.tagName === 'A' || parent.tagName === 'BUTTON') { if (isValidBtn(parent)) { log('通过父元素找到按钮:', parent); return parent; } } parent = parent.parentElement; } node = node.parentElement; } } catch(e) {} // 3. 全局扫描所有 a/button,寻找文本匹配 const all = document.querySelectorAll('a, button, .more, .load-more, [role="button"]'); for (const el of all) { if (isValidBtn(el)) { log('全局扫描找到按钮:', el); return el; } } log('未找到加载按钮'); return null; } // 获取滚动信息(更精确) 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(); // 延迟一下,等待DOM稳定 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; } const { scrollTop, windowHeight, documentHeight } = getScrollInfo(); const distanceToBottom = documentHeight - (scrollTop + windowHeight); log(`滚动位置: scrollTop=${scrollTop}, winH=${windowHeight}, docH=${documentHeight}, 距底部=${distanceToBottom}`); if (distanceToBottom <= CONFIG.AUTO_LOAD.triggerDistance) { const loadBtn = getLoadMoreBtn(); if (loadBtn) { log('触发自动加载,点击按钮'); isLoading = true; loadBtn.click(); // 设置后备定时器:3秒后如果还没有解锁则强制重置 window._loadLockTimer = setTimeout(() => { log('后备定时器触发,强制重置加载锁'); resetLoadingLock(); }, CONFIG.AUTO_LOAD.lockResetTime); // 重置重试计数 retryCount = 0; if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; } } else { // 找不到按钮,采用指数退避重试 if (retryCount < MAX_RETRY) { const delay = Math.min(CONFIG.AUTO_LOAD.retryBaseDelay * Math.pow(1.5, retryCount), CONFIG.AUTO_LOAD.maxRetryDelay); retryCount++; log(`未找到加载按钮,${delay}ms 后重试 (${retryCount}/${MAX_RETRY})`); if (retryTimer) clearTimeout(retryTimer); retryTimer = setTimeout(() => { if (!isLoading) handleAutoLoadMore(); }, delay); } else { log('已达最大重试次数,停止自动加载尝试'); } } } else { // 未到底部,重置重试计数(因为用户可能继续滚动) if (retryCount > 0) { retryCount = 0; if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; } } } } // 节流函数 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 injectAdaptStyle() { const style = document.createElement('style'); style.type = 'text/css'; style.textContent = ` html, body { width: 100% !important; max-width: 100vw !important; overflow-x: hidden !important; overflow-y: auto !important; font-size: 14px !important; padding: 0 !important; margin: 0 !important; touch-action: pan-y !important; } * { box-sizing: border-box !important; max-width: 100vw !important; overflow-x: hidden !important; } @media screen and (max-width: 768px) { .container, .wrapper, #wrapper, .main, .content, #db-global-nav, .top-nav, .nav-container, .explore-container, .tv-list-container, .article, .grid-view, .list-view { width: 100% !important; max-width: 100% !important; min-width: unset !important; padding: 0 8px !important; margin: 0 auto !important; } .nav-items, .nav, .header-nav, .top-nav-items { flex-wrap: wrap !important; gap: 8px !important; justify-content: flex-start !important; } .filter, .filters, .search-filter, .filter-bar, .explore-filter, .tv-filter, .tags, .filter-tags { width: 100% !important; flex-wrap: wrap !important; gap: 6px !important; padding: 8px 0 !important; } .filter-item, .tag, .filter-tag, .tab-item { flex-shrink: 0 !important; font-size: 13px !important; padding: 4px 8px !important; white-space: nowrap !important; } .grid, .items-grid, .movie-list, .list-container, #explore-items, .explore-list, .grid-list, .tv-list, .list-view, .album-list { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)) !important; gap: 12px !important; width: 100% !important; padding: 0 !important; } .item, .card, .movie-item, .list-item, .tv-item, .subject-item { width: 100% !important; margin: 0 !important; } img, .cover, .poster { max-width: 100% !important; height: auto !important; } .aside, .sidebar, .ad, .global-sidebar, .right-col, .banner-ad { display: none !important; } .section, .module, .panel { padding: 10px 0 !important; } button, .btn, a, .tab-item, .filter-item { min-height: 44px !important; min-width: 44px !important; line-height: 44px !important; } } `; document.head.appendChild(style); } function setViewport() { let meta = document.querySelector('meta[name="viewport"]'); if (!meta) { meta = document.createElement('meta'); meta.name = 'viewport'; document.head.appendChild(meta); } meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; } // 创建列表观察器(原有,用于加载后重新触发,但已通过内容监听替代部分功能) function createListObserver(retryCount = 0) { if (!needAdaptPage || !CONFIG.AUTO_LOAD.enable) return; const listContainer = document.querySelector('.list-view, .grid-view, .tv-list, .explore-list, #content, .article'); if (listContainer && !loadObserver) { loadObserver = new MutationObserver(() => { setTimeout(() => { if (!isLoading) handleAutoLoadMore(); }, 200); }); loadObserver.observe(listContainer, { childList: true, subtree: true }); log('列表观察器已绑定'); } else if (!listContainer && retryCount < 5) { setTimeout(() => createListObserver(retryCount + 1), 1000); } } // 首页适配(保持原有功能) function adaptHomePage() { const isHomePage = currentHostname === 'movie.douban.com' && (currentPathname === '/' || currentPathname === ''); if (!isHomePage) return; const homeStyle = document.createElement('style'); homeStyle.textContent = ` .grid-8, .aside, .recommendations, .sidebar, .right-col, .rankings, .top250, .reviews, .news, .ad, .global-sidebar, .banner-ad { display: none !important; } .grid-16, .main, .content, .screening, .recommend, .feed { width: 100% !important; max-width: 100% !important; margin-right: 0 !important; float: none !important; } .grid-16-8, .container, .wrapper { width: 100% !important; max-width: 100% !important; padding: 0 !important; margin: 0 !important; display: block !important; } body, html { overflow-x: hidden !important; width: 100% !important; position: relative !important; } .items, .movie-list, .list, .grid, .screening-list { display: grid !important; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)) !important; gap: 12px !important; } .item, .movie-item, .list-item { width: 100% !important; margin: 0 !important; } img { max-width: 100% !important; height: auto !important; } `; document.head.appendChild(homeStyle); setViewport(); const observer = new MutationObserver(() => { const rightSide = document.querySelector('.grid-8, .aside, .recommendations, .sidebar'); if (rightSide && rightSide.style.display !== 'none') { rightSide.style.display = 'none'; } const leftMain = document.querySelector('.grid-16, .main, .content'); if (leftMain) { leftMain.style.width = '100%'; leftMain.style.maxWidth = '100%'; } }); observer.observe(document.body, { childList: true, subtree: true }); } // 初始化 function initAfterDOMLoad() { adaptHomePage(); if (!needAdaptPage) return; if (CONFIG.ADAPT_ENABLE) { setViewport(); setTimeout(() => injectAdaptStyle(), 0); } if (CONFIG.AUTO_LOAD.enable) { // 启动内容监听(用于解锁) 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, 300); createListObserver(); }); // 动态内容可能提前出现 setTimeout(handleAutoLoadMore, 200); // 额外保险:每隔5秒检查一次(但仅在非加载状态且未到底部时轻量检测) setInterval(() => { if (!isLoading) { 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(); } })();