您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
B站收藏夹视频自动分类
// ==UserScript== // @name Bilibili收藏夹自动分类 // @namespace http://tampermonkey.net/ // @version 1.4 // @description B站收藏夹视频自动分类 // @author https://space.bilibili.com/1937042029,https://github.com/jqwgt // @license GPL-3.0-or-later // @match *://space.bilibili.com/*/favlist* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect api.bilibili.com // ==/UserScript== (function() { 'use strict'; // 添加全局样式 GM_addStyle(` .bili-classifier-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: #222; } .bili-classifier-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 10000; max-height: 80vh; overflow-y: auto; width: 700px; max-width: 90vw; } .bili-classifier-modal h3 { margin-top: 0; color: #00a1d6; font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 10px; } .bili-classifier-btn { padding: 10px 16px; background: #00a1d6; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.2s; margin-right: 10px; } .bili-classifier-btn:hover { background: #0087b4; transform: translateY(-1px); } .bili-classifier-btn.secondary { background: #f0f0f0; color: #666; } .bili-classifier-btn.secondary:hover { background: #e0e0e0; } .bili-classifier-btn.danger { background: #ff4d4f; } .bili-classifier-btn.danger:hover { background: #ff7875; } .bili-classifier-group { margin: 15px 0; padding: 15px; border: 1px solid #eee; border-radius: 8px; background: #fafafa; } .bili-classifier-group-header { display: flex; justify-content: space-between; margin-bottom: 10px; } .bili-classifier-input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; width: 200px; margin-right: 10px; } .bili-classifier-select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; width: 220px; margin-right: 10px; } .bili-classifier-checkbox-group { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; margin-top: 10px; } .bili-classifier-checkbox-label { display: flex; align-items: center; cursor: pointer; } .bili-classifier-checkbox { margin-right: 8px; } .bili-classifier-footer { display: flex; justify-content: flex-end; margin-top: 20px; padding-top: 15px; border-top: 1px solid #eee; } .bili-classifier-progress { position: fixed; bottom: 30px; right: 30px; background: white; padding: 15px 20px; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 10000; min-width: 250px; } .bili-classifier-progress-bar { width: 100%; height: 10px; background: #f0f0f0; border-radius: 5px; margin: 8px 0; overflow: hidden; } .bili-classifier-progress-fill { height: 100%; background: linear-gradient(90deg, #00a1d6, #00c4ff); border-radius: 5px; transition: width 0.3s; } .bili-classifier-float-btn { position: fixed; right: 30px; bottom: 30px; z-index: 9999; display: flex; flex-direction: column; gap: 10px; } .bili-classifier-links { display: flex; gap: 10px; margin-top: 20px; } .bili-classifier-link-btn { padding: 8px 12px; background: #f0f0f0; color: #666; border-radius: 4px; text-decoration: none; font-size: 12px; display: flex; align-items: center; gap: 5px; } .bili-classifier-link-btn:hover { background: #e0e0e0; } .bili-classifier-radio-group { display: flex; gap: 15px; margin: 15px 0; } .bili-classifier-radio-label { display: flex; align-items: center; gap: 5px; cursor: pointer; } .bili-classifier-option-group { margin: 15px 0; padding: 15px; border: 1px solid #eee; border-radius: 8px; } `); // 获取CSRF令牌 function getCsrf() { return document.cookie.match(/bili_jct=([^;]+)/)?.[1] || ''; } // 添加日志功能 function log(message, type = 'info') { const styles = { info: 'color: #00a1d6', error: 'color: #ff0000', success: 'color: #00ff00' }; console.log(`%c[收藏夹分类] ${message}`, styles[type]); } // 获取用户收藏夹 async function getUserFavLists() { const mid = window.location.pathname.split('/')[1]; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid=${mid}`, responseType: 'json', onload: function(response) { resolve(response.response.data.list || []); }, onerror: reject }); }); } // 获取视频详细信息 // 获取视频详细信息 增加跳过异常 async function getVideoInfo(aid) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/web-interface/view?aid=${aid}`, responseType: 'json', onload: function(response) { if (!response.response.data) { log(`视频 ${aid} 可能已失效或无法访问,跳过处理`, 'error'); reject(new Error(`视频 ${aid} 可能已失效或无法访问`)); return; } const data = response.response.data; log(`获取视频 ${aid} 详细信息:`, 'info'); console.table({ 标题: data.title, 分区ID: data.tid, 分区名: data.tname, 播放量: data.stat.view, }); resolve(data); }, onerror: function(error) { log(`视频 ${aid} 信息获取失败,跳过处理`, 'error'); reject(error); } }); }); } // 获取收藏夹中的视频 async function getFavVideos(mediaId, ps = 20, pn = 1, videos = []) { if (!document.getElementById('reading-progress')) { createReadingProgressDiv(); } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${mediaId}&pn=${pn}&ps=${ps}&order=mtime&type=0&platform=web`, responseType: 'json', onload: async function(response) { const data = response.response.data; log(`收藏夹API返回数据:`, 'info'); console.log(data); if (!data || !data.medias) { reject('获取视频列表失败'); return; } let currentCount = videos.length; let processedCount = 0; for (let video of data.medias) { try { const videoInfo = await getVideoInfo(video.id); videos.push({ aid: video.id, title: video.title, tid: videoInfo.tid, tname: videoInfo.tname, play: videoInfo.stat.view }); currentCount++; } catch (err) { log(`跳过视频 ${video.id}: ${err.message}`, 'error'); } finally { processedCount++; updateReadingProgress(`正在读取视频,已获取 ${currentCount} 个视频,处理进度 ${processedCount}/${data.medias.length}`); await new Promise(r => setTimeout(r, 300)); } } if (data.has_more) { await getFavVideos(mediaId, ps, pn + 1, videos).then(resolve); } else { document.getElementById('reading-progress')?.remove(); resolve(videos); } }, onerror: reject }); }); } // 创建新收藏夹 async function createFolder(title) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://api.bilibili.com/x/v3/fav/folder/add', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `csrf=${getCsrf()}&title=${encodeURIComponent(title)}`, responseType: 'json', onload: function(response) { resolve(response.response.data.id); }, onerror: reject }); }); } // 添加视频到收藏夹 async function addToFav(aid, fid) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://api.bilibili.com/x/v3/fav/resource/deal', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `csrf=${getCsrf()}&rid=${aid}&type=2&add_media_ids=${fid}`, responseType: 'json', onload: function(response) { resolve(response.response); }, onerror: reject }); }); } // 从收藏夹移除视频 async function removeFromFav(aid, fid) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://api.bilibili.com/x/v3/fav/resource/deal', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `csrf=${getCsrf()}&rid=${aid}&type=2&del_media_ids=${fid}`, responseType: 'json', onload: function(response) { resolve(response.response); }, onerror: reject }); }); } // 创建配置界面 function createConfigUI(tidGroups) { const modal = document.createElement('div'); modal.className = 'bili-classifier-container bili-classifier-modal'; let html = ` <h3>收藏夹自动分类</h3> <div class="bili-classifier-radio-group"> <label class="bili-classifier-radio-label"> <input type="radio" name="operationMode" value="copy" checked> 复制模式 </label> <label class="bili-classifier-radio-label"> <input type="radio" name="operationMode" value="move"> 移动模式 </label> </div> <div class="bili-classifier-option-group"> <label class="bili-classifier-checkbox-label"> <input type="checkbox" id="autoClassifyUnassigned" checked> 对未自定义分组的视频自动按分区分类 </label> </div> <div style="margin-bottom: 20px"> <button class="bili-classifier-btn" id="addCustomGroup">添加自定义分组</button> </div> <div id="customGroups"></div> <div id="defaultGroups"> <h4>视频分区分组</h4> `; Object.entries(tidGroups).forEach(([tid, videos]) => { html += ` <div class="bili-classifier-group tid-group" data-tid="${tid}"> <div class="bili-classifier-group-header"> <span>${videos[0].tname} (${videos.length}个视频)</span> </div> </div> `; }); html += ` </div> <div class="bili-classifier-footer"> <button class="bili-classifier-btn secondary" id="cancelClassify">取消</button> <button class="bili-classifier-btn" id="startClassify">开始分类</button> </div> <div class="bili-classifier-links"> <a href="https://space.bilibili.com/1937042029" target="_blank" class="bili-classifier-link-btn"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" fill="#666"/> <path d="M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z" fill="#666"/> </svg> 我的B站主页 </a> <a href="https://github.com/jqwgt" target="_blank" class="bili-classifier-link-btn"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z" fill="#666"/> </svg> GitHub项目 </a> </div> `; modal.innerHTML = html; document.body.appendChild(modal); let existingFolders = []; let customGroups = []; let operationMode = 'copy'; // 默认复制模式 let autoClassifyUnassigned = true; // 默认自动分类未分组视频 // 获取操作模式 modal.querySelectorAll('input[name="operationMode"]').forEach(radio => { radio.addEventListener('change', function() { operationMode = this.value; }); }); // 获取自动分类选项 modal.querySelector('#autoClassifyUnassigned').addEventListener('change', function() { autoClassifyUnassigned = this.checked; }); // 获取现有收藏夹 getUserFavLists().then(folders => { existingFolders = folders; }); // 添加自定义分组的处理 document.getElementById('addCustomGroup').onclick = async () => { const groupDiv = document.createElement('div'); groupDiv.className = 'bili-classifier-group custom-group'; const tidOptions = Object.entries(tidGroups) .map(([tid, videos]) => ` <label class="bili-classifier-checkbox-label"> <input type="checkbox" class="bili-classifier-checkbox" value="${tid}"> ${videos[0].tname} (${videos.length}个视频) </label> `).join(''); groupDiv.innerHTML = ` <div class="bili-classifier-group-header"> <input type="text" class="bili-classifier-input folder-name" placeholder="新收藏夹名称"> <button class="bili-classifier-btn secondary use-existing">使用现有收藏夹</button> <button class="bili-classifier-btn danger remove-group">删除分组</button> </div> <div class="bili-classifier-checkbox-group tid-options"> ${tidOptions} </div> `; document.getElementById('customGroups').appendChild(groupDiv); // 使用现有收藏夹按钮处理 groupDiv.querySelector('.use-existing').onclick = () => { const select = document.createElement('select'); select.className = 'bili-classifier-select'; select.innerHTML = ` <option value="">选择现有收藏夹</option> ${existingFolders.map(f => `<option value="${f.id}">${f.title}</option>`).join('')} `; const input = groupDiv.querySelector('.folder-name'); input.parentNode.replaceChild(select, input); }; // 删除分组按钮处理 groupDiv.querySelector('.remove-group').onclick = () => { groupDiv.remove(); }; }; return new Promise((resolve, reject) => { document.getElementById('startClassify').onclick = () => { const config = { custom: [], default: {}, operationMode: operationMode, autoClassifyUnassigned: autoClassifyUnassigned }; // 收集自定义分组配置 document.querySelectorAll('.custom-group').forEach(group => { const nameInput = group.querySelector('.folder-name, select'); const selectedTids = Array.from(group.querySelectorAll('input[type="checkbox"]:checked')) .map(cb => cb.value); if (selectedTids.length > 0 && nameInput.value) { config.custom.push({ name: nameInput.value, isExisting: nameInput.tagName === 'SELECT', fid: nameInput.tagName === 'SELECT' ? nameInput.value : null, tids: selectedTids }); } }); // 收集默认分组配置(仅在用户选择自动分类时) if (autoClassifyUnassigned) { Object.keys(tidGroups).forEach(tid => { if (!config.custom.some(g => g.tids.includes(tid))) { config.default[tid] = tidGroups[tid][0].tname; } }); } modal.remove(); resolve(config); }; document.getElementById('cancelClassify').onclick = () => { modal.remove(); reject('用户取消操作'); }; }); } // 创建读取视频进度显示 function createReadingProgressDiv() { const div = document.createElement('div'); div.id = 'reading-progress'; div.className = 'bili-classifier-progress'; div.innerHTML = ` <div>正在读取视频...</div> <div class="bili-classifier-progress-bar"> <div class="bili-classifier-progress-fill" style="width: 0%"></div> </div> `; document.body.appendChild(div); return div; } // 更新读取视频进度 function updateReadingProgress(message) { const progressDiv = document.getElementById('reading-progress') || createReadingProgressDiv(); progressDiv.querySelector('div:first-child').textContent = message; } // 创建进度显示 function createProgressDiv() { const div = document.createElement('div'); div.id = 'fav-progress'; div.className = 'bili-classifier-progress'; div.innerHTML = ` <div>正在处理...</div> <div class="bili-classifier-progress-bar"> <div class="bili-classifier-progress-fill" style="width: 0%"></div> </div> <div>0/0</div> `; document.body.appendChild(div); return div; } // 更新进度显示 function updateProgress(message, current, total, skipped = 0) { const progressDiv = document.getElementById('fav-progress') || createProgressDiv(); progressDiv.querySelector('div:first-child').textContent = message; progressDiv.querySelector('.bili-classifier-progress-fill').style.width = `${(current/total)*100}%`; progressDiv.querySelector('div:last-child').textContent = `${current}/${total}${skipped > 0 ? ` (跳过${skipped}个)` : ''}`; } // 主处理流程 async function processClassify() { let totalProcessed = 0; let totalVideos = 0; let skippedVideos = 0; const sourceFid = new URL(location.href).searchParams.get('fid'); try { if (!sourceFid) throw new Error('未找到收藏夹ID'); log('开始获取收藏夹视频...'); const videos = await getFavVideos(sourceFid); if (!videos.length) throw new Error('未找到视频'); // 按分区分组视频 const tidGroups = {}; videos.forEach(video => { if (!tidGroups[video.tid]) { tidGroups[video.tid] = []; } tidGroups[video.tid].push(video); }); totalVideos = videos.length; // 获取用户配置 const userConfig = await createConfigUI(tidGroups); // 处理自定义分组 for (const group of userConfig.custom) { let targetFid; if (group.isExisting) { targetFid = group.fid; } else { // 检查收藏夹名称是否存在 const existingFolders = await getUserFavLists(); let folderName = group.name; let counter = 1; while (existingFolders.some(f => f.title === folderName)) { folderName = `${group.name}_${counter++}`; log(`收藏夹名称"${group.name}"已存在,尝试使用"${folderName}"`, 'info'); } targetFid = await createFolder(folderName); } // 添加选中分区的视频 for (const tid of group.tids) { for (const video of tidGroups[tid]) { try { await addToFav(video.aid, targetFid); if (userConfig.operationMode === 'move') { await removeFromFav(video.aid, sourceFid); } totalProcessed++; } catch (error) { log(`处理视频 ${video.aid} 失败: ${error.message},已跳过`, 'error'); skippedVideos++; } updateProgress(`正在处理视频到分组"${group.name}"`, totalProcessed, totalVideos, skippedVideos); await new Promise(r => setTimeout(r, 300)); } } } // 处理未分组的视频(仅在用户选择自动分类时) if (userConfig.autoClassifyUnassigned) { for (const [tid, folderName] of Object.entries(userConfig.default)) { if (!userConfig.custom.some(g => g.tids.includes(tid))) { // 添加重名检测 const existingFolders = await getUserFavLists(); let folderNameToUse = folderName; let counter = 1; while (existingFolders.some(f => f.title === folderNameToUse)) { folderNameToUse = `${folderName}_${counter++}`; log(`收藏夹名称"${folderName}"已存在,尝试使用"${folderNameToUse}"`, 'info'); } const targetFid = await createFolder(folderNameToUse); for (const video of tidGroups[tid]) { await addToFav(video.aid, targetFid); if (userConfig.operationMode === 'move') { await removeFromFav(video.aid, sourceFid); } totalProcessed++; updateProgress(`正在处理视频到"${folderNameToUse}"`, totalProcessed, totalVideos); await new Promise(r => setTimeout(r, 300)); } } } } document.getElementById('fav-progress')?.remove(); log(`分类完成!处理了 ${totalProcessed} 个视频,跳过了 ${skippedVideos} 个视频`, 'success'); alert(`分类完成!处理了 ${totalProcessed} 个视频,跳过了 ${skippedVideos} 个视频`); } catch (error) { log(error.message, 'error'); alert('操作失败:' + error.message); } } // 添加触发按钮和链接 function addButton() { const btnContainer = document.createElement('div'); btnContainer.className = 'bili-classifier-float-btn'; const btn = document.createElement('button'); btn.className = 'bili-classifier-btn'; btn.textContent = '按分区分类'; btn.onclick = processClassify; const links = document.createElement('div'); links.className = 'bili-classifier-links'; links.innerHTML = ` <a href="https://space.bilibili.com/1937042029" target="_blank" class="bili-classifier-link-btn"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" fill="#666"/> <path d="M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z" fill="#666"/> </svg> 我的B站 </a> <a href="https://github.com/jqwgt" target="_blank" class="bili-classifier-link-btn"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z" fill="#666"/> </svg> GitHub </a> `; btnContainer.appendChild(btn); btnContainer.appendChild(links); document.body.appendChild(btnContainer); } // 初始化 addButton(); })();