FC2CMADB-improved

參考Duckee KememChan的fc2腳本用AI重構

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);

})();