B站动态视频添加到稍后观看

自动获取并播放B站动态视频

// ==UserScript==
// @name         B站动态视频添加到稍后观看
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  自动获取并播放B站动态视频
// @author       Your name
// @match        *://t.bilibili.com/*
// @match        *://www.bilibili.com/*
// @match        *://www.bilibili.com/video/*
// @grant        GM_xmlhttpRequest
// @connect      api.bilibili.com
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 常量定义
    const CONSTANTS = {
        API: {
            WATCH_LATER: 'https://api.bilibili.com/x/v2/history/toview/web',
            DYNAMIC_FEED: 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all',
            ADD_TO_WATCH: 'https://api.bilibili.com/x/v2/history/toview/add',
        },
        STORAGE_KEY: 'BILIBILI_ADDED_VIDEOS',
        MAX_PAGES: 20,
        REQUEST_DELAY: 300,
        STORAGE_EXPIRE_DAYS: 7,
    };

    // 优化的 StorageManager
    const StorageManager = {
        setWithExpiry(key, value, days = CONSTANTS.STORAGE_EXPIRE_DAYS) {
            const item = {
                value,
                expiry: new Date().getTime() + (days * 24 * 60 * 60 * 1000),
            }
            try {
                localStorage.setItem(key, JSON.stringify(item));
                return true;
            } catch (error) {
                console.error('存储数据失败:', error);
                return false;
            }
        },

        getWithExpiry(key) {
            try {
                const itemStr = localStorage.getItem(key);
                if (!itemStr) return null;

                const item = JSON.parse(itemStr);
                const now = new Date().getTime();

                if (now > item.expiry) {
                    localStorage.removeItem(key);
                    return null;
                }
                return item.value;
            } catch (error) {
                console.error('读取数据失败:', error);
                return null;
            }
        },

        clearExpired(key) {
            const item = this.getWithExpiry(key);
            if (!item) {
                console.log('数据已过期或不存在,已清除');
            }
        }
    };

    // 工具函数
    const utils = {
        async sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        },

        async retry(fn, times = 3, delay = 1000) {
            for (let i = 0; i < times; i++) {
                try {
                    return await fn();
                } catch (err) {
                    if (i === times - 1) throw err;
                    console.log(`操作失败,${delay/1000}秒后重试:`, err);
                    await this.sleep(delay);
                }
            }
        }
    };

    // API 请求封装
    const api = {
        // 获取 CSRF token
        getCsrfToken() {
            const cookies = document.cookie.split(';');
            for (const cookie of cookies) {
                const [name, value] = cookie.trim().split('=');
                if (name === 'bili_jct') {
                    return value;
                }
            }
            return '';
        },

        async request(url, options = {}) {
            const defaultOptions = {
                credentials: 'include',
                headers: {
                    'Accept': 'application/json',
                    'Cache-Control': 'no-cache'
                }
            };

            // 如果是 POST 请求,添加 CSRF token
            if (options.method === 'POST') {
                const csrf = this.getCsrfToken();
                if (!csrf) {
                    throw new Error('未找到 CSRF token,请确保已登录');
                }

                // 处理表单数据
                if (options.body) {
                    options.body += `&csrf=${csrf}`;
                } else {
                    options.body = `csrf=${csrf}`;
                }

                // 设置 Content-Type
                if (!options.headers) {
                    options.headers = {};
                }
                options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
            }

            const response = await fetch(url, { ...defaultOptions, ...options });
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const data = await response.json();
            if (data.code !== 0) {
                throw new Error(`API error! code: ${data.code}, message: ${data.message}`);
            }

            return data.data;
        },

        async getWatchLaterList() {
            try {
                const data = await this.request(CONSTANTS.API.WATCH_LATER);
                return new Set(data.list?.map(item => item.aid.toString()) || []);
            } catch (error) {
                console.error('获取稍后观看列表失败:', error);
                return new Set();
            }
        },

        async addToWatchLater(aid) {
            try {
                const csrf = this.getCsrfToken();
                if (!csrf) {
                    console.error('未登录状态,请先登录');
                    return false;
                }

                const response = await this.request(CONSTANTS.API.ADD_TO_WATCH, {
                    method: 'POST',
                    body: `aid=${aid}&csrf=${csrf}`
                });
                return true;
            } catch (error) {
                console.error('添加失败:', error);
                if (error.message.includes('未登录')) {
                    alert('请先登录 B 站账号!');
                }
                return false;
            }
        }
    };

    // 主要业务逻辑
    class VideoManager {
        constructor() {
            this.videoList = [];
            this.currentPage = 1;
            this.lastOffset = '';
            this.watchLaterList = null; // 新增:缓存稍后观看列表
        }

        // 新增:获取并缓存稍后观看列表
        async initWatchLaterList() {
            this.watchLaterList = await api.getWatchLaterList();
            console.log(`已获取稍后观看列表,共 ${this.watchLaterList.size} 个视频`);
        }

        async fetchVideos() {
            return utils.retry(async () => {
                const data = await api.request(`${CONSTANTS.API.DYNAMIC_FEED}?timezone_offset=-480&type=all&page=${this.currentPage}&offset=${this.lastOffset}`);
                
                const videos = data.items
                    .filter(item => item.modules?.module_dynamic?.major?.type === 'MAJOR_TYPE_ARCHIVE')
                    .map(item => {
                        const archive = item.modules.module_dynamic.major.archive;
                        return {
                            bvid: archive.bvid,
                            aid: archive.aid,
                            title: archive.title,
                            url: `https://www.bilibili.com/video/${archive.bvid}`
                        };
                    });

                console.log(`第 ${this.currentPage} 页找到 ${videos.length} 个视频`);
                this.videoList = this.videoList.concat(videos);
                this.lastOffset = data.offset || '';
                return this.lastOffset;
            });
        }

        async loadAllPages() {
            while (this.currentPage <= CONSTANTS.MAX_PAGES) {
                const hasMore = await this.fetchVideos();
                if (!hasMore) break;
                this.currentPage++;
                await utils.sleep(CONSTANTS.REQUEST_DELAY);
            }
        }

        async processVideos() {
            // 先获取稍后观看列表
            await this.initWatchLaterList();
            if (!this.watchLaterList) {
                console.error('获取稍后观看列表失败');
                return;
            }

            // 获取本地存储的已处理视频列表
            const processedVideos = getProcessedVideos();
            
            console.log(`当前稍后观看列表有 ${this.watchLaterList.size} 个视频`);
            console.log(`本地记录的已处理视频数: ${processedVideos.length}`);

            // 过滤需要添加的视频:既不在稍后观看列表中,也不在本地记录中
            const videosToAdd = this.videoList.filter(video => {
                const videoId = video.aid.toString();
                return !this.watchLaterList.has(videoId) && !processedVideos.includes(videoId);
            });

            console.log(`找到 ${this.videoList.length} 个视频,其中 ${videosToAdd.length} 个需要添加`);

            if (videosToAdd.length > 0) {
                console.log('即将添加的视频:');
                videosToAdd.forEach((video, index) => {
                    console.log(`${index + 1}. ${video.title}`);
                });
            }

            let successCount = 0;
            for (const video of videosToAdd) {
                console.log(`正在添加: ${video.title}`);
                if (await api.addToWatchLater(video.aid)) {
                    successCount++;
                    addToProcessedVideos(video.aid.toString());
                    console.log(`✅ 成功添加: ${video.title}`);
                } else {
                    console.log(`❌ 添加失败: ${video.title}`);
                }
                await utils.sleep(CONSTANTS.REQUEST_DELAY);
            }

            return {
                total: this.videoList.length,
                added: successCount,
                existing: this.videoList.length - videosToAdd.length
            };
        }
    }

    // UI 组件
    const UI = {
        createButton() {
            const button = document.createElement('div');
            button.innerHTML = `
                <div style="
                    position: fixed;
                    right: 20px;
                    top: 200px;
                    z-index: 999;
                    width: 32px;
                    height: 32px;
                    background: white;
                    border-radius: 50%;
                    box-shadow: 0 2px 4px rgba(0,0,0,0.2);
                    cursor: pointer;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    transition: all 0.3s ease;
                    opacity: 0.8;
                ">
                    <svg viewBox="0 0 24 24" width="20" height="20">
                        <path fill="#00AEEC" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
                    </svg>
                </div>
            `;

            const buttonElement = button.firstElementChild;
            this.addButtonEffects(buttonElement);
            document.body.appendChild(button);
        },

        addButtonEffects(button) {
            button.addEventListener('mouseover', () => {
                button.style.opacity = '1';
                button.style.transform = 'scale(1.1)';
            });
            
            button.addEventListener('mouseout', () => {
                button.style.opacity = '0.8';
                button.style.transform = 'scale(1)';
            });

            button.addEventListener('click', async () => {
                button.style.pointerEvents = 'none';
                button.style.opacity = '0.5';
                
                try {
                    const manager = new VideoManager();
                    await manager.loadAllPages();
                    const result = await manager.processVideos();
                    
                    console.log('\n处理完成:');
                    console.log(`✅ 成功添加: ${result.added} 个视频`);
                    console.log(`⏭️ 已在列表中: ${result.existing} 个视频`);
                    console.log(`📊 动态中总视频数: ${result.total}`);
                } catch (error) {
                    console.error('执行失败:', error);
                } finally {
                    button.style.pointerEvents = 'auto';
                    button.style.opacity = '0.8';
                }
            });
        }
    };

    // 初始化
    UI.createButton();

    function getProcessedVideos() {
        const stored = localStorage.getItem(CONSTANTS.STORAGE_KEY);
        return stored ? JSON.parse(stored) : [];
    }

    function addToProcessedVideos(videoId) {
        const processed = getProcessedVideos();
        if (!processed.includes(videoId)) {
            processed.push(videoId);
            localStorage.setItem(CONSTANTS.STORAGE_KEY, JSON.stringify(processed));
        }
    }

    function isVideoProcessed(videoId) {
        return getProcessedVideos().includes(videoId);
    }

    // 获取已添加的视频列表
    function getAddedVideos() {
        const stored = localStorage.getItem(CONSTANTS.STORAGE_KEY);
        return stored ? JSON.parse(stored) : [];
    }

    // 添加视频ID到记录中
    function addToVideoRecord(videoId) {
        const added = getAddedVideos();
        if (!added.includes(videoId)) {
            added.push(videoId);
            localStorage.setItem(CONSTANTS.STORAGE_KEY, JSON.stringify(added));
        }
    }

    async function processVideo(item) {
        const videoId = item.modules?.module_dynamic?.major?.archive?.aid;
        if (!videoId) return;

        // 检查是否已经添加过
        const addedVideos = getAddedVideos();
        if (addedVideos.includes(videoId)) {
            console.log(`视频 ${videoId} 已经添加过,跳过`);
            return;
        }

        const isWatched = await checkIfWatched(videoId);
        if (!isWatched) {
            // 只有成功添加后才记录
            const addSuccess = await addToWatchLater(videoId);
            if (addSuccess) {
                addToVideoRecord(videoId);
                console.log(`视频 ${videoId} 添加成功并记录`);
            } else {
                console.log(`视频 ${videoId} 添加失败,不记录`);
            }
        }
    }

    // 检查视频是否已观看
    async function checkIfWatched(videoId) {
        try {
            const watchLaterList = await api.getWatchLaterList();
            return watchLaterList.has(videoId.toString());
        } catch (error) {
            console.error('检查视频状态失败:', error);
            return false;
        }
    }

    // 添加到稍后观看
    async function addToWatchLater(videoId) {
        try {
            return await api.addToWatchLater(videoId);
        } catch (error) {
            console.error('添加到稍后观看失败:', error);
            return false;
        }
    }

})();