參考Duckee KememChan的fc2腳本用AI重構
// ==UserScript==
// @name FC2CMADB-improved
// @namespace https://sleazyfork.org/zh-CN/scripts/583333-fc2cmadb-improved
// @version 1.0.0
// @description 參考Duckee KememChan的fc2腳本用AI重構
// @author Awei
// @match *://fc2cmadb.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// =========================================================================
// 1. 介面樣式定義 (毛玻璃質感 UI)
// =========================================================================
const customCSS = `
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css");
#fc2-custom-panel {
background: rgba(17, 25, 40, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 20px;
margin: 20px 0 30px 0;
width: 100%;
color: #fff;
}
.fc2-btn-row {
display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;
padding-bottom: 15px; border-bottom: 1px solid rgba(255,255,255,0.1);
}
.fc2-btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
padding: 8px 16px; border-radius: 8px; font-size: 14px; font-weight: 600;
text-decoration: none !important; transition: all 0.2s ease;
background: rgba(255,255,255,0.1); color: #fff;
}
.fc2-btn:hover { transform: translateY(-2px); background: rgba(255,255,255,0.25); color: #fff; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
.fc2-btn-missav { color: #ff9e9e; }
.fc2-btn-njav { color: #a78bfa; }
.fc2-btn-sukebei { color: #ffda9e; }
.fc2-btn-magnet { color: #9eecff; background: rgba(59, 130, 246, 0.2); }
.fc2-preview-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px;
}
.fc2-media {
width: 100%; height: auto; border-radius: 8px; object-fit: cover;
box-shadow: 0 4px 10px rgba(0,0,0,0.2); background-color: #1a1a2e;
}
/* ------------------------------------------------------------- */
/* 新增:外框包裝卡片 (Wrapper) 樣式 */
/* ------------------------------------------------------------- */
.fc2-custom-card-wrapper {
display: flex;
flex-direction: column;
background: rgba(30, 41, 59, 0.5); /* 半透明深藍灰 */
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.fc2-custom-card-wrapper:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
}
/* 覆寫原本卡片的樣式,讓它與外框融為一體 */
.fc2-original-card-override {
background: transparent !important;
border: none !important;
box-shadow: none !important;
border-radius: 12px 12px 0 0 !important;
flex-grow: 1; /* 讓內容佔滿剩餘空間,推擠底部按鈕列 */
}
/* 卡片底部的按鈕區塊 */
.fc2-card-btn-row {
display: flex; gap: 8px; flex-wrap: wrap;
padding: 12px; width: 100%;
background: rgba(15, 23, 42, 0.7); /* 較深的底部背景 */
border-top: 1px solid rgba(255, 255, 255, 0.05);
margin-top: auto; /* 置底對齊 */
justify-content: center; /* 置中按鈕 */
position: relative; z-index: 20;
}
.fc2-card-btn {
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
padding: 5px 10px; border-radius: 6px; font-size: 11px; font-weight: 600;
text-decoration: none !important; transition: all 0.2s ease;
}
.fc2-card-btn:hover { transform: translateY(-2px); filter: brightness(1.2); }
.fc2-card-btn-missav { color: #ff9e9e; background: rgba(255, 158, 158, 0.15); }
.fc2-card-btn-njav { color: #a78bfa; background: rgba(167, 139, 250, 0.15); }
.fc2-card-btn-sukebei { color: #ffda9e; background: rgba(255, 218, 158, 0.15); }
.fc2-card-btn-magnet { color: #9eecff; background: rgba(158, 236, 255, 0.15); }
@keyframes fc2-fade-in {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
`;
GM_addStyle(customCSS);
// =========================================================================
// 2. API 請求模組與快取機制 (Caching)
// =========================================================================
const seedCache = new Map(); // 紀錄已經查過的種子資料
const pendingRequests = new Map(); // 紀錄正在查詢中的請求,防止短時間重複查詢同一個番號
const API = {
// 一次性打包多個番號查詢 Sukebei
async getSukebeiBatch(codes) {
const codesToFetch = codes.filter(code => !seedCache.has(code) && !pendingRequests.has(code));
let batchPromise = Promise.resolve();
if (codesToFetch.length > 0) {
batchPromise = new Promise(resolve => {
const query = encodeURIComponent(codesToFetch.join('|'));
GM_xmlhttpRequest({
method: "GET",
url: `https://sukebei.nyaa.si/?f=0&c=0_0&q=${query}&s=seeders&o=desc`,
onload: (res) => {
codesToFetch.forEach(code => pendingRequests.delete(code));
if (res.status !== 200) {
codesToFetch.forEach(code => seedCache.set(code, null));
return resolve();
}
const parser = new DOMParser();
const doc = parser.parseFromString(res.responseText, "text/html");
const rows = doc.querySelectorAll("tbody > tr");
codesToFetch.forEach(code => seedCache.set(code, null));
rows.forEach(row => {
const titleText = Array.from(row.querySelectorAll("td a:not(.comments)")).map(a => a.textContent).join(" ");
const matchedCode = codesToFetch.find(code => titleText.includes(code));
if (matchedCode && !seedCache.get(matchedCode)) {
const dlLink = row.querySelector("td a i.fa-download")?.parentElement?.href || "";
const magLink = row.querySelector("td a i.fa-magnet")?.parentElement?.href || "";
const seeds = row.querySelector("td:nth-last-child(3)")?.textContent.replace(/[^0-9]/g, "") || "0";
seedCache.set(matchedCode, {
torrent: dlLink ? new URL(dlLink, "https://sukebei.nyaa.si").href : "",
magnet: magLink,
seed: seeds
});
}
});
resolve();
},
onerror: () => {
codesToFetch.forEach(code => {
pendingRequests.delete(code);
seedCache.set(code, null);
});
resolve();
}
});
});
codesToFetch.forEach(code => pendingRequests.set(code, batchPromise));
}
const allPromises = codes.map(code => pendingRequests.get(code)).filter(Boolean);
await Promise.all([batchPromise, ...allPromises]);
},
async getBaihuse(fc2code) {
const url = `https://baihuse.com/fc2daily/detail/FC2-PPV-${fc2code}`;
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: (res) => {
if (res.status !== 200) return resolve({ images: [], videos: [] });
const parser = new DOMParser();
const doc = parser.parseFromString(res.responseText, "text/html");
const expectedPath = `/fc2daily/data/FC2-PPV-${fc2code}/`;
const images = Array.from(doc.querySelectorAll('img'))
.map(img => img.getAttribute('src'))
.filter(src => src && src.includes(expectedPath))
.map(src => src.startsWith('/') ? `https://baihuse.com${src}` : `https://baihuse.com/${src}`);
const videos = Array.from(doc.querySelectorAll('video'))
.map(v => {
let src = v.getAttribute('src');
if (!src) {
const source = v.querySelector('source');
if (source) src = source.getAttribute('src');
}
return src;
})
.filter(src => src && src.includes(expectedPath))
.map(src => src.startsWith('/') ? `https://baihuse.com${src}` : `https://baihuse.com/${src}`);
resolve({ images, videos });
},
onerror: () => resolve({ images: [], videos: [] })
});
});
}
};
// =========================================================================
// 3. 頁面控制與渲染模組
// =========================================================================
const App = {
async renderList() {
const links = document.querySelectorAll('a[href*="/articles/"]');
const newCodes = [];
const elementMap = new Map();
// 掃描畫面上的卡片並蒐集番號
links.forEach((link) => {
const match = link.href.match(/\/articles\/(\d{6,8})/);
if (!match) return;
const code = match[1];
const img = link.querySelector('img');
if (!img) return;
const imgContainer = img.parentElement;
// 若已處理過,跳過
if (!imgContainer || imgContainer.dataset.fc2Processed === code) return;
imgContainer.dataset.fc2Processed = code;
// --- 尋找卡片外層並使用自定義卡片包裝 ---
// 通常外圍卡片會有 rounded-lg 或背景色,我們往上找
const cardContainer = link.closest('.rounded-lg') || link.closest('.bg-gray-800') || link.parentElement;
let btnRow = null;
if (cardContainer && cardContainer.parentElement && !cardContainer.parentElement.classList.contains('fc2-custom-card-wrapper')) {
// 建立全新的卡片外框
const wrapper = document.createElement('div');
wrapper.className = 'fc2-custom-card-wrapper';
// 繼承原卡片的高度屬性 (Tailwind 'h-full'),保證網格對齊
if (cardContainer.classList.contains('h-full')) {
cardContainer.classList.remove('h-full');
wrapper.classList.add('h-full');
}
// 將自定義卡片插入到 DOM 中,並將原卡片包覆進去
cardContainer.parentNode.insertBefore(wrapper, cardContainer);
wrapper.appendChild(cardContainer);
// 清除原卡片的背景與邊框,融入新卡片中
cardContainer.classList.add('fc2-original-card-override');
// 建立置於底部的按鈕列
btnRow = document.createElement('div');
btnRow.className = 'fc2-card-btn-row';
// 點擊按鈕列時不要觸發卡片上的全域連結跳轉
btnRow.addEventListener('click', (e) => e.stopPropagation());
// 預先放入靜態連結按鈕
btnRow.innerHTML = `
<a href="https://missav.ws/en/fc2-ppv-${code}" target="_blank" class="fc2-card-btn fc2-card-btn-missav" title="MissAV">
MissAV
</a>
<a href="https://123av.com/en/dm2/v/fc2-ppv-${code}" target="_blank" class="fc2-card-btn fc2-card-btn-njav" title="Njav">
Njav
</a>
<a href="https://sukebei.nyaa.si/?f=0&c=0_0&q=${code}&s=seeders&o=desc" target="_blank" class="fc2-card-btn fc2-card-btn-sukebei fc2-sukebei-btn-${code}" title="Sukebei 搜尋">
<i class="fa-solid fa-magnifying-glass"></i> 搜尋
</a>
`;
wrapper.appendChild(btnRow);
} else if (cardContainer && cardContainer.parentElement.classList.contains('fc2-custom-card-wrapper')) {
// 若已包裝過,直接獲取按鈕列
btnRow = cardContainer.parentElement.querySelector('.fc2-card-btn-row');
}
newCodes.push(code);
if (!elementMap.has(code)) elementMap.set(code, []);
elementMap.get(code).push({
btnRow: btnRow
});
});
if (newCodes.length > 0) {
// 去除重複的番號
const uniqueCodes = [...new Set(newCodes)];
// 一次性打包全部番號並發送單次請求
await API.getSukebeiBatch(uniqueCodes);
// 把快取中的結果統一渲染到底部按鈕列上
uniqueCodes.forEach(code => {
const sukebei = seedCache.get(code);
if (sukebei) {
const containersData = elementMap.get(code) || [];
containersData.forEach(data => {
if (data.btnRow) {
// 1. 將種子數更新到底層 Sukebei 按鈕中
const sukebeiBtn = data.btnRow.querySelector(`.fc2-sukebei-btn-${code}`);
if (sukebeiBtn) {
sukebeiBtn.innerHTML = `<i class="fa-solid fa-seedling"></i> ${sukebei.seed}`;
}
// 2. 渲染 Magnet 磁力按鈕 (如果有)
if (!data.btnRow.querySelector('.fc2-card-btn-magnet') && sukebei.magnet) {
data.btnRow.insertAdjacentHTML('beforeend', `
<a href="${sukebei.magnet}" class="fc2-card-btn fc2-card-btn-magnet" title="Magnet (${sukebei.seed})">
<i class="fa-solid fa-magnet"></i> 磁力
</a>
`);
}
}
});
}
});
}
},
async renderDetail() {
if (document.getElementById('fc2-custom-panel')) return;
const match = location.href.match(/articles\/(\d+)/);
if (!match) return;
const fc2code = match[1];
let container = document.querySelector('.container') || document.querySelector('main');
let h1 = document.querySelector('h1');
let insertTarget = h1 ? h1.parentElement : container;
if (!insertTarget || !insertTarget.parentNode) return;
const panel = document.createElement('div');
panel.id = 'fc2-custom-panel';
panel.innerHTML = `
<div class="fc2-btn-row" id="fc2-btn-row">
<span style="color: #a5a5b5; font-size: 14px; display: flex; align-items: center;">
<i class="fa-solid fa-spinner fa-spin" style="margin-right: 8px;"></i> 正在撈取資料...
</span>
</div>
<div class="fc2-preview-grid" id="fc2-preview-grid"></div>
`;
insertTarget.parentNode.insertBefore(panel, insertTarget.nextSibling);
// 調用批次請求方法 (即便只有一個番號,依然會進入快取系統)
await Promise.all([
API.getSukebeiBatch([fc2code]),
API.getBaihuse(fc2code)
]);
const sukebei = seedCache.get(fc2code);
const btnRow = document.getElementById('fc2-btn-row');
btnRow.innerHTML = `
<a href="https://missav.ws/en/fc2-ppv-${fc2code}" target="_blank" class="fc2-btn fc2-btn-missav">
<i class="fa-solid fa-globe"></i> MissAV
</a>
<a href="https://123av.com/en/dm2/v/fc2-ppv-${fc2code}" target="_blank" class="fc2-btn fc2-btn-njav">
<i class="fa-solid fa-globe"></i> Njav
</a>
<a href="https://sukebei.nyaa.si/?f=0&c=0_0&q=${fc2code}&s=seeders&o=desc" target="_blank" class="fc2-btn fc2-btn-sukebei">
<i class="fa-solid fa-magnifying-glass"></i> Sukebei
</a>
`;
if (sukebei) {
btnRow.innerHTML += `
<a href="${sukebei.magnet}" class="fc2-btn fc2-btn-magnet" title="Magnet">
<i class="fa-solid fa-magnet"></i> Magnet (${sukebei.seed})
</a>
`;
}
const grid = document.getElementById('fc2-preview-grid');
let mediaHtml = '';
// 如果有獲取到 Baihuse 的資料
const baihuse = await API.getBaihuse(fc2code);
baihuse.videos.forEach(src => {
mediaHtml += `<video src="${src}" class="fc2-media" autoplay loop muted playsinline controls></video>`;
});
baihuse.images.forEach(src => {
mediaHtml += `<img src="${src}" class="fc2-media" loading="lazy" />`;
});
if (!mediaHtml) {
grid.innerHTML = `<div style="color: gray; padding: 20px; text-align: center; grid-column: 1 / -1;">暫無可用的預覽圖片或影片</div>`;
} else {
grid.innerHTML = mediaHtml;
}
},
init() {
this.renderList();
if (location.href.includes('/articles/')) {
this.renderDetail();
}
}
};
// =========================================================================
// 4. SPA 動態路由與渲染監聽 (防抖處理)
// =========================================================================
let lastUrl = location.href;
let renderTimeout;
const observer = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
const oldPanel = document.getElementById('fc2-custom-panel');
if (oldPanel) oldPanel.remove();
setTimeout(() => App.init(), 300);
} else {
// 防抖:當滾動載入更多時,DOM 會瘋繁變更。延遲 200 毫秒才執行,降低效能開銷。
clearTimeout(renderTimeout);
renderTimeout = setTimeout(() => {
App.renderList();
}, 200);
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => App.init(), 500);
})();