// ==UserScript== // @name 蜜柑计划评分显示(改) // @namespace http://tampermonkey.net/ // @version 1.1 // @description 评分显示及详情页Bangumi链接自动替换 // @author CNOS // @match https://mikanani.me/* // @match https://mikanani.tv/* // @match https://mikanani.kas.pub/* // @connect mikanani.me // @connect mikanani.tv // @connect mikanani.kas.pub // @connect bangumi.lol // @connect bgm.tv // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // ================= 配置区域 ================= const CONFIG = { requestInterval: 1500, // 抓取间隔 1.5秒 cacheExpiry: 15 * 24 * 60 * 60 * 1000, // 缓存15天 retryLimit: 2, style: ` /* 评分标签样式 */ .bgm-score-tag { position: absolute; top: 0; right: 0; transform: translateY(-100%); background: rgba(0, 0, 0, 0.85); color: #ff4d4d; font-weight: 900; font-size: 14px; padding: 2px 8px; border-top-left-radius: 6px; z-index: 20; font-family: Arial, sans-serif; pointer-events: none; line-height: normal; box-shadow: -1px -1px 2px rgba(0,0,0,0.3); } /* 确保文字区域是定位基准 */ .an-info { position: relative !important; overflow: visible !important; } ` }; GM_addStyle(CONFIG.style); // ================= 缓存系统 ================= const Cache = { get: (id) => { const data = GM_getValue(`bgm_score_${id}`); if (!data) return null; if (Date.now() - data.timestamp > CONFIG.cacheExpiry) return null; return data.score; }, set: (id, score) => { GM_setValue(`bgm_score_${id}`, { score: score, timestamp: Date.now() }); } }; // ================= 任务队列 ================= const queue = []; let isRunning = false; function enqueueTask(mikanId, infoElement) { const cachedScore = Cache.get(mikanId); if (cachedScore) { renderScore(infoElement, cachedScore); return; } if (!queue.some(t => t.id === mikanId)) { queue.push({ id: mikanId, el: infoElement, retries: 0 }); runQueue(); } } async function runQueue() { if (isRunning || queue.length === 0) return; isRunning = true; const task = queue.shift(); try { const score = await fetchFullChain(task.id); Cache.set(task.id, score); renderScore(task.el, score); } catch (err) { console.error(`[BGM Fetcher] ID: ${task.id} Error:`, err); if (task.retries < CONFIG.retryLimit) { task.retries++; queue.push(task); } else { Cache.set(task.id, "Err"); } } finally { setTimeout(() => { isRunning = false; runQueue(); }, CONFIG.requestInterval); } } // ================= 网络请求 ================= function request(url) { return new Promise((resolve, reject) => { let referer = window.location.origin + "/"; if (url.includes("bangumi.lol")) { referer = "https://bangumi.lol/"; } GM_xmlhttpRequest({ method: "GET", url: url, timeout: 15000, headers: { "Referer": referer }, onload: (res) => { if (res.status >= 200 && res.status < 400) resolve(res.responseText); else reject(`HTTP ${res.status}`); }, onerror: reject, ontimeout: reject }); }); } async function fetchFullChain(mikanId) { const currentOrigin = window.location.origin; const htmlB = await request(`${currentOrigin}/Home/Bangumi/${mikanId}`); const bgmLinkMatch = htmlB.match(/href="(https?:\/\/(?:bgm\.tv|bangumi\.lol)\/subject\/\d+)"/); if (!bgmLinkMatch) { throw new Error("BGM link not found in Mikan page"); } const bgmUrl = bgmLinkMatch[1].replace("bgm.tv", "bangumi.lol"); const htmlC = await request(bgmUrl); let scoreMatch = htmlC.match(/property="v:average">([\d\.]+)<\/span>/); if (!scoreMatch) { scoreMatch = htmlC.match(/class="score"[^>]*>([\d\.]+)<\/span>/); } if (!scoreMatch) { scoreMatch = htmlC.match(/(\d+\.\d+)/); } if (!scoreMatch) { throw new Error("Score parse failed in BGM page"); } return scoreMatch[1]; } // ================= 渲染 ================= function renderScore(container, score) { if (container.querySelector('.bgm-score-tag')) return; if (!score || score === "Err" || score === "") return; const tag = document.createElement('div'); tag.className = 'bgm-score-tag'; tag.innerText = score; container.appendChild(tag); } // ================= 详情页链接替换 ================= function replaceBgmLinks() { // 策略1:基于HTML结构特征定位 // 查找包含“Bangumi番组计划”或“Bangumi”字样的容器元素 const containers = document.querySelectorAll('p, div, span, li, td'); containers.forEach(container => { if (container.textContent.includes('Bangumi番组计划') || container.textContent.includes('Bangumi')) { const links = container.querySelectorAll('a[href*="bgm.tv"]'); links.forEach(link => { // 仅替换域名部分,保持协议、路径、查询参数等不变 link.href = link.href.replace(/(https?:\/\/)bgm\.tv/, '$1bangumi.lol'); }); } }); // 策略2:全局兜底替换 // 确保页面上所有指向 bgm.tv 的链接都被替换,防止遗漏特殊结构 const allLinks = document.querySelectorAll('a[href*="bgm.tv"]'); allLinks.forEach(link => { link.href = link.href.replace(/(https?:\/\/)bgm\.tv/, '$1bangumi.lol'); }); } // ================= 初始化 ================= function init() { // 页面加载完成后,首先执行详情页链接替换 replaceBgmLinks(); const scan = () => { // 寻找带有 id 的封面元素,操作其兄弟元素 .an-info const coverSpans = document.querySelectorAll('.js-expand_bangumi[data-bangumiid]'); coverSpans.forEach(span => { if (span.dataset.bgmHandled) return; span.dataset.bgmHandled = "true"; const id = span.getAttribute('data-bangumiid'); const parentLi = span.closest('li'); if (!parentLi) return; const infoDiv = parentLi.querySelector('.an-info'); if (id && infoDiv) { enqueueTask(id, infoDiv); } }); }; scan(); // 监听动态加载(支持周一到周日所有 Tab 切换及详情页动态内容) const observer = new MutationObserver((mutations) => { let shouldScan = false; mutations.forEach(m => { if (m.addedNodes.length > 0) shouldScan = true; }); if (shouldScan) { scan(); // 动态加载节点后,重新尝试替换可能新出现的 Bangumi 链接 replaceBgmLinks(); } }); observer.observe(document.body, { childList: true, subtree: true }); } init(); })();