全局动画搜索与追番助手

🎯 专业动画追番神器!一键搜索动画资源,智能收藏管理,个性化追番体验。快捷键说明:[Shift+F]呼出搜索 | [Shift+C]收藏当前动画 | [Shift+D]管理收藏夹

// ==UserScript==
// @name         全局动画搜索与追番助手
// @namespace    http://tampermonkey.net/
// @version      0.1.1
// @description  🎯 专业动画追番神器!一键搜索动画资源,智能收藏管理,个性化追番体验。快捷键说明:[Shift+F]呼出搜索 | [Shift+C]收藏当前动画 | [Shift+D]管理收藏夹
// @author       Aomine
// @match        *://*/*
// @icon         data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'><text x='0' y='24' font-size='24'>🔍 </text></svg>
// @license      GPL License
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // 创建搜索框HTML结构
    const searchHTML = `
    <div id="search-overlay"></div>
    <div id="global-search-container">
        <div class="search-header">
            <h2 class="search-title">动画资源搜索</h2>
            <button class="close-btn">&times;</button>
        </div>
        <div class="search-input-group">
            <input type="text" id="search-input" placeholder="输入动画名称..." autocomplete="off">
            <button id="search-btn">
                <svg class="search-icon" viewBox="0 0 24 24">
                    <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
                </svg>
            </button>
        </div>
        <div class="engine-selector">
            <label class="engine-label">选择搜索引擎:</label>
            <select id="engine-select">
                <option value="0">次元城</option>
                <option value="1">稀饭动漫</option>
                <option value="2">MuteFun</option>
                <option value="3">咕咕番</option>
                <option value="4">NT动漫</option>
                <option value="5">风铃动漫</option>
                <option value="6">喵物次元</option>
                <option value="7">Bangumi评分</option>
            </select>
        </div>
        <div class="search-footer">
            按 <span class="search-hotkey">ESC</span> 关闭 | 按 <span class="search-hotkey">Enter</span> 搜索
        </div>
    </div>
    `;

    // 创建CSS样式
    const css = `
    #global-search-container {
        position: fixed;
        top: 20%;
        left: 50%;
        transform: translateX(-50%);
        z-index: 999999;
        background: rgba(255, 255, 255, 0.95);
        border-radius: 12px;
        box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
        backdrop-filter: blur(10px);
        border: 1px solid rgba(255, 255, 255, 0.18);
        padding: 20px;
        width: 550px;
        max-width: 90%;
        display: none;
        animation: pop-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }

    #search-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.5);
        backdrop-filter: blur(3px);
        z-index: 999998;
        display: none;
    }

    @keyframes pop-in {
        0% { opacity: 0; transform: translate(-50%, -20px); }
        100% { opacity: 1; transform: translate(-50%, 0); }
    }

    .search-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 15px;
    }

    .search-title {
        font-size: 20px;
        font-weight: 600;
        color: #2c3e50;
        margin: 0;
    }

    .close-btn {
        background: none;
        border: none;
        font-size: 24px;
        cursor: pointer;
        color: #7f8c8d;
        transition: color 0.2s;
    }

    .close-btn:hover {
        color: #e74c3c;
    }

    .search-input-group {
        display: flex;
        margin-bottom: 15px;
        border-radius: 30px;
        overflow: hidden;
        box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
    }

    #search-input {
        flex: 1;
        padding: 15px 20px;
        border: none;
        outline: none;
        font-size: 16px;
        background: #f8f9fa;
        color: #000000 !important;
        caret-color: #3498db !important;
    }

    #search-btn {
        background: #3498db;
        border: none;
        padding: 0 25px;
        cursor: pointer;
        transition: background 0.3s;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    #search-btn:hover {
        background: #2980b9;
    }

    .search-icon {
        width: 22px;
        height: 22px;
        fill: white;
    }

    .engine-selector {
        display: flex;
        flex-direction: column;
        margin-top: 15px;
    }

    .engine-label {
        font-size: 14px;
        margin-bottom: 8px;
        color: #34495e;
        font-weight: 500;
    }

    #engine-select {
        padding: 12px 15px;
        border-radius: 8px;
        border: 1px solid #ddd;
        background: #f8f9fa;
        font-size: 15px;
        outline: none;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
        cursor: pointer;
        width: 100%;
    }

    #engine-select:focus {
        border-color: #3498db;
        box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
    }

    .search-footer {
        margin-top: 15px;
        font-size: 13px;
        color: #7f8c8d;
        text-align: center;
        padding-top: 10px;
        border-top: 1px solid #eee;
    }

    .search-hotkey {
        background: #f1f2f6;
        padding: 2px 6px;
        border-radius: 4px;
        font-weight: 600;
    }
    `;

    // 将样式和HTML添加到文档
    document.head.insertAdjacentHTML('beforeend', `<style>${css}</style>`);
    document.body.insertAdjacentHTML('beforeend', searchHTML);

    // 搜索引擎列表
    const searchEngines = [
        {
            name: "次元城动画",
            url: "https://www.cycani.org/search.html?wd=${name}"
        },
        {
            name: "稀饭动漫",
            url: "https://dm.xifanacg.com/search.html?wd=${name}"
        },
        {
            name: "MuteFun",
            url: "https://www.mutean.com/vodsearch/${name}-------------.html"
        },
        {
            name: "咕咕番",
            url: "https://www.gugu3.com/index.php/vod/search.html?wd=${name}"
        },
        {
            name: "NT动漫",
            url: "http://www.ntdm8.com/search/-------------.html?wd=${name}&page=1"
        },
        {
            name: "风铃动漫",
            url: "https://www.bbfun.cc/#/search?wd=${name}"
        },
        {
            name: "喵物次元",
            url: "https://www.mwcy.net/search.html?wd=${name}"
        },
        {
            name: "Bangumi评分",
            url: "https://bangumi.tv/subject_search/${name}?cat=2"
        }
    ];

    // 获取DOM元素
    const searchContainer = document.getElementById('global-search-container');
    const searchOverlay = document.getElementById('search-overlay');
    const searchInput = document.getElementById('search-input');
    const searchBtn = document.getElementById('search-btn');
    const engineSelect = document.getElementById('engine-select');
    const closeBtn = document.querySelector('.close-btn');

    // 显示搜索框
    function showSearch() {
        searchContainer.style.display = 'block';
        searchOverlay.style.display = 'block';
        searchInput.focus();
        document.body.style.overflow = 'hidden';
    }

    // 隐藏搜索框
    function hideSearch() {
        searchContainer.style.display = 'none';
        searchOverlay.style.display = 'none';
        searchInput.value = '';
        document.body.style.overflow = '';
    }

    // 执行搜索
    function performSearch() {
        const searchTerm = searchInput.value.trim();
        if (!searchTerm) return;

        const selectedEngine = searchEngines[engineSelect.value];
        const encodedTerm = encodeURIComponent(searchTerm);
        const searchUrl = selectedEngine.url.replace('${name}', encodedTerm);

        window.open(searchUrl, '_blank');
        hideSearch();
    }

    // 事件监听
    document.addEventListener('keydown', function(e) {
        // Shift + F 打开搜索框
        if (e.shiftKey && e.key === 'F') {
            e.preventDefault();
            showSearch();
        }

        // ESC 关闭搜索框
        if (e.key === 'Escape' && searchContainer.style.display === 'block') {
            hideSearch();
        }

        // 在搜索框中按Enter搜索
        if (e.key === 'Enter' && document.activeElement === searchInput && searchContainer.style.display === 'block') {
            performSearch();
        }
    });

    searchBtn.addEventListener('click', performSearch);
    closeBtn.addEventListener('click', hideSearch);
    searchOverlay.addEventListener('click', hideSearch);
})();


(function () {
    'use strict';

    /* ========================= 白名单配置 ========================= */
    const whitelist = [
        "https://www.gugu3.com/index.php/vod/play/id",
        "https://www.ntdm8.com/play",
        "https://www.cycani.org/watch",
        "https://dm.xifanacg.com/watch",
        "https://www.aafun.cc/f",
        "https://www.mwcy.net/play",
        "https://www.mutean.com/vodplay",
    ];

    const STORAGE_KEY = 'anime_favorites_v2';
    const POS_KEY = 'anime_fav_panel_pos_v2';
    const NOISE_WORDS = [
        '免费在线观看','在线观看','高清','超清','原声','全集','无广告','在线播放',
        '高清版','未删减','官方','官网','弹幕','字幕','BT','迅雷','下载','观看'
    ];

    /* ========================= Title 捕获 ========================= */
    let currentTitle = (function() {
        try {
            let tnode = document.querySelector && document.querySelector('title');
            return (tnode && tnode.textContent || document.title || '').trim();
        } catch(e) {
            return document.title || '';
        }
    })();

    const titleObserver = new MutationObserver(() => {
        currentTitle = document.title;
    });

    const tNode = document.querySelector('title');
    if(tNode) {
        titleObserver.observe(tNode, { subtree: true, characterData: true, childList: true });
    }

    /* ========================= 数据存储 ========================= */
    function getFavorites() {
        try { return JSON.parse(GM_getValue(STORAGE_KEY, '[]')); }
        catch(e) { return []; }
    }

    function saveFavorites(list) {
        GM_setValue(STORAGE_KEY, JSON.stringify(list));
    }

    function savePanelPos(pos) {
        GM_setValue(POS_KEY, JSON.stringify(pos));
    }

    function loadPanelPos() {
        try { return JSON.parse(GM_getValue(POS_KEY, 'null')); }
        catch(e) { return null; }
    }

    /* ========================= 标题处理 ========================= */
    function cleanRawTitle(t) {
        if(!t) return '';
        t = t.replace(/【.*?】|\[.*?\]|\(.*?\)|(.*?)/g, '');
        NOISE_WORDS.forEach(w => { t = t.replace(new RegExp(w,'gi'), ''); });
        t = t.replace(/\s*[-|_|—|–|_]\s*[^-_|—–_]{1,50}$/g, '');
        t = t.replace(/\s+/g, ' ').trim();
        return t;
    }

    function extractSeriesAndEpisode(rawTitle) {
        const t = cleanRawTitle(rawTitle || '');
        let m = t.match(/(.+?)\s*(第[\d一二三四五六七八九十百千]+[集话回])/i);
        if(m) return { full: t, series: m[1].trim(), episode: m[2].trim() };
        return { full: t, series: t.trim(), episode: '' };
    }

    function normalizeSeriesName(name) {
        return (name||'').replace(/\s+/g,'').replace(/[^\w\u4e00-\u9fa5]/g,'').toLowerCase();
    }

    function getCurrentTitle() { return currentTitle || document.title || ''; }

    /* ========================= 创建UI ========================= */
    const panel = document.createElement('div');
    panel.id = 'animeFavPanel';
    panel.style.cssText = `
        position: fixed;
        top: 60px;
        right: 20px;
        width: 320px;
        max-height: 72vh;
        overflow: hidden;
        background: rgba(255,255,255,0.96);
        border: 1px solid rgba(0,0,0,0.12);
        border-radius: 12px;
        box-shadow: 0 6px 20px rgba(0,0,0,0.12);
        padding: 8px;
        font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial;
        font-size: 13px;
        color: #111;
        display: none;
        z-index: 2147483646;
        user-select: none;
    `;
    document.body.appendChild(panel);

    // header
    const header = document.createElement('div');
    header.style.cssText = `
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 6px 4px;
        cursor: grab;
    `;

    const titleText = document.createElement('strong');
    titleText.textContent = '🎞 我的收藏';
    header.appendChild(titleText);

    const closeBtn = document.createElement('button');
    closeBtn.textContent = '✖';
    closeBtn.style.cssText = `
        border: none;
        background: transparent;
        color: #888;
        font-size: 14px;
        cursor: pointer;
        padding: 2px 6px;
    `;
    closeBtn.onclick = () => { panel.style.display = 'none'; };
    header.appendChild(closeBtn);
    panel.appendChild(header);

    // list
    const listWrap = document.createElement('div');
    listWrap.style.cssText = `
        overflow: auto;
        max-height: 56vh;
        padding-right: 6px;
    `;

    const list = document.createElement('div');
    list.id = 'animeFavList';
    list.style.cssText = `
        display: flex;
        flex-direction: column;
        gap: 6px;
        padding: 6px;
    `;

    listWrap.appendChild(list);
    panel.appendChild(listWrap);

    // footer
    const footer = document.createElement('div');
    footer.style.cssText = `
        padding: 6px 4px;
        border-top: 1px solid rgba(0,0,0,0.04);
        font-size: 12px;
        color: #666;
    `;
    footer.innerHTML = `
        <div>快捷键:</div>
        <ul style="margin:4px 0;padding-left:18px;">
            <li>Shift+C 显示/隐藏收藏栏</li>
            <li>Shift+D 收藏当前番剧(仅白名单页)</li>
        </ul>
    `;
    panel.appendChild(footer);

    /* ========================= 拖动功能 ========================= */
    (function makeDraggable(handle, target) {
        let dragging = false;
        let startX = 0;
        let startY = 0;
        let startLeft = 0;
        let startTop = 0;

        handle.addEventListener('mousedown', e => {
            if (e.button !== 0) return;

            dragging = true;
            startX = e.clientX;
            startY = e.clientY;

            if (!target.style.left) target.style.left = target.getBoundingClientRect().left + 'px';
            startLeft = parseFloat(target.style.left);
            startTop = parseFloat(target.style.top || target.getBoundingClientRect().top);

            target.style.right = 'auto';

            //在document上强制设置鼠标样式
            document.body.style.cursor = 'grabbing';
            handle.style.cursor = 'grabbing';

            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onUp);
            e.preventDefault();
        });

        function onMove(e) {
            if (!dragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;

            const newLeft = Math.max(6, Math.min(window.innerWidth - target.offsetWidth - 6, startLeft + dx));
            const newTop = Math.max(6, Math.min(window.innerHeight - target.offsetHeight - 6, startTop + dy));

            target.style.left = newLeft + 'px';
            target.style.top = newTop + 'px';
        }

        function onUp() {
            dragging = false;

            //拖动结束时恢复默认鼠标样式
            document.body.style.cursor = '';
            handle.style.cursor = 'grab';

            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('mouseup', onUp);
            savePanelPos({ left: parseFloat(target.style.left), top: parseFloat(target.style.top) });
        }
    })(header, panel);
    /* ========================= 渲染收藏列表 ========================= */

    // 获取网站名称的辅助函数
    function getSiteNameByUrl(url) {
        const defaultName = "未知来源";
        const engines = [
            { name: "次元城动画", url: "https://www.cycani.org" },
            { name: "稀饭动漫", url: "https://dm.xifanacg.com" },
            { name: "MuteFun", url: "https://www.mutean.com" },
            { name: "咕咕番", url: "https://www.gugu3.com" },
            { name: "NT动漫", url: "http://www.ntdm8.com" },
            { name: "风铃动漫", url: "https://www.bbfun.cc" },
            { name: "喵物次元", url: "https://www.mwcy.net" }
        ];

        for (const engine of engines) {
            if (url.startsWith(engine.url)) {
                return engine.name;
            }
        }
        return defaultName;
    }

    function renderList() {
        list.innerHTML = '';
        const data = getFavorites();
        data.sort((a, b) => (b.ts || 0) - (a.ts || 0));
        data.forEach((item, idx) => {
            const row = document.createElement('div');
            row.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 6px;
            border-radius: 8px;
        `;

            const left = document.createElement('div');
            left.style.cssText = `
            flex: 1;
            min-width: 0;
        `;

            const a = document.createElement('a');
            a.textContent = item.title;
            a.href = item.url;
            a.target = '_blank';
            a.title = item.title;
            a.style.cssText = `
            display: block;
            text-decoration: none;
            color: #111;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        `;

            const meta = document.createElement('div');
            const siteName = getSiteNameByUrl(item.url);
            const timestamp = item.ts ? (new Date(item.ts)).toLocaleString() : '';
            meta.textContent = `[${siteName}] ${timestamp}`;
            meta.style.cssText = `
            font-size: 11px;
            color: #777;
            margin-top: 4px;
        `;

            left.appendChild(a);
            left.appendChild(meta);

            const btns = document.createElement('div');
            btns.style.cssText = `
            display: flex;
            flex-direction: column;
            gap: 6px;
            margin-left: 8px;
        `;

            const del = document.createElement('button');
            del.textContent = '删除';
            del.style.cssText = `
            border: none;
            background: #e24;
            color: #fff;
            padding: 4px 8px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 12px;
        `;
            del.onclick = () => {
                const arr = getFavorites();
                arr.splice(idx, 1);
                saveFavorites(arr);
                renderList();
            };

            btns.appendChild(del);
            row.appendChild(left);
            row.appendChild(btns);
            list.appendChild(row);
        });
    }
    /* ========================= 收藏逻辑 ========================= */
    function urlInWhitelist(url) {
        return whitelist.some(w => url.startsWith(w));
    }

    function collectCurrentPage() {
        if (!urlInWhitelist(location.href)) {
            alert('当前页面不在收藏白名单,无法收藏');
            return;
        }

        const url = location.href;
        const parsed = extractSeriesAndEpisode(getCurrentTitle());
        const newTitle = parsed.episode ? `${parsed.series} ${parsed.episode}` : parsed.full;
        const newSeriesNorm = normalizeSeriesName(parsed.series);

        let arr = getFavorites();

        // 尝试通过番剧名查找已存在的收藏
        let idx = arr.findIndex(it => normalizeSeriesName(extractSeriesAndEpisode(it.title).series) === newSeriesNorm);

        // 如果未找到,尝试通过 URL 的相似性来查找
        if (idx === -1) {
            // 提取 URL 的主干部分进行比较,例如:
            // "https://dm.xifanacg.com/watch/3272/1/"
            const baseUrl = url.replace(/\/\d+\/\d+\.html$/, '/');
            idx = arr.findIndex(it => it.url.startsWith(baseUrl));
        }

        if (idx > -1) {
            const existing = arr[idx];
            if (existing.url === url) {
                alert('该页面已收藏,无需重复添加:' + newTitle);
                return;
            }

            if (confirm(`检测到已有该番剧收藏:\n${existing.title}\n是否用当前页面更新为:\n${newTitle} ?`)) {
                arr.splice(idx, 1, { title: newTitle, url, ts: Date.now() });
                saveFavorites(arr);
                renderList();
                alert('已更新收藏并置顶:' + newTitle);
            }
            return; // 取消则不操作
        }

        // 新收藏,防止重复 URL
        if (!arr.some(it => it.url === url)) {
            arr.unshift({ title: newTitle, url, ts: Date.now() });
            saveFavorites(arr);
            renderList();
            alert('已收藏:' + newTitle);
        }
    }
    /* ========================= 快捷键 ========================= */
    document.addEventListener('keydown', e => {
        const tgt = e.target;
        const isTyping = tgt && (tgt.tagName === 'INPUT' || tgt.tagName === 'TEXTAREA' || tgt.isContentEditable);
        if(isTyping) return;

        if(e.shiftKey && e.code === 'KeyC') {
            panel.style.display = (panel.style.display === 'none' || panel.style.display === '') ? 'block' : 'none';
            renderList();
            return;
        }

        if(e.shiftKey && e.code === 'KeyD') {
            e.preventDefault();
            collectCurrentPage();
            return;
        }
    });

})();