// ==UserScript== // @name 豆瓣影视筛选大师 // @namespace doveboy_js // @version 2.7 // @description 恢复豆瓣“选电影”和“选剧集”的经典分类导航、排序、分页,包含类别筛选和国家及地区筛选 // @author 白鸽男孩 // @license Zlib/Libpng License // @icon data:image/png;base64,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 // @match https://movie.douban.com/explore* // @match https://movie.douban.com/tv* // @icon https://img1.doubanio.com/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // @connect movie.douban.com // @run-at document-start // ==/UserScript== (function () { 'use strict'; /* * ------------------------------------------------------------------ * 基础配置 * ------------------------------------------------------------------ */ const API_NEW_URL = 'https://movie.douban.com/j/new_search_subjects'; const API_TAG_URL = 'https://movie.douban.com/j/search_subjects'; const API_CHART_URL = 'https://movie.douban.com/j/chart/top_list'; const SETTINGS_KEY = 'douban-classic-settings-v1'; const DB_NAME = 'douban-classic-nav-v2'; const DB_VERSION = 1; const PAGE_SIZE = 20; // 列表接口缓存时间:5 分钟。 const LIST_CACHE_MAX_AGE = 5 * 60 * 1000; const NEW_SORT_MAP = { recommend: 'T', time: 'R', rank: 'S' }; // 电影 /j/chart/top_list 的类型 ID 映射。 const CHART_GENRE_IDS = { 剧情: 11, 喜剧: 24, 动作: 5, 爱情: 13, 科幻: 17, 动画: 25, 悬疑: 10, 惊悚: 19, 恐怖: 20, 犯罪: 3, 同性: 26, 音乐: 14, 歌舞: 7, 传记: 2, 历史: 4, 战争: 22, 西部: 27, 奇幻: 16, 冒险: 15, 灾难: 12, 武侠: 29, 情色: 6, 纪录片: 1, 短片: 23 }; const MOVIE_GENRES = [ '剧情', '喜剧', '动作', '爱情', '科幻', '动画', '悬疑', '惊悚', '恐怖', '犯罪', '同性', '音乐', '歌舞', '传记', '历史', '战争', '西部', '奇幻', '冒险', '灾难', '武侠', '情色', '纪录片', '短片' ]; const TV_GENRES = [ '剧情', '喜剧', '动作', '爱情', '科幻', '动画', '悬疑', '惊悚', '恐怖', '犯罪', '同性', '音乐', '歌舞', '传记', '历史', '战争', '奇幻', '冒险', '古装', '武侠', '家庭', '儿童', '纪录片', '真人秀' ]; const REGIONS = [ '中国大陆', '中国香港', '中国台湾', '美国', '日本', '韩国', '英国', '法国', '德国', '意大利', '西班牙', '印度', '泰国', '俄罗斯', '加拿大', '澳大利亚', '爱尔兰', '瑞典', '巴西', '丹麦' ]; const SORTS = [ { value: 'recommend', label: '按推荐排序' }, { value: 'time', label: '按时间排序' }, { value: 'rank', label: '按评分排序' } ]; const state = { mediaType: isTvPage() ? 'tv' : 'movie', genre: '', region: '', sort: 'time', minRate: 0, playableOnly: false, view: 'grid', page: 1, loading: false, abortToken: 0, candidates: [], filteredItems: [], reachedEnd: false, fetchedCandidateCount: 0, fastModePage: false, dataSource: 'new', fallbackMessage: '' }; let root = null; let statusElement = null; let resultElement = null; let paginationElement = null; let clickHandler = null; let changeHandler = null; let popstateHandler = null; const storage = createStorage(); /* * ------------------------------------------------------------------ * 存储层:IndexedDB 为主,GM API 为降级 * ------------------------------------------------------------------ */ function createStorage() { const idb = window.indexedDB || (typeof unsafeWindow !== 'undefined' && unsafeWindow.indexedDB); if (idb) { try { return createIndexedDBStorage(idb); } catch (error) { console.warn('[豆瓣经典导航] IndexedDB 初始化失败,回退到 GM 存储', error); } } return createGMStorage(); } function createIndexedDBStorage(idb) { let dbPromise = null; function open() { if (dbPromise) { return dbPromise; } dbPromise = new Promise((resolve, reject) => { const request = idb.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('lists')) { db.createObjectStore('lists', { keyPath: 'key' }); } if (!db.objectStoreNames.contains('meta')) { db.createObjectStore('meta', { keyPath: 'key' }); } }; request.onsuccess = (event) => { resolve(event.target.result); }; request.onerror = (event) => { dbPromise = null; reject(event.target.error); }; request.onblocked = () => { dbPromise = null; reject(new Error('IndexedDB blocked')); }; }); return dbPromise; } function get(store, key) { return open().then((db) => new Promise((resolve, reject) => { const tx = db.transaction([store], 'readonly'); const obj = tx.objectStore(store); const req = obj.get(key); req.onsuccess = () => resolve(req.result || null); req.onerror = () => reject(req.error); })); } function put(store, value) { return open().then((db) => new Promise((resolve, reject) => { const tx = db.transaction([store], 'readwrite'); const obj = tx.objectStore(store); const req = obj.put(value); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); req.onerror = () => reject(req.error); })); } function clearStore(store) { return open().then((db) => new Promise((resolve, reject) => { const tx = db.transaction([store], 'readwrite'); const obj = tx.objectStore(store); const req = obj.clear(); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); req.onerror = () => reject(req.error); })); } return { getList: (key) => get('lists', key).then((result) => { return result && result.data ? { data: result.data, savedAt: result.savedAt } : null; }), setList: (key, data) => put('lists', { key, data, savedAt: Date.now() }), getMeta: (key) => get('meta', key), setMeta: (key, value) => put('meta', { key, value }), clearLists: async () => { await clearStore('lists'); } }; } function createGMStorage() { const listsKey = 'douban-classic-lists-v1'; let listsCache = null; function getLists() { if (listsCache === null) { listsCache = gmGetValue(listsKey, {}); } return listsCache; } function saveLists() { gmSetValue(listsKey, listsCache); } return { getList: async (key) => { const lists = getLists(); const value = lists[key]; if (!value || !value.data) { return null; } if (Date.now() - value.savedAt > LIST_CACHE_MAX_AGE) { delete lists[key]; saveLists(); return null; } return { data: value.data, savedAt: value.savedAt }; }, setList: async (key, data) => { getLists(); listsCache[key] = { savedAt: Date.now(), data }; saveLists(); }, getMeta: async (key) => gmGetValue(`douban-classic-meta:${key}`, null), setMeta: async (key, value) => gmSetValue(`douban-classic-meta:${key}`, value), clearLists: async () => { listsCache = {}; gmDeleteValue(listsKey); } }; } async function getListCache(key) { const result = await storage.getList(key); if (!result) { return null; } if (Date.now() - result.savedAt > LIST_CACHE_MAX_AGE) { return null; } return result.data; } async function setListCache(key, data) { await storage.setList(key, data); } /* * ------------------------------------------------------------------ * GM API 兼容封装 * ------------------------------------------------------------------ */ function gmGetValue(key, fallback) { try { if (typeof GM_getValue === 'function') { return GM_getValue(key, fallback); } } catch (error) { console.warn('[豆瓣经典导航] 读取存储失败:', error); } try { const raw = localStorage.getItem(key); return raw === null ? fallback : JSON.parse(raw); } catch { return fallback; } } function gmSetValue(key, value) { try { if (typeof GM_setValue === 'function') { GM_setValue(key, value); return; } } catch (error) { console.warn('[豆瓣经典导航] 写入存储失败:', error); } try { localStorage.setItem(key, JSON.stringify(value)); } catch { // 忽略存储错误。 } } function gmDeleteValue(key) { try { if (typeof GM_deleteValue === 'function') { GM_deleteValue(key); return; } } catch (error) { console.warn('[豆瓣经典导航] 删除存储失败:', error); } try { localStorage.removeItem(key); } catch { // 忽略存储错误。 } } function gmRequest(options) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== 'function') { fetch(options.url, { method: options.method || 'GET', credentials: 'include', headers: options.headers || {} }) .then(async (response) => { const text = await response.text(); resolve({ status: response.status, responseText: text, finalUrl: response.url }); }) .catch(reject); return; } GM_xmlhttpRequest({ method: options.method || 'GET', url: options.url, headers: options.headers || {}, timeout: options.timeout || 20000, responseType: options.responseType || 'text', onload(response) { resolve(response); }, onerror() { reject(new Error('网络请求失败')); }, ontimeout() { reject(new Error('网络请求超时')); } }); }); } /* * ------------------------------------------------------------------ * 页面识别与初始化 * ------------------------------------------------------------------ */ function isTvPage() { return /^\/tv(?:\/|$)/.test(location.pathname); } function waitForApp() { let scheduled = false; const restoreApplication = () => { scheduled = false; const app = document.querySelector('#app'); if (!app) { return; } const hasClassicNavigation = app.querySelector('.dcn-app'); if (hasClassicNavigation) { root = app; return; } if (app.dataset.doubanClassicInitialized === '1') { app.dataset.doubanClassicInitialized = ''; } initialize(app); }; const scheduleRestore = () => { if (scheduled) { return; } scheduled = true; requestAnimationFrame(() => { requestAnimationFrame(restoreApplication); }); }; const observer = new MutationObserver(() => { const app = document.querySelector('#app'); if (!app || !app.querySelector('.dcn-app')) { scheduleRestore(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); scheduleRestore(); } function initialize(app) { if (app.dataset.doubanClassicInitialized === '1') { return; } app.dataset.doubanClassicInitialized = '1'; root = app; injectStyles(); loadInitialState(); renderApplication(); bindEvents(); updateDocumentTitle(); runSearch(); if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('清除豆瓣经典导航缓存', clearAllCache); GM_registerMenuCommand('恢复默认筛选设置', resetAllSettings); } if (popstateHandler) { window.removeEventListener('popstate', popstateHandler); } popstateHandler = () => { loadStateFromUrl(); renderControls(); runSearch(); }; window.addEventListener('popstate', popstateHandler); } function loadInitialState() { const settings = gmGetValue(SETTINGS_KEY, {}); if (settings.view === 'list' || settings.view === 'grid') { state.view = settings.view; } if (Number.isFinite(Number(settings.minRate))) { state.minRate = Number(settings.minRate); } loadStateFromUrl(); } function loadStateFromUrl() { const params = new URLSearchParams(location.search); state.genre = params.get('genre') || ''; state.region = params.get('region') || ''; state.sort = validSort(params.get('sort')) ? params.get('sort') : 'time'; state.minRate = normalizeRate(params.get('min_rate'), state.minRate); state.playableOnly = params.get('playable') === '1'; state.page = Math.max(1, Number.parseInt(params.get('page'), 10) || 1); const urlView = params.get('view'); if (urlView === 'grid' || urlView === 'list') { state.view = urlView; } } function normalizeRate(value, fallback = 0) { if (value === null || value === undefined || value === '') { return fallback; } const number = Number(value); if (!Number.isFinite(number)) { return fallback; } return Math.min(10, Math.max(0, number)); } function validSort(sort) { return SORTS.some((item) => item.value === sort); } /* * ------------------------------------------------------------------ * 页面结构 * ------------------------------------------------------------------ */ function renderApplication() { root.innerHTML = `
`; statusElement = root.querySelector('#dcn-status'); resultElement = root.querySelector('#dcn-results'); paginationElement = root.querySelector('#dcn-pagination'); renderControls(); } function renderControls() { const container = root.querySelector('#dcn-controls'); if (!container) { return; } const genres = state.mediaType === 'tv' ? TV_GENRES : MOVIE_GENRES; container.innerHTML = `
${renderFilterRow( '类型', 'genre', genres.map((value) => ({ value, label: value })), state.genre )} ${renderFilterRow( '地区', 'region', REGIONS.map((value) => ({ value, label: value })), state.region )}
`; updateToolbar(); updateSelectedSummary(); } function renderFilterRow(label, key, options, selected) { return `
${escapeHtml(label)}:
${options.map((option) => ` `).join('')}
`; } function updateToolbar() { const sort = root.querySelector('#dcn-sort'); const rate = root.querySelector('#dcn-min-rate'); const playable = root.querySelector('#dcn-playable'); const gridButton = root.querySelector('#dcn-grid-view'); const listButton = root.querySelector('#dcn-list-view'); if (sort) sort.value = state.sort; if (rate) rate.value = String(state.minRate); if (playable) playable.checked = state.playableOnly; gridButton?.classList.toggle('active', state.view === 'grid'); listButton?.classList.toggle('active', state.view === 'list'); } function updateSelectedSummary() { const container = root.querySelector('#dcn-selected'); if (!container) { return; } const selected = []; if (state.genre) selected.push(['genre', '类型', state.genre]); if (state.region) selected.push(['region', '地区', state.region]); if (state.minRate > 0) { selected.push(['minRate', '评分', `${state.minRate} 分以上`]); } if (state.playableOnly) { selected.push(['playableOnly', '播放', '可播放']); } if (!selected.length) { container.innerHTML = ` 当前条件: 全部${state.mediaType === 'tv' ? '剧集' : '电影'} `; return; } container.innerHTML = ` 当前条件: ${selected.map(([key, name, value]) => ` `).join('')} `; } /* * ------------------------------------------------------------------ * 事件 * ------------------------------------------------------------------ */ function bindEvents() { if (clickHandler) { root.removeEventListener('click', clickHandler); } if (changeHandler) { root.removeEventListener('change', changeHandler); } clickHandler = (event) => { const filter = event.target.closest('[data-filter]'); if (filter) { const key = filter.dataset.filter; const value = filter.dataset.value || ''; state[key] = value; state.page = 1; renderControls(); saveUrl(); runSearch(); return; } const clear = event.target.closest('[data-clear]'); if (clear) { clearStateValue(clear.dataset.clear); state.page = 1; renderControls(); saveUrl(); runSearch(); return; } if (event.target.closest('#dcn-clear-all')) { clearFilters(); renderControls(); saveUrl(); runSearch(); return; } if (event.target.closest('.dcn-relax-button')) { state.region = ''; state.minRate = 0; state.playableOnly = false; state.page = 1; saveSettings(); saveUrl(); renderControls(); applyLocalFiltersAndRender(); return; } if (event.target.closest('#dcn-grid-view')) { setView('grid'); return; } if (event.target.closest('#dcn-list-view')) { setView('list'); return; } if (event.target.closest('#dcn-refresh')) { runSearch(true); return; } const pageButton = event.target.closest('[data-page]'); if (pageButton && !pageButton.disabled && !state.loading) { const page = Number(pageButton.dataset.page); if (Number.isFinite(page) && page >= 1) { state.page = page; saveUrl(); if (state.fastModePage) { runSearch(); } else { renderResults(); } scrollToResults(); } } }; changeHandler = (event) => { if (event.target.id === 'dcn-sort') { state.sort = validSort(event.target.value) ? event.target.value : 'recommend'; state.page = 1; saveUrl(); runSearch(); return; } if (event.target.id === 'dcn-min-rate') { state.minRate = normalizeRate(event.target.value, 0); state.page = 1; saveSettings(); saveUrl(); runSearch(); return; } if (event.target.id === 'dcn-playable') { state.playableOnly = event.target.checked; state.page = 1; saveUrl(); applyLocalFiltersAndRender(); } }; root.addEventListener('click', clickHandler); root.addEventListener('change', changeHandler); } function clearStateValue(key) { if (key === 'minRate') { state.minRate = 0; saveSettings(); return; } if (key === 'playableOnly') { state.playableOnly = false; return; } if (Object.prototype.hasOwnProperty.call(state, key)) { state[key] = ''; } } function clearFilters() { state.genre = ''; state.region = ''; state.minRate = 0; state.playableOnly = false; state.page = 1; saveSettings(); } function setView(view) { if (view !== 'grid' && view !== 'list') { return; } state.view = view; saveSettings(); saveUrl(); updateToolbar(); renderResults(); } function saveSettings() { gmSetValue(SETTINGS_KEY, { view: state.view, minRate: state.minRate }); } function saveUrl() { const params = new URLSearchParams(); if (state.genre) params.set('genre', state.genre); if (state.region) params.set('region', state.region); if (state.sort !== 'time') params.set('sort', state.sort); if (state.minRate > 0) params.set('min_rate', String(state.minRate)); if (state.playableOnly) params.set('playable', '1'); if (state.view !== 'grid') params.set('view', state.view); if (state.page > 1) params.set('page', String(state.page)); const query = params.toString(); const newUrl = `${location.pathname}${query ? `?${query}` : ''}`; history.replaceState(null, '', newUrl); updateDocumentTitle(); } function updateDocumentTitle() { const parts = []; if (state.genre) parts.push(state.genre); if (state.region) parts.push(state.region); const mediaName = state.mediaType === 'tv' ? '选剧集' : '选电影'; document.title = parts.length ? `${parts.join(' · ')} - ${mediaName}` : mediaName; } /* * ------------------------------------------------------------------ * 搜索 * ------------------------------------------------------------------ */ async function runSearch(forceRefresh = false) { if (state.loading) { state.abortToken++; } const token = ++state.abortToken; state.loading = true; state.page = Math.max(1, state.page); state.candidates = []; state.filteredItems = []; state.reachedEnd = false; state.fetchedCandidateCount = 0; state.fastModePage = false; state.dataSource = 'new'; state.fallbackMessage = ''; showLoadingSkeleton(); try { const result = await fetchCandidates(token, forceRefresh); if (token !== state.abortToken) { return; } state.candidates = result.items; state.filteredItems = result.items; state.fetchedCandidateCount = result.items.length; state.reachedEnd = result.reachedEnd; state.fastModePage = true; state.dataSource = result.source; state.fallbackMessage = result.fallbackMessage; applyLocalFiltersAndRender(); if (!state.filteredItems.length) { showStatus( '没有找到符合当前条件的条目。可以减少筛选条件后重试。', 'empty' ); } } catch (error) { if (token !== state.abortToken) { return; } console.error('[豆瓣经典导航] 搜索失败:', error); showError( error?.message || '请求失败,可能遇到豆瓣访问限制,请稍后重试。' ); } finally { if (token === state.abortToken) { state.loading = false; } } } function shouldUseChart() { return ( state.mediaType === 'movie' && state.genre && !state.region && state.sort === 'rank' && CHART_GENRE_IDS[state.genre] ); } async function fetchCandidates(token, forceRefresh = false) { const start = Math.max(0, (state.page - 1) * PAGE_SIZE); // 1) 排行榜优先 if (shouldUseChart()) { showStatus(`正在读取“${state.genre}”排行榜……`, 'loading'); const data = await fetchChartPage(start, PAGE_SIZE, forceRefresh, token); checkToken(token); const items = normalizeChartSubjects(Array.isArray(data) ? data : []); if (items.length) { return { items, source: 'chart', reachedEnd: items.length < PAGE_SIZE, fallbackMessage: '' }; } } const label = [state.genre, state.region] .filter(Boolean) .join('·') || (state.mediaType === 'tv' ? '电视剧' : '电影'); // 2) 新接口全条件 showStatus(`正在读取“${label}”列表……`, 'loading'); const newData = await fetchNewSearchPage(start, { forceRefresh, token }); checkToken(token); const newItems = normalizeNewSearchSubjects(newData); if (newItems.length) { return { items: newItems, source: 'new', reachedEnd: (newData?.data?.length || 0) < PAGE_SIZE, fallbackMessage: '' }; } // 3) 去掉地区重试 if (state.region) { showStatus( `“${state.genre || (state.mediaType === 'tv' ? '剧集' : '电影')}”在“${state.region}”暂无数据,尝试扩大地区范围……`, 'loading' ); const noRegionData = await fetchNewSearchPage(start, { includeRegion: false, forceRefresh, token }); checkToken(token); const noRegionItems = normalizeNewSearchSubjects(noRegionData); if (noRegionItems.length) { return { items: noRegionItems, source: 'new', reachedEnd: (noRegionData?.data?.length || 0) < PAGE_SIZE, fallbackMessage: `未在“${state.region}”找到结果,已扩大至全部地区搜索。` }; } } // 4) 标签接口兜底 const tag = state.genre || (state.mediaType === 'tv' ? '电视剧' : '电影'); showStatus(`尝试通过标签“${tag}”查找……`, 'loading'); const tagData = await fetchTagSearchPage(start, tag, forceRefresh, token); checkToken(token); const tagItems = normalizeTagSearchSubjects(tagData); if (tagItems.length) { return { items: tagItems, source: 'tag', reachedEnd: (tagData?.subjects?.length || 0) < PAGE_SIZE, fallbackMessage: '' }; } // 5) 排行榜兜底 if (state.mediaType === 'movie' && state.genre && CHART_GENRE_IDS[state.genre]) { showStatus(`尝试读取“${state.genre}”排行榜……`, 'loading'); const chartData = await fetchChartPage(start, PAGE_SIZE, forceRefresh, token); checkToken(token); const chartItems = normalizeChartSubjects(Array.isArray(chartData) ? chartData : []); if (chartItems.length) { return { items: chartItems, source: 'chart', reachedEnd: chartItems.length < PAGE_SIZE, fallbackMessage: `已使用“${state.genre}”排行榜数据。` }; } } return { items: [], source: 'new', reachedEnd: true, fallbackMessage: '' }; } async function fetchJson(url, options = {}) { const { useCache = true, cacheKey = url, token, cacheWhen } = options; if (useCache) { const cached = await getListCache(cacheKey); if (cached) { return cached; } } const response = await gmRequest({ url, headers: { Accept: 'application/json, text/plain, */*', Referer: location.href }, timeout: 20000 }); if (response.status !== 200) { throw new Error(`接口返回 HTTP ${response.status}`); } let data; try { data = JSON.parse(response.responseText); } catch { if (looksLikeVerificationPage(response.responseText)) { throw new Error( '豆瓣返回了验证页面。请先正常浏览豆瓣,稍后再刷新。' ); } throw new Error('接口返回了无法识别的数据。'); } // 不缓存空结果,避免“刷新仍然空白”。 if (useCache && (!token || token === state.abortToken) && (!cacheWhen || cacheWhen(data))) { await setListCache(cacheKey, data); } return data; } async function fetchNewSearchPage(start, { includeRegion = true, forceRefresh = false, token } = {}) { const params = { tags: state.mediaType === 'tv' ? '电视剧' : '电影', sort: NEW_SORT_MAP[state.sort] || 'T', range: `${state.minRate},10`, start: String(start) }; if (state.genre) { params.genres = state.genre; } if (includeRegion && state.region) { params.countries = state.region; } const url = `${API_NEW_URL}?${new URLSearchParams(params).toString()}`; return fetchJson(url, { useCache: !forceRefresh, token, source: 'new' }); } async function fetchTagSearchPage(start, tag, forceRefresh = false, token) { const params = { type: state.mediaType, tag, sort: state.sort, page_limit: String(PAGE_SIZE), page_start: String(start) }; const url = `${API_TAG_URL}?${new URLSearchParams(params).toString()}`; return fetchJson(url, { useCache: !forceRefresh, token, cacheWhen: (d) => d && Array.isArray(d.subjects) && d.subjects.length > 0 }); } async function fetchChartPage(start, limit, forceRefresh = false, token) { const typeId = CHART_GENRE_IDS[state.genre]; const params = { type: String(typeId), interval_id: '100:90', action: '', start: String(start), limit: String(limit) }; const url = `${API_CHART_URL}?${new URLSearchParams(params).toString()}`; return fetchJson(url, { useCache: !forceRefresh, token, cacheWhen: (d) => Array.isArray(d) && d.length > 0 }); } function normalizeNewSearchSubjects(data) { const items = (data && data.data) || []; return items.map((item) => ({ id: String(item.id || ''), title: item.title || '未知标题', url: item.url || ( item.id ? `https://movie.douban.com/subject/${item.id}/` : '#' ), cover: item.cover || '', rate: normalizeSubjectRate(item.rate), playable: item.playable === undefined && item.is_playable === undefined ? undefined : Boolean(item.playable || item.is_playable), isNew: Boolean(item.is_new), episodesInfo: item.episodes_info || '', year: item.year || '', genres: item.genres || [], regions: item.regions || [], directors: item.directors || [], actors: item.casts || [], releaseDates: [], summary: '', detailLoaded: false, source: 'new' })); } function normalizeTagSearchSubjects(data) { const items = (data && data.subjects) || []; return items.map((item) => ({ id: String(item.id || ''), title: item.title || '未知标题', url: item.url || ( item.id ? `https://movie.douban.com/subject/${item.id}/` : '#' ), cover: item.cover || '', rate: normalizeSubjectRate(item.rate), playable: item.playable === undefined ? undefined : Boolean(item.playable), isNew: Boolean(item.is_new), episodesInfo: item.episodes_info || '', year: '', genres: [], regions: [], directors: [], actors: [], releaseDates: [], summary: '', detailLoaded: false, source: 'tag' })); } function normalizeChartSubjects(list) { return list.map((item) => ({ id: String(item.id || ''), title: item.title || '未知标题', url: item.url || ( item.id ? `https://movie.douban.com/subject/${item.id}/` : '#' ), cover: item.cover_url || '', rate: normalizeSubjectRate(item.score), playable: Boolean(item.is_playable), isNew: false, episodesInfo: '', year: extractYear(item.release_date), genres: item.types || [], regions: item.regions || [], directors: [], actors: item.actors || [], releaseDates: item.release_date ? [item.release_date] : [], summary: '', detailLoaded: true, source: 'chart' })); } function extractYear(dateString) { const match = String(dateString || '').match(/(\d{4})/); return match ? match[1] : ''; } function normalizeSubjectRate(rate) { const number = Number(rate); return Number.isFinite(number) && number > 0 ? number : 0; } /* * ------------------------------------------------------------------ * 缓存清理 * ------------------------------------------------------------------ */ async function clearAllCache() { try { await storage.clearLists(); alert('豆瓣经典导航的列表缓存已清除。'); } catch (error) { console.error('[豆瓣经典导航] 清除缓存失败', error); alert('清除缓存失败:' + error.message); } } function resetAllSettings() { gmDeleteValue(SETTINGS_KEY); const cleanPath = state.mediaType === 'tv' ? '/tv/' : '/explore'; location.href = cleanPath; } /* * ------------------------------------------------------------------ * 本地筛选与排序 * ------------------------------------------------------------------ */ function applyLocalFiltersAndRender(showSummary = true) { let items = state.candidates.filter(matchesFilters); // new 与 tag 接口本身已按用户选择的排序返回,不再客户端重排。 const serverSorted = state.dataSource === 'new' || state.dataSource === 'tag'; if (!serverSorted) { if (state.sort === 'rank') { items = stableSort(items, (left, right) => { return right.rate - left.rate; }); } if (state.sort === 'time') { items = stableSort(items, (left, right) => { return Number(right.year || 0) - Number(left.year || 0); }); } } state.filteredItems = items; renderResults(); if (showSummary) { updateResultStatus(); } updateSelectedSummary(); } function matchesFilters(item) { if (state.minRate > 0 && item.rate < state.minRate) { return false; } // 仅当接口明确返回不可播放时才过滤;未返回 playable 时不做假设。 if (state.playableOnly && item.playable === false) { return false; } if (state.genre && item.genres && item.genres.length) { if (!item.genres.some((genre) => tagMatches(genre, state.genre))) { return false; } } if (state.region && item.regions && item.regions.length) { if (!item.regions.some((region) => regionMatches(region, state.region))) { return false; } } return true; } function tagMatches(actual, selected) { const left = normalizeRegionOrGenre(actual); const right = normalizeRegionOrGenre(selected); return left === right || left.includes(right) || right.includes(left); } function regionMatches(actual, selected) { const aliases = { '中国香港': ['中国香港', '香港'], '中国台湾': ['中国台湾', '台湾'], '中国大陆': ['中国大陆', '大陆'] }; const actualNormalized = normalizeRegionOrGenre(actual); const acceptable = aliases[selected] || [selected]; return acceptable.some((value) => { const normalized = normalizeRegionOrGenre(value); return ( actualNormalized === normalized || actualNormalized.includes(normalized) ); }); } function normalizeRegionOrGenre(value) { return String(value) .replace(/\s+/g, '') .replace(/^中国/, '中国') .toLowerCase(); } function stableSort(items, compare) { return items .map((item, index) => ({ item, index })) .sort((left, right) => { return compare(left.item, right.item) || left.index - right.index; }) .map((entry) => entry.item); } /* * ------------------------------------------------------------------ * 结果渲染 * ------------------------------------------------------------------ */ function renderResults() { if (state.fastModePage) { renderFastModeResults(state.filteredItems); return; } const total = state.filteredItems.length; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); if (state.page > totalPages) { state.page = totalPages; saveUrl(); } const start = (state.page - 1) * PAGE_SIZE; const items = state.filteredItems.slice(start, start + PAGE_SIZE); renderItemCollection(items); renderPagination(totalPages); updateResultStatus(); } function renderFastModeResults(items) { renderItemCollection(items); const currentPage = state.page; paginationElement.innerHTML = ` 第 ${currentPage} 页 `; } function renderItemCollection(items) { if (!items.length) { const candidateCount = state.candidates.length; let extra = ''; if (candidateCount > 0) { extra = `

实际搜索到 ${candidateCount} 条候选,均被当前评分/地区/可播放条件过滤。

`; } resultElement.innerHTML = `
🎬

当前页没有符合条件的条目

尝试清除地区或评分条件。

${extra}
`; return; } resultElement.className = state.view === 'grid' ? 'dcn-grid' : 'dcn-list'; resultElement.innerHTML = items .map(state.view === 'grid' ? renderGridCard : renderListCard) .join(''); installImageFallbacks(); } function renderGridCard(item) { const meta = buildMeta(item); return `
${escapeAttribute(item.title)} ${item.isNew ? '' : ''}

${escapeHtml(item.title)}

${renderStars(item.rate)} ${item.rate ? item.rate.toFixed(1) : '暂无评分'}
${meta ? `

${escapeHtml(meta)}

` : ''}
${item.playable ? '可播放' : ''} ${item.episodesInfo ? `${escapeHtml(item.episodesInfo)}` : ''}
`; } function renderListCard(item) { const meta = buildMeta(item); const creators = [ item.directors.length ? `导演:${item.directors.slice(0, 2).join(' / ')}` : '', item.actors.length ? `主演:${item.actors.slice(0, 4).join(' / ')}` : '' ].filter(Boolean); return `
${escapeAttribute(item.title)}

${escapeHtml(item.title)} ${item.year ? `(${escapeHtml(item.year)})` : ''}

${renderStars(item.rate)} ${item.rate ? item.rate.toFixed(1) : '暂无评分'}
${meta ? `

${escapeHtml(meta)}

` : ''} ${creators.map((line) => `

${escapeHtml(line)}

`).join('')} ${item.summary ? `

${escapeHtml(truncate(item.summary, 150))}

` : ''}
${item.playable ? '可播放' : ''} ${item.episodesInfo ? `${escapeHtml(item.episodesInfo)}` : ''}
`; } function buildMeta(item) { const parts = []; if (item.year) parts.push(item.year); if (item.regions.length) { parts.push(item.regions.slice(0, 3).join(' / ')); } if (item.genres.length) { parts.push(item.genres.slice(0, 4).join(' / ')); } return parts.join(' · '); } function renderStars(rate) { if (!rate) { return '☆☆☆☆☆'; } const full = Math.round(rate / 2); const empty = 5 - full; return ` ${'★'.repeat(full)}${'☆'.repeat(empty)} `; } function renderPagination(totalPages) { if (totalPages <= 1) { paginationElement.innerHTML = ''; return; } const pages = buildPageNumbers(state.page, totalPages); paginationElement.innerHTML = ` ${pages.map((page) => { if (page === '…') { return ''; } return ` `; }).join('')} `; } function buildPageNumbers(current, total) { if (total <= 7) { return Array.from({ length: total }, (_, index) => index + 1); } const pages = new Set([ 1, total, current - 2, current - 1, current, current + 1, current + 2 ]); const valid = Array.from(pages) .filter((page) => page >= 1 && page <= total) .sort((a, b) => a - b); const output = []; for (let index = 0; index < valid.length; index++) { if (index > 0 && valid[index] - valid[index - 1] > 1) { output.push('…'); } output.push(valid[index]); } return output; } function updateResultStatus() { const count = state.filteredItems.length; showStatus( `第 ${state.page} 页,共显示 ${count} 个条目。`, 'success' ); } function showLoadingSkeleton() { resultElement.className = 'dcn-grid'; paginationElement.innerHTML = ''; resultElement.innerHTML = Array.from({ length: 8 }, () => `
`).join(''); } function showError(message) { showStatus(message, 'error'); resultElement.className = ''; resultElement.innerHTML = `

加载失败

${escapeHtml(message)}

`; paginationElement.innerHTML = ''; } function showStatus(message, type = '') { if (!statusElement) { return; } statusElement.className = `dcn-status ${type}`; statusElement.textContent = message; } function installImageFallbacks() { root.querySelectorAll('img.dcn-poster, img.dcn-list-poster') .forEach((image) => { image.addEventListener('error', () => { image.removeAttribute('src'); image.classList.add('dcn-image-missing'); }, { once: true }); }); } function scrollToResults() { const top = resultElement.getBoundingClientRect().top + window.scrollY - 20; window.scrollTo({ top, behavior: 'smooth' }); } /* * ------------------------------------------------------------------ * 工具函数 * ------------------------------------------------------------------ */ function checkToken(token) { if (token !== state.abortToken) { throw new Error('搜索已取消'); } } function delay(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } function cleanText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } function truncate(text, maxLength) { const normalized = cleanText(text); if (normalized.length <= maxLength) { return normalized; } return `${normalized.slice(0, maxLength)}…`; } function looksLikeVerificationPage(html) { const text = String(html || '').toLowerCase(); return ( text.includes('sec.douban.com') || text.includes('captcha') || text.includes('访问验证') || text.includes('检测到有异常请求') || text.includes('机器人程序') ); } function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function escapeHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeAttribute(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(/"/g, '"'); } function injectStyles() { const css = ` .dcn-app { max-width: 1200px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; color: #111; line-height: 1.6; } .dcn-filter-box { background: #f7f7f7; border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; } .dcn-filter-row { display: flex; align-items: flex-start; margin-bottom: 12px; } .dcn-filter-row:last-child { margin-bottom: 0; } .dcn-filter-name { flex-shrink: 0; width: 48px; font-weight: 600; color: #666; padding-top: 4px; } .dcn-filter-values { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } .dcn-filter-link { background: transparent; border: none; color: #37a; cursor: pointer; padding: 4px 10px; border-radius: 4px; font-size: 14px; line-height: 1.4; } .dcn-filter-link:hover { background: #e8f1f8; } .dcn-filter-link.active { background: #37a; color: #fff; } .dcn-separator { color: #ccc; margin: 0 6px; } .dcn-year-input { width: 90px; padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; } .dcn-mini-button { background: #37a; color: #fff; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 14px; } .dcn-mini-button:hover { background: #256aa7; } .dcn-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 12px; } .dcn-toolbar-left, .dcn-toolbar-right { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; } .dcn-toolbar label { display: flex; align-items: center; gap: 6px; font-size: 14px; } .dcn-label-text { color: #666; } .dcn-toolbar select { padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; } .dcn-check { cursor: pointer; } .dcn-icon-button { background: #fff; border: 1px solid #ccc; border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 16px; line-height: 1; } .dcn-icon-button.active { background: #37a; color: #fff; border-color: #37a; } .dcn-button { background: #37a; color: #fff; border: none; border-radius: 4px; padding: 6px 16px; cursor: pointer; font-size: 14px; } .dcn-button:hover { background: #256aa7; } .dcn-button.secondary { background: #fff; color: #37a; border: 1px solid #37a; } .dcn-selected { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 16px; min-height: 28px; } .dcn-selected-title { color: #666; font-size: 14px; } .dcn-selected-tag { display: inline-flex; align-items: center; gap: 4px; background: #eef7ff; color: #37a; border: 1px solid #cce3f5; border-radius: 12px; padding: 2px 10px; cursor: pointer; font-size: 13px; } .dcn-selected-tag:hover { background: #dcefff; } .dcn-clear-all { background: transparent; border: none; color: #999; cursor: pointer; font-size: 13px; } .dcn-clear-all:hover { color: #37a; } .dcn-muted { color: #999; } .dcn-status { margin-bottom: 12px; font-size: 14px; min-height: 22px; } .dcn-status.loading { color: #37a; } .dcn-status.success { color: #666; } .dcn-status.empty { color: #999; } .dcn-status.error { color: #c33; } .dcn-status.warning { color: #c90; } .dcn-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; } .dcn-list { display: flex; flex-direction: column; gap: 16px; } .dcn-card { background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } .dcn-poster-link { display: block; position: relative; aspect-ratio: 2 / 3; background: #f0f0f0; } .dcn-poster { width: 100%; height: 100%; object-fit: cover; display: block; } .dcn-poster.dcn-image-missing { display: flex; align-items: center; justify-content: center; background: #e0e0e0; } .dcn-poster.dcn-image-missing::after { content: "暂无海报"; color: #999; font-size: 13px; } .dcn-new { position: absolute; top: 6px; left: 6px; background: #ff6a00; color: #fff; font-size: 12px; padding: 2px 6px; border-radius: 4px; } .dcn-card-body { padding: 12px; } .dcn-title { margin: 0 0 6px; font-size: 15px; line-height: 1.4; } .dcn-title a { color: #37a; text-decoration: none; } .dcn-title a:hover { color: #256aa7; text-decoration: underline; } .dcn-rating { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } .dcn-stars { color: #ffac2d; letter-spacing: -1px; } .dcn-stars.empty { color: #ddd; } .dcn-rate-number { color: #ffac2d; font-weight: 600; font-size: 14px; } .dcn-no-rate { color: #999; font-size: 13px; } .dcn-meta { margin: 0 0 8px; font-size: 13px; color: #666; line-height: 1.5; } .dcn-card-tags { display: flex; flex-wrap: wrap; gap: 6px; } .dcn-badge { display: inline-block; background: #f0f0f0; color: #666; font-size: 12px; padding: 2px 6px; border-radius: 4px; } .dcn-badge.playable { background: #e6f7e6; color: #2a8a2a; } .dcn-list-item { display: flex; gap: 16px; background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } .dcn-list-poster-link { flex-shrink: 0; width: 100px; height: 150px; background: #f0f0f0; border-radius: 4px; overflow: hidden; } .dcn-list-poster { width: 100%; height: 100%; object-fit: cover; display: block; } .dcn-list-content { flex: 1; min-width: 0; } .dcn-list-title { margin: 0 0 8px; font-size: 17px; } .dcn-list-title a { color: #37a; text-decoration: none; } .dcn-list-title a:hover { color: #256aa7; text-decoration: underline; } .dcn-title-year { color: #999; font-weight: normal; margin-left: 6px; } .dcn-list-meta { margin: 0 0 8px; font-size: 13px; color: #666; } .dcn-creator { margin: 0 0 6px; font-size: 13px; color: #555; } .dcn-summary { margin: 8px 0 0; font-size: 13px; color: #777; line-height: 1.6; } .dcn-pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 24px; flex-wrap: wrap; } .dcn-page-button, .dcn-page-number { background: #fff; border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 14px; color: #37a; } .dcn-page-button:hover:not(:disabled), .dcn-page-number:hover:not(.active) { background: #f7f7f7; } .dcn-page-button:disabled { color: #bbb; cursor: not-allowed; } .dcn-page-number.active { background: #37a; color: #fff; border-color: #37a; } .dcn-current-page { color: #666; font-size: 14px; } .dcn-page-ellipsis { color: #999; padding: 0 4px; } .dcn-empty, .dcn-error-box { text-align: center; padding: 48px 20px; color: #666; } .dcn-empty-icon { font-size: 48px; margin-bottom: 12px; } .dcn-skeleton-card { background: #fff; border-radius: 8px; padding: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } .dcn-skeleton { background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%); background-size: 200% 100%; animation: dcn-skeleton-loading 1.2s infinite; border-radius: 4px; } .dcn-skeleton-poster { aspect-ratio: 2 / 3; margin-bottom: 12px; } .dcn-skeleton-line { height: 14px; margin-bottom: 8px; width: 60%; } .dcn-skeleton-line.wide { width: 90%; } @keyframes dcn-skeleton-loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } `; const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); } waitForApp(); })();