// ==UserScript== // @name 豆瓣榜单助手·Douban-Ranker // @namespace https://github.com/eddiehe99/douban-ranker // @homepageURL https://douban-ranker.eddiehe.top // @supportURL https://github.com/eddiehe99/douban-ranker/issues // @updateURL https://douban-ranker.eddiehe.top/douban-ranker.user.js // @downloadURL https://douban-ranker.eddiehe.top/douban-ranker.user.js // @license MIT // @version 0.4.3 // @description 在豆瓣电影、播客、音乐页面展示作品在不同榜单中的排名 // @author Eddie He // @contributor CRonaldoWei // @icon https://img3.doubanio.com/favicon.ico // @match https://movie.douban.com/subject/* // @match https://www.douban.com/podcast/* // @match https://music.douban.com/subject/* // @include https://movie.douban.com/* // @include https://music.douban.com/* // @include https://book.douban.com/* // @include https://www.douban.com/podcast/* // @connect * // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // ==/UserScript== (() => { 'use strict'; console.log("脚本: 豆瓣榜单助手·Douban-Ranker--开始执行--GitHub: https://github.com/eddiehe99/douban-ranker"); // 配置常量 const CONFIG = { movieRankUrl: "https://rank4douban.eddiehe.top/data.json", podcastRankUrl: "https://xyzrank.eddiehe.top/full.json", musicRankUrl: "https://hma.eddiehe.top/data.json", top250Class: "top250", top250CssUrl: "https://img1.doubanio.com/cuphead/movie-static/charts/top250.24c18.css", toggleButtonId: "rank_toggle", rankLabelClass: "rank-label rank-label-other", rankLabelCssUrl: "https://img1.doubanio.com/cuphead/movie-static/subject/rank_label.dda40.css" }; // 缓存样式是否已加载 let isTop250StyleInjected = false; let isRankLabelStyleInjected = false; /** * 注入外部 Top 250 CSS 样式表 */ function injectTop250Stylesheet() { if (isTop250StyleInjected) return; const existingLink = document.querySelector(`link[href*="${CONFIG.top250CssUrl}"]`); if (!existingLink) { const styleLink = document.createElement('link'); styleLink.rel = 'stylesheet'; styleLink.href = CONFIG.top250CssUrl; document.head.appendChild(styleLink); } isTop250StyleInjected = true; } /** * 注入外部 Rank Label CSS 样式表 */ function injectRankLabelStylesheet() { if (isRankLabelStyleInjected) return; const existingLink = document.querySelector(`link[href*="${CONFIG.rankLabelCssUrl}"]`); if (!existingLink) { const styleLink = document.createElement('link'); styleLink.rel = 'stylesheet'; styleLink.href = CONFIG.rankLabelCssUrl; document.head.appendChild(styleLink); } isRankLabelStyleInjected = true; } /** * 创建排名组件 * @param {Object} options * @param {string} options.prefix 前缀(如"TOP") * @param {number} options.position 排名位置 * @param {string} options.title 榜单标题 * @param {string} options.shortTitle 简短标题 * @param {string} options.href 榜单链接 * @returns {string} HTML字符串 */ function createTop250RankItem({ prefix = 'No.', position, title, shortTitle, href }) { return [ `
`, `${prefix}${position}`, ``, `${shortTitle}`, ``, `
`, ].join(''); } /** * 移除指定链接的 loading 占位元素 */ function removeProcessingItem(href) { const items = document.querySelectorAll(`.${CONFIG.top250Class} a[href="${href}"]`); items.forEach(item => { const parent = item.closest(`.${CONFIG.top250Class}`); if (parent) parent.remove(); }); } /** * 创建排名组件 * @param {Object} options * @param {string} options.prefix 前缀(如"TOP") * @param {number} options.position 排名位置 * @param {string} options.title 榜单标题 * @param {string} options.shortTitle 简短标题 * @param {string} options.href 榜单链接 * @returns {string} HTML字符串 */ function createRankLabelRankItem({ prefix = 'No.', position, title, shortTitle, href }) { return [ `
`, ``, `${prefix}${position}`, ``, ``, `${shortTitle}`, ``, `
`, ].join(''); } /** * 初始化展示逻辑 * @param {HTMLElement} container 插入位置 * @param {Array} items 排名组件数组 */ function renderRankListWithToggle(container, items) { // 将 HTML 字符串转换为真实的 DOM 节点 const fragment = document.createDocumentFragment(); items.forEach(html => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; tempDiv.style.display = "inline-block"; fragment.appendChild(tempDiv.firstElementChild); // 取出真正的 div 元素 const space = document.createTextNode(' '); fragment.appendChild(space); }); // 在 container 之前插入 fragment container.parentNode.insertBefore(fragment, container); // 折叠逻辑 const allItems = document.querySelectorAll(`.${CONFIG.top250Class}, .${CONFIG.rankLabelClass}`); allItems.forEach(item => { item.style.display = "inline-block" }); if (allItems.length > 4) { // 创建折叠按钮 const toggleBtn = document.createElement('div'); toggleBtn.id = CONFIG.toggleButtonId; toggleBtn.innerHTML = '展示剩余 →'; toggleBtn.setAttribute('data-toggle', 'show'); toggleBtn.style.display = 'inline-block'; // 在 container 之前插入 toggleBtn container.parentNode.insertBefore(toggleBtn, container); Array.from(allItems).slice(4).forEach(item => { item.style.display = 'none' }); // 点击按钮切换显示/隐藏 toggleBtn.addEventListener('click', function () { const toggleState = this.getAttribute('data-toggle'); if (toggleState === 'show') { Array.from(allItems).slice(4).forEach(item => { item.style.display = "inline-block" }); this.setAttribute('data-toggle', 'hide'); this.innerHTML = '隐藏剩余 ←'; } else { Array.from(allItems).slice(4).forEach(item => { item.style.display = "none" }); this.setAttribute('data-toggle', 'show'); this.innerHTML = '展示剩余 →'; } }); } } /** * 处理电影页面 */ function handleMoviePage() { const matchResult = window.location.href.match(/^https:\/\/movie\.douban\.com\/subject\/(\d+)\/?$/); if (!matchResult) return; const doubanId = matchResult[1]; const header = document.querySelector("#content > h1"); if (!header) return; const processingStatusItem = createTop250RankItem({ position: '...', title: 'rank4douban', shortTitle: '处理中', href: 'https://rank4douban.eddiehe.top/' }); injectTop250Stylesheet(); renderRankListWithToggle(header, [processingStatusItem]); GM_xmlhttpRequest({ method: 'GET', url: CONFIG.movieRankUrl, onload: function (response) { try { const data = JSON.parse(response.responseText); const lists = Object.values(data); const rankItems = lists .filter(list => list.list[doubanId]) .map(list => ({ prefix: list.prefix || 'No.', position: list.list[doubanId], title: list.title, shortTitle: list.short_title, href: list.href })) .map(createTop250RankItem); // remove processing status item removeProcessingItem("https://rank4douban.eddiehe.top/"); if (rankItems.length > 0) { renderRankListWithToggle(header, rankItems); } } catch (e) { console.error("【豆瓣榜单助手·Douban-Ranker】电影榜单数据处理失败:", e); alert("豆瓣榜单助手·Douban-Ranker:电影榜单数据处理时发生错误,请稍后再试!"); } }, onerror: function (error) { console.error("【豆瓣榜单助手·Douban-Ranker】电影榜单网络请求失败:", error); alert("豆瓣榜单助手 · Douban-Ranker:电影榜单网络请求时发生错误,请检查您的网络连接后重试!"); } }); } /** * 处理播客页面 */ function handlePodcastPage() { const header = document.querySelector("#content > h1"); if (!header) return; const podcastName = header.innerText.trim(); const processingStatusItem = createTop250RankItem({ position: '...', title: '中文播客榜', shortTitle: '处理中', href: 'https://xyzrank.eddiehe.top/' }); injectTop250Stylesheet(); renderRankListWithToggle(header, [processingStatusItem]); GM_xmlhttpRequest({ method: 'GET', url: CONFIG.podcastRankUrl, onload: function (response) { try { const data = JSON.parse(response.responseText); const podcast = data.data?.podcasts?.find(p => p.name === podcastName); // remove processing status item removeProcessingItem("https://xyzrank.eddiehe.top/"); if (podcast && podcast.rank) { const item = createTop250RankItem({ position: podcast.rank, title: '中文播客榜', shortTitle: '中文播客榜', href: podcast.links[0]?.url || '#' }); renderRankListWithToggle(header, [item]); } } catch (e) { console.error("【豆瓣榜单助手·Douban-Ranker】播客榜单数据处理失败:", e); alert("豆瓣榜单助手·Douban-Ranker:播客榜单数据处理时发生错误,请稍后再试!"); } }, onerror: function (error) { console.error("【豆瓣榜单助手·Douban-Ranker】播客榜单网络请求失败:", error); alert("豆瓣榜单助手·Douban-Ranker:播客榜单网络请求时发生错误,请检查您的网络连接后重试!"); } }); } /** * 处理音乐页面 */ function handleMusicPage() { const header = document.querySelector("h1"); if (!header) return; const albumName = header.textContent.trim(); const processingStatusItem = createTop250RankItem({ position: '...', title: 'HOPICO MUSIC AWARD', shortTitle: '处理中', href: 'https://hma.eddiehe.top/' }); injectTop250Stylesheet(); injectRankLabelStylesheet(); renderRankListWithToggle(header, [processingStatusItem]); GM_xmlhttpRequest({ method: 'GET', url: CONFIG.musicRankUrl, onload: function (response) { try { const data = JSON.parse(response.responseText); const rankItems = []; // 遍历所有届次 (HOPICO MUSIC AWARDS) Object.entries(data).forEach(([awardName, awardData]) => { if (!awardName.includes('HOPICO MUSIC AWARDS')) return; const categories = awardData.categories || {}; // 检查所有奖项类别 Object.entries(categories).forEach(([categoryKey, category]) => { const matchingAlbums = category.albums.filter(album => album.name == albumName ); matchingAlbums.forEach(album => { rankItems.push(createRankLabelRankItem({ prefix: '# ', position: album.award, title: category.title, shortTitle: category.short_title, href: awardData.href, })); }); }); }); // remove processing status item removeProcessingItem("https://hma.eddiehe.top/"); if (rankItems.length > 0) { renderRankListWithToggle(header, rankItems); } } catch (e) { console.error("【豆瓣榜单助手·Douban-Ranker】音乐榜单数据处理失败:", e); alert("豆瓣榜单助手·Douban-Ranker:音乐榜单数据处理时发生错误,请稍后再试!"); } }, onerror: function (error) { console.error("【豆瓣榜单助手·Douban-Ranker】音乐榜单网络请求失败:", error); alert("豆瓣榜单助手·Douban-Ranker:音乐榜单网络请求时发生错误,请检查您的网络连接后重试!"); } }); } // 页面适配入口 switch (location.host) { case 'movie.douban.com': handleMoviePage(); break; case 'www.douban.com': if (location.pathname.startsWith('/podcast')) handlePodcastPage(); break; case 'music.douban.com': handleMusicPage(); break; } })();