// ==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 // @downloadURL https://update.greasyfork.icu/scripts/531672/Bilibili%E6%94%B6%E8%97%8F%E5%A4%B9%E8%87%AA%E5%8A%A8%E5%88%86%E7%B1%BB.user.js // @updateURL https://update.greasyfork.icu/scripts/531672/Bilibili%E6%94%B6%E8%97%8F%E5%A4%B9%E8%87%AA%E5%8A%A8%E5%88%86%E7%B1%BB.meta.js // ==/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 = `

收藏夹自动分类

视频分区分组

`; Object.entries(tidGroups).forEach(([tid, videos]) => { html += `
${videos[0].tname} (${videos.length}个视频)
`; }); html += `
`; 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]) => ` `).join(''); groupDiv.innerHTML = `
${tidOptions}
`; document.getElementById('customGroups').appendChild(groupDiv); // 使用现有收藏夹按钮处理 groupDiv.querySelector('.use-existing').onclick = () => { const select = document.createElement('select'); select.className = 'bili-classifier-select'; select.innerHTML = ` ${existingFolders.map(f => ``).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 = `
正在读取视频...
`; 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 = `
正在处理...
0/0
`; 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 = ` 我的B站 GitHub `; btnContainer.appendChild(btn); btnContainer.appendChild(links); document.body.appendChild(btnContainer); } // 初始化 addButton(); })();