// ==UserScript== // @name 哔哩哔哩收藏夹导出 // @namespace https://github.com/AHCorn/Bilibili-Favlist-Export // @icon https://www.bilibili.com/favicon.ico // @version 3.1 // @license GPL-3.0 // @description 导出哔哩哔哩收藏夹为 CSV 或 HTML 文件,以便导入 Raindrop 或 Firefox。 // @author AHCorn // @match http*://space.bilibili.com/*/* // @grant GM_addStyle // @grant GM_download // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js // @downloadURL https://update.greasyfork.icu/scripts/487532/%E5%93%94%E5%93%A9%E5%93%94%E5%93%A9%E6%94%B6%E8%97%8F%E5%A4%B9%E5%AF%BC%E5%87%BA.user.js // @updateURL https://update.greasyfork.icu/scripts/487532/%E5%93%94%E5%93%A9%E5%93%94%E5%93%A9%E6%94%B6%E8%97%8F%E5%A4%B9%E5%AF%BC%E5%87%BA.meta.js // ==/UserScript== (function () { 'use strict'; // 要改导出速度可以在下方更改(单位ms) let DELAY = GM_getValue('exportDelay', 2000); const DELAY_SPEEDS = { slow: 4000, normal: 2000, fast: 1000 }; let filterInvalidVideos = GM_getValue('filterInvalidVideos', true); let csvHeaderOptions = { title: "\uFEFFtitle", url: "url", foldername: "folder", created: "created" }; let csvHeaderActive = ["\uFEFFtitle", "url", "folder", "created"]; function updateCSVHeader() { csvHeaderActive = Object.keys(csvHeaderOptions) .filter(option => csvInclude[option]) .map(option => csvHeaderOptions[option]); } let csvContent = csvHeaderActive.join(",") + "\n"; let htmlTemplateStart = `
`; const HTML_TEMPLATE_END = `
`; let htmlContent = ""; let csvInclude = { title: true, url: true, foldername: true, created: true }; let exportCurrentFolderOnly = false; let currentFolderName = ""; let addedFolders = new Set(); GM_addStyle(` #bilibili-export-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(135deg, #f6f8fa, #e9ecef); border-radius: 24px; box-shadow: 0 10px 30px rgba(0,0,0,0.1), 0 1px 8px rgba(0,0,0,0.06); padding: 30px; width: 90%; max-width: 400px; display: none; z-index: 10000; font-family: 'Segoe UI', 'Roboto', sans-serif; transition: all 0.3s cubic-bezier(0.25,0.8,0.25,1); box-sizing: border-box !important; -webkit-box-sizing: border-box !important; } #bilibili-export-panel * { box-sizing: border-box !important; -webkit-box-sizing: border-box !important; } #bilibili-export-panel h2 { margin: 0 0 20px; color: #00a1d6; font-size: 28px; text-align: center; font-weight: 700; } #current-exporting { margin-bottom: 20px; padding: 10px; background-color: rgba(0,161,214,0.1); border-left: 4px solid #00a1d6; border-right: 4px solid #00a1d6; border-radius: 4px; font-size: 14px; color: #00a1d6; text-align: center; transition: all 0.5s ease; } #current-exporting.completed { background-color: rgba(76,175,80,0.1); border-left-color: #4CAF50; border-right-color: #4CAF50; color: #4CAF50; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } #current-exporting.completed { animation: pulse 0.5s ease-in-out; } #formatSelector { display: flex; justify-content: center; align-items: center; margin-bottom: 25px; position: relative; background-color: #e0e0e0; border-radius: 20px; padding: 5px; box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; } .formatButton { z-index: 2; padding: 10px 20px; font-size: 16px; color: #3c4043; cursor: pointer; transition: color 0.3s ease-in-out; font-weight: 600; flex: 1; text-align: center; position: relative; user-select: none; } .formatButton.selected { color: #FFF; } .slider { position: absolute; left: 5px; top: 5px; background-color: #00a1d6; border-radius: 15px; transition: transform 0.3s ease-in-out; height: calc(100% - 10px); width: calc(50% - 5px); z-index: 1; box-shadow: 0 2px 4px rgba(0,0,0,0.2); pointer-events: none; } .toggle-switch { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; padding: 10px 15px; background-color: #f1f3f4; border-radius: 12px; transition: all 0.3s ease; } .toggle-switch:hover { background-color: #e8eaed; } .toggle-switch label { font-size: 16px; color: #3c4043; font-weight: 600; } .switch { position: relative; display: inline-block; width: 52px; height: 28px; } .switch input { opacity: 0; width: 0; height: 0; } .switch-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } .switch-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .switch-slider { background-color: #00a1d6; } input:checked + .switch-slider:before { transform: translateX(24px); } #export-button { display: block; width: 100%; padding: 12px; background-color: #00a1d6; color: white; border: none; border-radius: 12px; font-size: 18px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; min-height: 48px; } #export-button::after { content: ''; position: absolute; top: 0; left: 0; height: 100%; width: var(--progress, 0%); background-color: rgba(0,0,0,0.1); transition: width 0.3s ease; z-index: 1; pointer-events: none; } #export-button.exporting { background-color: #e0e0e0; color: #333; } #export-button .progress-text { position: relative; z-index: 2; } #export-button:not(.exporting) { background-color: #00a1d6; color: white; } #export-button.exporting:hover { background-color: #ff4d4f; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(255,77,79,0.3); } #export-button.exporting:hover .progress-text { opacity: 0; } #export-button.exporting:hover::before { content: '终止导出'; position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background-color: transparent; color: white; z-index: 3; transition: all 0.3s ease; } .input-group { margin-bottom: 15px; width: 100%; } .input-group input { width: 100%; padding: 10px; border: 1px solid #dadce0; border-radius: 8px; font-size: 14px; box-sizing: border-box !important; -webkit-box-sizing: border-box !important; margin: 0; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideIn { from { transform: translate(-50%, -60%); } to { transform: translate(-50%, -50%); } } #bilibili-export-panel.show { display: block; animation: fadeIn 0.3s ease-out, slideIn 0.3s ease-out; } #panel-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 9999; display: none; } #current-exporting.completed { cursor: help; } #current-exporting[title] { text-decoration: underline dotted; } #refresh-button { display: none; width: 100%; padding: 8px; background-color: #4CAF50; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; margin-top: 10px; text-align: center; opacity: 0; transform: translateY(-10px); } #refresh-button.show { display: block; opacity: 1; transform: translateY(0); } #refresh-button:hover { background-color: #45a049; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(76,175,80,0.3); } #refresh-button:active { transform: translateY(0); } .button-container { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; } #speedSelector { display: flex; justify-content: center; align-items: center; margin-bottom: 25px; position: relative; background-color: #e0e0e0; border-radius: 20px; padding: 5px; box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; } .speedButton { z-index: 2; padding: 10px 15px; font-size: 14px; color: #3c4043; cursor: pointer; transition: color 0.3s ease-in-out; font-weight: 600; flex: 1; text-align: center; position: relative; user-select: none; } .speedButton.selected { color: #FFF; } .speed-slider { position: absolute; left: 5px; top: 5px; background-color: #00a1d6; border-radius: 15px; transition: transform 0.3s ease-in-out; height: calc(100% - 10px); width: calc(33.33% - 5px); z-index: 1; box-shadow: 0 2px 4px rgba(0,0,0,0.2); pointer-events: none; } .github-link { position: absolute; top: 20px; right: 20px; width: 24px; height: 24px; opacity: 0.7; transition: opacity 0.3s ease; } .github-link:hover { opacity: 1; } .github-link svg { width: 100%; height: 100%; fill: #00a1d6; } `); function* listGen() { // 新版 if ($(".favlist-aside").length > 0) { let groups = $(".favlist-aside .fav-collapse"); let group = groups.filter(function() { let headerText = $(this).find(".vui_collapse_item_header").first().text().trim(); return headerText.indexOf("我创建的收藏夹") !== -1; }).first(); if (group.length > 0) { let favorites = group.find(".fav-sidebar-item"); for (let item of favorites.get()) { yield item; } return; } } // 旧版 if ($("#fav-createdList-container").length > 0) { let defaultFolder = $("#fav-createdList-container > .fav-item.cur"); if (defaultFolder.length > 0) { yield defaultFolder[0]; } let folders = $("#fav-createdList-container > ul.fav-list > li.fav-item"); for (let folder of folders.get()) { yield folder; } return; } if ($(".fav-list").length > 0) { let folders = $(".fav-list .fav-item").get(); for (let folder of folders) { yield folder; } } else if ($(".favlist-aside .fav-sidebar-item").length > 0) { let folders = $(".favlist-aside .fav-sidebar-item").get(); for (let folder of folders) { yield folder; } } else if ($(".fav-sortable-list .fav-sidebar-item").length > 0) { let folders = $(".fav-sortable-list .fav-sidebar-item").get(); for (let folder of folders) { yield folder; } } } let gen = listGen(); let panel = null; let exportButton = null; let formatButtons = null; let bookmarkTitleInput = null; let globalFolderNameInput = null; let totalPage = 0; let currentPage = 0; let isExporting = false; let hasExportedData = false; let exportFormat = GM_getValue('exportFormat', 'csv'); let exportedData = { csv: null, html: null, htmlConfig: { title: '', globalFolderName: '' }, errors: [] }; function getCSVFileName() { let userName = ""; if ($(".nickname").length > 0) { userName = $(".nickname").first().text().trim(); } else { userName = $("#h-name").text().trim(); } if (exportCurrentFolderOnly) { // 文件名优化 let folderName = currentFolderName || getFolderName() || getFolderNameFromSidebar() || ""; return userName + "的" + folderName + "收藏.csv"; } else { return userName + "的收藏夹.csv"; } } function getHTMLFileName() { let userName = ""; if ($(".nickname").length > 0) { userName = $(".nickname").first().text().trim(); } else { userName = $("#h-name").text().trim(); } if (exportCurrentFolderOnly) { let folderName = currentFolderName || getFolderName() || getFolderNameFromSidebar() || ""; return userName + "的" + folderName + "收藏.html"; } else { return userName + "的收藏夹.html"; } } function getFolderName() { let folderName = ""; // 新版 if ($(".favlist-info-detail__title .vui_ellipsis.multi-mode").length > 0) { folderName = $(".favlist-info-detail__title .vui_ellipsis.multi-mode").first().text().trim(); } // 旧版 else if ($(".fav-name").length > 0) { folderName = $(".fav-name").first().text().trim(); } // 兜底 else if ($("#fav-createdList-container").length > 0) { folderName = $("#fav-createdList-container .fav-item.cur a.text").text().trim(); } return folderName || "未知收藏夹"; } function getFolderNameFromSidebar() { let selected = $(".fav-sidebar-item.vui_sidebar-item--active").first(); if (selected.length > 0) { let folder = selected.find(".vui_ellipsis").first().text().trim(); if (!folder) { folder = selected.text().trim(); } return folder; } return ""; } // 去掉 URL 中查询参数 function getVideosFromPage() { var results = []; var folderName = currentFolderName.replace(/\//g, '\\'); // 新版 if ($(".items").length > 0) { $(".items__item").each(function () { var title = $(this).find(".bili-video-card__title a").text().trim(); if (!title) return; if (filterInvalidVideos && (title === "已失效视频" || title.includes("视频不见了"))) { return; } var url = $(this).find(".bili-video-card__cover a").attr("href"); if (url) { if (url.indexOf("http") !== 0) { url = "https:" + url; } url = url.split('?')[0]; } var pubText = $(this).find(".bili-video-card__subtitle a .bili-video-card__text span").attr("title") || ""; var created = ""; var match = pubText.match(/收藏于((?:\d{4}[\/-])?\d{1,2}[\/-]\d{1,2})/); if(match) { created = parseTime(match[1]); } else { var textContent = $(this).find(".bili-video-card__subtitle").text().trim(); var m2 = textContent.match(/收藏于((?:\d{4}[\/-])?\d{1,2}[\/-]\d{1,2})/); created = m2 ? parseTime(m2[1]) : ""; } results.push(generateCSVLine(folderName, title, url, created)); addHTMLBookmark(folderName, title, url, created); }); } else if ($(".fav-video-list").length > 0) { $(".fav-video-list > li").each(function () { var titleElement = $(this).find("a.title"); var title = titleElement.text().trim(); if (!title) return; if (filterInvalidVideos && (title === "已失效视频" || title.includes("视频不见了"))) { return; } var url = titleElement.attr("href"); if (url) { if (url.indexOf("http") !== 0) { url = "https:" + url; } url = url.split('?')[0]; } var timeElement = $(this).find(".meta.pubdate"); var timeText = timeElement.text().trim().replace("收藏于:", "").trim(); var created = parseTime(timeText); results.push(generateCSVLine(folderName, title, url, created)); addHTMLBookmark(folderName, title, url, created); }); } return results.join('\n'); } function escapeCSV(field) { return '"' + String(field).replace(/"/g, '""') + '"'; } function getCurrentTimestamp() { return Math.floor(Date.now() / 1000); } function addHTMLFolder(folderName) { if (addedFolders.has(folderName)) { htmlContent += `
\n`; return; } if (addedFolders.size > 0) { htmlContent += `
\n`; } addedFolders.add(folderName); // 获取当前时间戳 let dateNow = getCurrentTimestamp(); htmlContent += `
\n`; } function addHTMLBookmark(folderName, title, url, created) { if (!addedFolders.has(folderName) || addedFolders.size === 0) { addHTMLFolder(folderName); } else if (Array.from(addedFolders).pop() !== folderName) { addHTMLFolder(folderName); } // 创建时间转换为时间戳 let timestamp; if (created) { timestamp = Math.floor(new Date(created).getTime() / 1000); } else { timestamp = getCurrentTimestamp(); } htmlContent += `
\n`; } exportedData.csv = csvContent; exportedData.html = htmlTemplateStart .replace("{globalFolderName}", exportedData.htmlConfig.globalFolderName) .replace("{BOOKMARK_TITLE}", exportedData.htmlConfig.title) .replace("{dateNow}", getCurrentTimestamp()) + htmlContent + HTML_TEMPLATE_END; hasExportedData = true; exportButton.innerHTML = `立即下载`; if (exportedData.errors.length > 0) { let errorMsg = "导出完成,但存在以下异常:\n"; exportedData.errors.forEach(error => { errorMsg += `${error.folder}(第${error.page}页): ${error.message}\n`; }); currentExporting.textContent = "导出完成(存在异常)"; currentExporting.title = errorMsg; } else { currentExporting.textContent = "导出完成"; } const refreshButton = document.querySelector('#refresh-button'); refreshButton.classList.add('show'); refreshButton.onclick = () => { location.reload(); }; exportButton.onclick = () => { if (exportFormat === "csv") { downloadCSV(); } else if (exportFormat === "html") { if (!bookmarkTitleInput.value || !globalFolderNameInput.value) { alert("请配置书签标题和全局父文件夹名称。"); return; } downloadHTML(); } }; } else { if (csvContent.length > csvHeaderActive.join(",").length + 2) { exportedData.csv = csvContent; exportedData.html = htmlTemplateStart .replace("{globalFolderName}", exportedData.htmlConfig.globalFolderName) .replace("{BOOKMARK_TITLE}", exportedData.htmlConfig.title) .replace("{dateNow}", getCurrentTimestamp()) + htmlContent + HTML_TEMPLATE_END; hasExportedData = true; exportButton.innerHTML = "立即下载"; currentExporting.textContent = "导出已终止(可下载已导出内容)"; const refreshButton = document.querySelector('#refresh-button'); refreshButton.classList.add('show'); refreshButton.onclick = () => { location.reload(); }; exportButton.onclick = () => { if (exportFormat === "csv") { downloadCSV(); } else if (exportFormat === "html") { if (!bookmarkTitleInput.value || !globalFolderNameInput.value) { alert("请配置书签标题和全局父文件夹名称。"); return; } downloadHTML(); } }; } else { exportButton.innerHTML = "开始导出"; currentExporting.textContent = "导出已终止"; exportedData.csv = null; exportedData.html = null; exportedData.errors = []; hasExportedData = false; document.querySelector('#refresh-button').classList.remove('show'); exportButton.onclick = startExport; } } currentExporting.classList.add('completed'); } function downloadCSV() { if (!exportedData.csv) { alert('没有可用的导出数据,请先执行导出操作。'); return; } let fileName = getCSVFileName(); let blobUrl = URL.createObjectURL(new Blob([exportedData.csv], {type: 'text/csv;charset=utf-8;'})); GM_download({ url: blobUrl, name: fileName, onload: () => {}, onerror: () => { alert('下载失败,正在尝试弹出新标签页进行下载,请允许弹窗权限'); let htmlContent = `
点击下载 CSV 文件 `; let htmlBlob = new Blob([htmlContent], {type: 'text/html;charset=utf-8;'}); let htmlBlobUrl = URL.createObjectURL(htmlBlob); window.open(htmlBlobUrl, '_blank'); } }); } function downloadHTML() { if (!exportedData.html) { alert('没有可用的导出数据,请先执行导出操作。'); return; } let fileName = getHTMLFileName(); let blobUrl = URL.createObjectURL(new Blob([exportedData.html], {type: 'text/html;charset=utf-8;'})); GM_download({ url: blobUrl, name: fileName, onload: () => {}, onerror: () => { alert('下载失败,正在尝试弹出新标签页进行下载,请允许弹窗权限'); let htmlContent = ` 点击下载 HTML 文件 `; let htmlBlob = new Blob([htmlContent], {type: 'text/html;charset=utf-8;'}); let htmlBlobUrl = URL.createObjectURL(htmlBlob); window.open(htmlBlobUrl, '_blank'); } }); } function createPanel() { panel = document.createElement("div"); panel.id = "bilibili-export-panel"; panel.innerHTML = `