// ==UserScript== // @name 哔哩哔哩收藏夹导出 // @namespace https://github.com/AHCorn/Bilibili-Favlist-Export // @icon https://www.bilibili.com/favicon.ico // @version 2.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 // @downloadURL none // ==/UserScript== (function () { 'use strict'; const DELAY = 2000; 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 = ` Bookmarks

{BOOKMARK_TITLE}

{globalFolderName}

\n

`; const HTML_TEMPLATE_END = `

`; let htmlContent = ""; let globalParentFolderName = ""; let csvInclude = { title: true, url: true, foldername: true, created: true }; let exportCurrentFolderOnly = false; 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); } #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); } .formatButton { z-index: 1; 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; } .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: 0; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } .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; margin-top: 20px; position: relative; overflow: hidden; } #export-button:hover { background-color: #0091c2; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 161, 214, 0.3); } #export-button:disabled { background-color: #cccccc; cursor: not-allowed; } #export-button::after { content: ''; position: absolute; top: 0; left: 0; height: 100%; width: 0%; background-color: rgba(255,255,255,0.2); transition: width 0.3s ease; } .input-group { margin-bottom: 15px; } .input-group input { width: calc(100% - 20px); padding: 10px; border: 1px solid #dadce0; border-radius: 8px; font-size: 14px; } @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; } `); let gen = listGen(); let panel = null; let exportButton = null; let formatButtons = null; let folderInputSection = null; let bookmarkTitleInput = null; let globalFolderNameInput = null; let lastAddedFolderName = ""; let totalPage = 0; let currentPage = 0; let isExporting = false; let exportFormat = GM_getValue('exportFormat', 'csv'); function getCSVFileName() { let userName = $("#h-name").text(); return userName + "的收藏夹.csv"; } function getHTMLFileName() { let userName = $("#h-name").text(); return userName + "的收藏夹.html"; } function getFolderName() { return $("#fav-createdList-container .fav-item.cur a.text").text().trim(); } function escapeCSV(field) { return '"' + String(field).replace(/"/g, '""') + '"'; } function getCurrentTimestamp() { return Math.floor(Date.now() / 1000); } function addHTMLFolder(folderName) { let dateNow = getCurrentTimestamp(); if (folderName !== lastAddedFolderName) { if (lastAddedFolderName !== "") { htmlContent += `

\n`; } htmlContent += `

${folderName}

\n

\n`; lastAddedFolderName = folderName; } } function addHTMLBookmark(folderName, title, url, created) { addHTMLFolder(folderName); let dateNow = new Date(created).getTime() / 1000; htmlContent += `

${title}\n`; } function generateCSVLine(folderName, title, url, created) { let parts = []; if (csvInclude.title) parts.push(escapeCSV(title)); if (csvInclude.url) parts.push(escapeCSV(url)); if (csvInclude.foldername) parts.push(escapeCSV(folderName)); if (csvInclude.created) parts.push(escapeCSV(created)); return parts.join(','); } function parseTime(timeText) { let now = new Date(); let time; if (timeText.includes("天前")) { let days = parseInt(timeText); time = new Date(now - days * 24 * 60 * 60 * 1000); } else if (timeText.includes("周前")) { let weeks = parseInt(timeText); time = new Date(now - weeks * 7 * 24 * 60 * 60 * 1000); } else if (timeText.includes("小时前")) { let hours = parseInt(timeText); time = new Date(now - hours * 60 * 60 * 1000); } else if (timeText.includes("分钟前")) { let minutes = parseInt(timeText); time = new Date(now - minutes * 60 * 1000); } else { let [month, day] = timeText.split('-').map(Number); let year = now.getFullYear(); let fullDate = new Date(year, month - 1, day); if (fullDate > now) { year--; } return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; } return time.toISOString().split('T')[0]; } function getVideosFromPage() { var results = []; var folderName = getFolderName().replace(/\//g, '\\'); $(".fav-video-list > li").each(function () { var titleElement = $(this).find("a.title"); var title = titleElement.text().replace(/,/g, ''); if (title !== "已失效视频") { var url = 'https:' + titleElement.attr("href"); var timeElement = $(this).find(".meta.pubdate"); var timeText = timeElement.text().trim().replace("收藏于:", "").trim(); var created = parseTime(timeText); results.push(generateCSVLine(folderName, title, url, created)); if (exportFormat === "html") { addHTMLBookmark(folderName, title, url, created); } } }); return results.join('\n'); } function processVideos() { if (isExporting) { csvContent += getVideosFromPage() + '\n'; currentPage++; updateProgress(Math.round((currentPage / totalPage) * 100)); if (currentPage >= totalPage || $(".be-pager-next:visible").length == 0) { if (exportCurrentFolderOnly) { finishExport(); } else { setTimeout(changeList, DELAY); } } else { $(".be-pager-next").click(); setTimeout(processVideos, DELAY); } } } function* listGen() { for (let list of $("#fav-createdList-container .fav-item a").get()) { yield list; } } function changeList() { if (isExporting) { if (exportCurrentFolderOnly) { processVideos(); } else { let list = gen.next().value; if (list) { list.click(); setTimeout(() => { totalPage = parseInt($(".be-pager-total").text().match(/\d+/)[0]) || 1; currentPage = 0; updateProgress(0); let currentFolderName = getFolderName(); document.querySelector('#current-exporting').textContent = `正在导出:${currentFolderName}`; document.querySelector('#current-exporting').classList.remove('completed'); processVideos(); }, DELAY); } else { finishExport(); } } } } function updateProgress(percentage) { exportButton.textContent = `导出中... ${percentage}%`; exportButton.style.setProperty('--progress', `${percentage}%`); exportButton.style.backgroundImage = `linear-gradient(to right, rgba(255,255,255,0.2) ${percentage}%, transparent ${percentage}%)`; } function finishExport() { isExporting = false; exportButton.textContent = "立即下载"; exportButton.disabled = false; let currentExporting = document.querySelector('#current-exporting'); currentExporting.textContent = "导出完成"; currentExporting.classList.add('completed'); exportButton.onclick = () => { if (exportFormat === "csv") { downloadCSV(); } else if (exportFormat === "html") { downloadHTML(); } exportButton.textContent = "开始导出"; exportButton.disabled = true; setTimeout(() => { exportButton.disabled = false; }, 3000); }; } function startExport() { if (exportFormat === "html" && (!bookmarkTitleInput.value || !globalFolderNameInput.value)) { alert("请配置书签标题和全局父文件夹名称。"); return; } GM_setValue('bookmarkTitle', bookmarkTitleInput.value); GM_setValue('globalFolderName', globalFolderNameInput.value); exportButton.disabled = true; exportButton.textContent = "导出中... 0%"; isExporting = true; htmlContent = ""; csvContent = "\uFEFF" + csvHeaderActive.join(",") + "\n"; document.querySelector('#current-exporting').textContent = "准备开始导出..."; document.querySelector('#current-exporting').classList.remove('completed'); if (exportCurrentFolderOnly) { totalPage = parseInt($(".be-pager-total").text().match(/\d+/)[0]) || 1; currentPage = 0; processVideos(); } else { gen = listGen(); changeList(); } } function downloadCSV() { let fileName = getCSVFileName(); let blobUrl = URL.createObjectURL(new Blob([csvContent], {type: 'text/csv;charset=utf-8;'})); GM_download({ url: blobUrl, name: fileName, onload: () => { hidePanel(); }, 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() { let fileName = getHTMLFileName(); let globalParentFolderName = globalFolderNameInput.value; let htmlFinalContent = htmlTemplateStart.replace("{globalFolderName}", globalFolderNameInput.value).replace("{BOOKMARK_TITLE}", bookmarkTitleInput.value.trim()) + htmlContent + HTML_TEMPLATE_END; let blobUrl = URL.createObjectURL(new Blob([htmlFinalContent], {type: 'text/html;charset=utf-8;'})); GM_download({ url: blobUrl, name: fileName, onload: () => { hidePanel(); }, 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 = `

收藏夹导出设置

点击下方按钮开始导出
CSV 格式
HTML 格式
`; document.body.appendChild(panel); let overlay = document.createElement("div"); overlay.id = "panel-overlay"; document.body.appendChild(overlay); formatButtons = panel.querySelectorAll('.formatButton'); formatButtons.forEach(button => { button.addEventListener('click', () => { exportFormat = button.dataset.format; GM_setValue('exportFormat', exportFormat); updateFormatButtons(); toggleOptions(); }); }); folderInputSection = panel.querySelector('#html-options'); bookmarkTitleInput = panel.querySelector('#bookmark-title'); globalFolderNameInput = panel.querySelector('#global-folder-name'); ['title', 'url', 'foldername', 'created'].forEach(option => { panel.querySelector(`#include-${option}`).addEventListener('change', (e) => { csvInclude[option] = e.target.checked; GM_setValue(`include_${option}`, csvInclude[option]); updateCSVHeader(); }); }); panel.querySelector('#export-current-folder-only').addEventListener('change', (e) => { exportCurrentFolderOnly = e.target.checked; GM_setValue('exportCurrentFolderOnly', exportCurrentFolderOnly); }); exportButton = panel.querySelector('#export-button'); exportButton.onclick = startExport; overlay.addEventListener('click', hidePanel); updateFormatButtons(); toggleOptions(); loadSavedSettings(); } function updateFormatButtons() { formatButtons.forEach(button => { button.classList.toggle('selected', button.dataset.format === exportFormat); }); const slider = panel.querySelector('.slider'); slider.style.transform = exportFormat === 'csv' ? 'translateX(0)' : 'translateX(100%)'; } function toggleOptions() { panel.querySelector('#csv-options').style.display = exportFormat === 'csv' ? 'block' : 'none'; panel.querySelector('#html-options').style.display = exportFormat === 'html' ? 'block' : 'none'; } function loadSavedSettings() { ['title', 'url', 'foldername', 'created'].forEach(option => { const saved = GM_getValue(`include_${option}`); if (saved !== undefined) { csvInclude[option] = saved; panel.querySelector(`#include-${option}`).checked = saved; } }); updateCSVHeader(); bookmarkTitleInput.value = GM_getValue('bookmarkTitle', ''); globalFolderNameInput.value = GM_getValue('globalFolderName', ''); exportCurrentFolderOnly = GM_getValue('exportCurrentFolderOnly', false); panel.querySelector('#export-current-folder-only').checked = exportCurrentFolderOnly; } function showPanel() { panel.style.opacity = 0; panel.style.display = 'block'; document.getElementById('panel-overlay').style.display = 'block'; setTimeout(() => { panel.style.opacity = 1; }, 0); } function hidePanel() { panel.style.opacity = 0; document.getElementById('panel-overlay').style.display = 'none'; setTimeout(() => { panel.style.display = 'none'; }, 300); } function init() { createPanel(); GM_registerMenuCommand("导出 Bilibili 收藏夹", showPanel); } if (location.href.includes("https://space.bilibili.com/") && location.href.includes("/favlist")) { init(); } })();