Greasy Fork

来自缓存

Anime1 本地收藏夹

站内收藏管理功能(支持集数保存)

// ==UserScript==
// @name         Anime1 本地收藏夹
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  站内收藏管理功能(支持集数保存)
// @author       zhist
// @match        https://anime1.me/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 样式配置
    GM_addStyle(`
        .anime1-collect-btn {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 9999;
            padding: 10px 15px;
            background: #ff4757;
            color: white;
            border: none;
            border-radius: 25px;
            cursor: pointer;
            box-shadow: 0 3px 10px rgba(255,71,87,0.4);
            transition: all 0.3s;
            font-weight: bold;
        }
        .anime1-collect-btn:hover {
            transform: scale(1.05);
            background: #ff6b81;
        }
        .bookmarks-panel {
            position: fixed;
            top: 70px;
            right: 20px;
            width: 350px;
            background: rgba(255,255,255,0.95);
            border-radius: 10px;
            box-shadow: 0 8px 30px rgba(0,0,0,0.12);
            backdrop-filter: blur(10px);
            padding: 15px;
            display: none;
            max-height: 70vh;
            overflow-y: auto;
        }
        .bookmark-item {
            display: flex;
            align-items: center;
            padding: 12px;
            margin: 8px 0;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 6px rgba(0,0,0,0.08);
            transition: transform 0.2s;
        }
        .bookmark-item:hover {
            transform: translateX(5px);
        }
        .bookmark-content {
            flex: 1;
            overflow: hidden;
        }
        .bookmark-link {
            display: block;
            color: #2f3542;
            text-decoration: none;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            font-weight: 500;
        }
        .bookmark-episode {
            font-size: 0.85em;
            color: #ff6b81;
            margin-top: 2px;
            font-weight: bold;
        }
        .bookmark-time {
            font-size: 0.75em;
            color: #999;
            margin-top: 2px;
        }
        .delete-btn {
            color: #ff4757;
            cursor: pointer;
            margin-left: 10px;
            padding: 3px;
            border-radius: 50%;
            width: 22px;
            height: 22px;
            text-align: center;
            line-height: 22px;
            flex-shrink: 0;
        }
        .delete-btn:hover {
            background: #ffe4e6;
        }
        .episode-indicator {
            background: #ff6b81;
            color: white;
            padding: 2px 6px;
            border-radius: 10px;
            font-size: 0.75em;
            margin-left: 8px;
            font-weight: bold;
        }
    `);

    const watchedSet = new Set();

    // 存储系统初始化
    const STORAGE_KEY = 'anime1_bookmarks';
    let bookmarks = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    let currentEpisode = null;


    const btn = createCollectButton();

    // 保存收藏数据
    function saveBookmarks() {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
    }

    function setCurrentEpisode(episode) {
        if (!watchedSet.has(episode)) {
            watchedSet.add(episode);
            currentEpisode = episode;
            console.log('更新currentEpisode:', episode);
            updateButtonText(btn);
            updateEpisode(btn)
        }
    }

    // 从URL中提取集数
    // function extractEpisodeFromUrl(url) {
    //     // 匹配类似 https://hajime.v.anime1.me/1623/9.mp4 的格式
    //     const match = url.match(/\/(\d+)\.mp4$/);
    //     return match ? parseInt(match[1]) : null;
    // }
    // 从URL中提取集数
    function extractEpisodeFromUrl(url) {
        // 匹配类似以下格式的URL:
        // https://hajime.v.anime1.me/1623/9.mp4 → 集数: 9
        // https://bocchi.v.anime1.me/1655/8b.mp4 → 集数: 8
        // 格式: /数字/集数数字+其它什么.mp4,但只提取开头的数字部分
        const match = url.match(/\/\d+\/(\d+)[^\/]*\.mp4$/);
        return match ? parseInt(match[1]) : null;
    }

    // 监听所有网络请求来捕获视频URL
    function interceptNetworkRequests() {
        // 拦截XMLHttpRequest
        const originalXHROpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function (method, url, ...args) {
            if (url.includes('.mp4') && url.includes('anime1.me')) {
                const episode = extractEpisodeFromUrl(url);
                if (episode !== null) {
                    setCurrentEpisode(episode);
                    console.log('检测到视频集数:', episode);
                }
            }
            return originalXHROpen.call(this, method, url, ...args);
        };

        // 拦截fetch请求
        const originalFetch = window.fetch;
        window.fetch = function (url, ...args) {
            if (typeof url === 'string' && url.includes('.mp4') && url.includes('anime1.me')) {
                const episode = extractEpisodeFromUrl(url);
                if (episode !== null) {
                    setCurrentEpisode(episode);
                    console.log('检测到视频集数:', episode);
                    updateEpisodeIfSaved(episode)
                }
            }
            return originalFetch.call(this, url, ...args);
        };
    }

    // 监听所有网络流量(包括video标签的src变化)
    function monitorVideoElements() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
                    const target = mutation.target;
                    if (target.tagName === 'VIDEO' || target.tagName === 'SOURCE') {
                        const src = target.src || target.getAttribute('src');
                        if (src && src.includes('.mp4') && src.includes('anime1.me')) {
                            const episode = extractEpisodeFromUrl(src);
                            if (episode !== null) {
                                setCurrentEpisode(episode);
                                console.log('检测到视频集数:', episode);
                            }
                        }
                    }
                }

                // 检查新增的video元素
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === 1) { // Element node
                            const videos = node.tagName === 'VIDEO' ? [node] : node.querySelectorAll('video');
                            videos.forEach((video) => {
                                if (video.src && video.src.includes('.mp4') && video.src.includes('anime1.me')) {
                                    const episode = extractEpisodeFromUrl(video.src);
                                    if (episode !== null) {
                                        setCurrentEpisode(episode);
                                        console.log('检测到视频集数:', episode);
                                    }
                                }
                            });
                        }
                    });
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['src']
        });
    }

    // 定期检查页面中的视频元素
    function checkExistingVideos() {
        const videos = document.querySelectorAll('video, source');
        videos.forEach((video) => {
            const src = video.src || video.getAttribute('src');
            if (src && src.includes('.mp4') && src.includes('anime1.me')) {
                const episode = extractEpisodeFromUrl(src);
                if (episode !== null) {
                    setCurrentEpisode(episode);
                    // console.log('检测到视频集数:', episode);

                }
            }
        });
    }

    // 判断当前是否视频页面
    function isVideoPage() {
        return location.pathname.includes('/category');
    }

    // 判断当前视频是否被藏
    function isFavorited() {
        const currentUrl = location.href;
        const existingIndex = bookmarks.findIndex(b => b.url === currentUrl);
        return existingIndex;
    }

    // 创建收藏按钮
    function createCollectButton() {
        const btn = document.createElement('button');
        btn.className = 'anime1-collect-btn';
        updateButtonText(btn);
        return btn;
    }

    // 更新按钮文本
    function updateButtonText(btn) {
        if (isVideoPage()) {
            if (isFavorited() === -1) {
                const episodeText = currentEpisode ? ` (第${currentEpisode}集)` : '';
                btn.innerHTML = `⭐ 收藏本视频${episodeText}`;
            } else {
                btn.innerHTML = '📚 查看收藏夹';
            }
        } else {
            btn.innerHTML = '📚 查看收藏夹';
        }
    }

    // 创建收藏面板
    function createBookmarkPanel() {
        const panel = document.createElement('div');
        panel.className = 'bookmarks-panel';
        return panel;
    }

    // 格式化时间
    function formatTime(timeString) {
        const date = new Date(timeString);
        return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
    }

    // 渲染收藏列表
    function renderBookmarks(panel) {
        panel.innerHTML = bookmarks.length ?
            bookmarks.map((b, i) => `
                <div class="bookmark-item">
                    <div class="bookmark-content">
                        <a href="${b.url}" class="bookmark-link" target="_blank">${b.title}</a>
                        ${b.episode ? `<div class="bookmark-episode">第 ${b.episode} 集</div>` : ''}
                        <div class="bookmark-time">${formatTime(b.time)}</div>
                    </div>
                    <div class="delete-btn" data-index="${i}">×</div>
                </div>
            `).join('') :
            '<div style="text-align:center; color:#666; padding: 20px;">暂无收藏内容</div>';
    }

    // 更新集数
    function updateEpisode(btn) {
        let existingIndex, r1, r2, r3, r4;
        // 更新现有收藏的集数信息
        // console.log('try to update episode : ' + currentEpisode)
        if ((r1 = currentEpisode) &&
            // (r2=!watchedSet.has(currentEpisode)) &&
            (r3 = (existingIndex = isFavorited()) !== -1) &&
            (r4 = bookmarks[existingIndex].episode !== currentEpisode)) {
            bookmarks[existingIndex].episode = currentEpisode;
            bookmarks[existingIndex].time = new Date().toISOString(); // 更新时间
            saveBookmarks();
            btn.innerHTML = `🔄 已更新至第${currentEpisode}集!`;
            setTimeout(() => updateButtonText(btn), 2000);
        }
        console.log(r1, r2, r3, r4)
        // else {
        //     btn.innerHTML = '⚠️ 已存在!';
        //     setTimeout(() => updateButtonText(btn), 1000);
        // }
    }

    // 主逻辑
    function init() {
        // 启动网络监听
        interceptNetworkRequests();
        monitorVideoElements();

        // 定期检查视频元素
        setInterval(checkExistingVideos, 2000);

        const container = document.createElement('div');
        const panel = createBookmarkPanel();

        document.body.appendChild(btn);
        document.body.appendChild(panel);

        // 定期更新按钮文本(当检测到新集数时)
        // setInterval(() => {
        updateButtonText(btn);
        updateEpisode(btn)
        // }, 1000);

        // 按钮点击事件
        btn.addEventListener('click', () => {
            if (isVideoPage()) {
                // 收藏当前页面
                const currentUrl = location.href;
                const currentTitle = document.title;

                const existingIndex = bookmarks.findIndex(b => b.url === currentUrl);

                if (existingIndex === -1) {
                    // 新增收藏
                    bookmarks.unshift({
                        title: currentTitle,
                        url: currentUrl,
                        episode: currentEpisode,
                        time: new Date().toISOString()
                    });
                    saveBookmarks();
                    btn.innerHTML = `✅ 已收藏!${currentEpisode ? ` (第${currentEpisode}集)` : ''}`;
                    setTimeout(() => updateButtonText(btn), 1500);
                } else {
                    panel.style.display = panel.style.display === 'block' ? 'none' : 'block';
                    if (panel.style.display === 'block') renderBookmarks(panel);
                }
            } else {
                // 切换收藏面板
                panel.style.display = panel.style.display === 'block' ? 'none' : 'block';
                if (panel.style.display === 'block') renderBookmarks(panel);
            }
        });

        // 删除功能
        panel.addEventListener('click', (e) => {
            if (e.target.classList.contains('delete-btn')) {
                const index = parseInt(e.target.dataset.index);
                bookmarks.splice(index, 1);
                saveBookmarks();
                renderBookmarks(panel);
                updateButtonText(btn);
            }
        });

        // 点击外部关闭
        document.addEventListener('click', (e) => {
            if (!btn.contains(e.target) && !panel.contains(e.target)) {
                panel.style.display = 'none';
            }
        });
    }

    // 等待页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();