🏠 Home 

哔哩哔哩收藏夹导出

导出哔哩哔哩收藏夹为 CSV 或 HTML 文件,以便导入 Raindrop 或 Firefox。


Install this script?
// ==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/[email protected]/dist/jquery.min.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 = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>{BOOKMARK_TITLE}</H1>
<DL><p>
<DT><H3 ADD_DATE="{dateNow}" LAST_MODIFIED="{dateNow}">{globalFolderName}</H3>
<DL><p>`;
const HTML_TEMPLATE_END = `</DL><p>
</DL><p>`;
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 r###lts = [];
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]) : "";
}
r###lts.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);
r###lts.push(generateCSVLine(folderName, title, url, created));
addHTMLBookmark(folderName, title, url, created);
});
}
return r###lts.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 += `</DL><p>\n`;
return;
}
if (addedFolders.size > 0) {
htmlContent += `</DL><p>\n`;
}
addedFolders.add(folderName);
// 获取当前时间戳
let dateNow = getCurrentTimestamp();
htmlContent += `<DT><H3 ADD_DATE="${dateNow}" LAST_MODIFIED="${dateNow}">${folderName}</H3>\n<DL><p>\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 += `<DT><A HREF="${url}" ADD_DATE="${timestamp}" LAST_MODIFIED="${timestamp}">${title}</A>\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) {
timeText = timeText.replace(/^收藏于/, '').trim();
if (timeText.indexOf("年") > -1) {
timeText = timeText.replace("年", "-")
.replace("月", "-")
.replace("日", "")
.trim();
}
let now = new Date();
let currentYear = now.getFullYear();
if (timeText.match(/^\d{1,2}-\d{1,2}$/)) {
let [month, day] = timeText.split('-').map(Number);
return `${currentYear}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
} else if (timeText.match(/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/)) {
let delimiter = timeText.indexOf('/') > -1 ? '/' : '-';
let parts = timeText.split(delimiter);
let year = parts[0];
let month = parts[1].padStart(2, '0');
let day = parts[2].padStart(2, '0');
return `${year}-${month}-${day}`;
} else if (timeText === "昨天") {
let yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
return yesterday.toISOString().split('T')[0];
} else if (timeText.includes("年前") || timeText.includes("月前") || timeText.includes("天前") ||
timeText.includes("小时前") || timeText.includes("分钟前")) {
let time;
if (timeText.includes("年前")) {
let years = parseInt(timeText);
time = new Date(now.getFullYear() - years, now.getMonth(), now.getDate());
} else if (timeText.includes("月前")) {
let months = parseInt(timeText);
time = new Date(now.getFullYear(), now.getMonth() - months, now.getDate());
} else if (timeText.includes("天前")) {
let days = parseInt(timeText);
time = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
} else if (timeText.includes("小时前")) {
let hours = parseInt(timeText);
time = new Date(now.getTime() - hours * 60 * 60 * 1000);
} else if (timeText.includes("分钟前")) {
let minutes = parseInt(timeText);
time = new Date(now.getTime() - minutes * 60 * 1000);
}
return time.toISOString().split('T')[0];
} else {
return timeText;
}
}
// 遍历当前收藏夹
async function processVideos() {
if (isExporting) {
let retryCount = 0;
let videosLine = "";
while (retryCount < 2) {
videosLine = getVideosFromPage();
if (videosLine.length === 0) {
if (retryCount === 0) {
document.querySelector('#current-exporting').textContent = `正在导出:${currentFolderName}(内容为空,等待重试...)`;
await new Promise(resolve => setTimeout(resolve, 3000));
retryCount++;
continue;
} else {
exportedData.errors.push({
folder: currentFolderName,
page: currentPage + 1,
message: "页面内容为空"
});
document.querySelector('#current-exporting').textContent =
`正在导出:${currentFolderName}(警告:第${currentPage + 1}页内容为空)`;
}
}
break;
}
csvContent += videosLine + '\n';
currentPage++;
updateProgress(Math.round((currentPage / totalPage) * 100));
// 检查新版 / 旧版的下一页
let hasOldNext = $(".be-pager-next:visible").length > 0;
let hasNewNext = $(".vui_pagenation .vui_pagenation--btn-side:contains('下一页'):visible").length > 0;
if (currentPage >= totalPage || (!hasOldNext && !hasNewNext)) {
if (exportCurrentFolderOnly) {
finishExport();
} else {
setTimeout(changeList, DELAY);
}
} else {
if (hasOldNext) {
$(".be-pager-next").click();
} else if (hasNewNext) {
$(".vui_pagenation .vui_pagenation--btn-side:contains('下一页')").click();
}
setTimeout(processVideos, DELAY);
}
}
}
function changeList() {
if (isExporting) {
if (exportCurrentFolderOnly) {
processVideos();
} else {
let list = gen.next().value;
if (list) {
let folderTitle = "";
if ($("#fav-createdList-container").length > 0) {
// 区分默认
if ($(list).hasClass("cur")) {
folderTitle = $(list).find("a.text").text().trim();
} else {
folderTitle = $(list).find("a.text").text().trim();
$(list).find("a.text")[0].click();
}
}
// 新
else {
folderTitle = $(list).attr("title") ||
$(list).find(".vui_ellipsis").first().text().trim() ||
$(list).text().trim();
if ($(list).find(".vui_sidebar-item").length > 0) {
$(list).find(".vui_sidebar-item").click();
} else {
$(list).click();
}
}
currentFolderName = folderTitle || getFolderName() || getFolderNameFromSidebar() || "未知收藏夹";
setTimeout(() => {
if ($(".be-pager-total").length > 0) {
totalPage = parseInt($(".be-pager-total").text().match(/\d+/)[0]) || 1;
} else if ($(".vui_pagenation-go__count").length > 0) {
let text = $(".vui_pagenation-go__count").text();
let match = text.match(/共\s*(\d+)\s*页/);
totalPage = match ? parseInt(match[1]) : 1;
} else {
totalPage = 1;
}
currentPage = 0;
document.querySelector('#current-exporting').textContent = `正在导出:${currentFolderName}`;
document.querySelector('#current-exporting').classList.remove('completed');
processVideos();
}, DELAY);
} else {
finishExport();
}
}
}
}
function updateProgress(percentage) {
exportButton.innerHTML = `<span class="progress-text">导出中... ${percentage}%</span>`;
exportButton.style.setProperty('--progress', `${percentage}%`);
exportButton.classList.add('exporting');
}
function updateHTMLConfig() {
exportedData.htmlConfig = {
title: bookmarkTitleInput.value.trim(),
globalFolderName: globalFolderNameInput.value.trim()
};
if (exportedData.html) {
exportedData.html = htmlTemplateStart
.replace("{globalFolderName}", exportedData.htmlConfig.globalFolderName)
.replace("{BOOKMARK_TITLE}", exportedData.htmlConfig.title)
.replace("{dateNow}", getCurrentTimestamp()) +
htmlContent + HTML_TEMPLATE_END;
}
}
function startExport() {
if (hasExportedData) {
if (exportFormat === "csv") {
downloadCSV();
} else if (exportFormat === "html") {
if (!bookmarkTitleInput.value || !globalFolderNameInput.value) {
alert("请配置书签标题和全局父文件夹名称。");
return;
}
downloadHTML();
}
return;
}
exportedData.htmlConfig = {
title: bookmarkTitleInput.value.trim(),
globalFolderName: globalFolderNameInput.value.trim()
};
exportedData.errors = [];
htmlContent = "";
addedFolders = new Set();
csvContent = "\uFEFF" + csvHeaderActive.join(",") + "\n";
GM_setValue('bookmarkTitle', bookmarkTitleInput.value);
GM_setValue('globalFolderName', globalFolderNameInput.value);
exportButton.disabled = false;
exportButton.style.cursor = 'pointer';
exportButton.innerHTML = "导出中... 0%";
isExporting = true;
document.querySelector('#current-exporting').textContent = "准备开始导出...";
document.querySelector('#current-exporting').classList.remove('completed');
exportButton.classList.add('exporting');
exportButton.onclick = () => {
if (confirm('确定要终止导出吗?')) {
isExporting = false;
finishExport(true);
}
};
if (exportCurrentFolderOnly) {
currentFolderName = getFolderName() || getFolderNameFromSidebar() || "未知收藏夹";
if ($(".be-pager-total").length > 0) {
totalPage = parseInt($(".be-pager-total").text().match(/\d+/)[0]) || 1;
} else if ($(".vui_pagenation-go__count").length > 0) {
let text = $(".vui_pagenation-go__count").text();
let match = text.match(/共\s*(\d+)\s*页/);
totalPage = match ? parseInt(match[1]) : 1;
} else {
totalPage = 1;
}
currentPage = 0;
document.querySelector('#current-exporting').textContent = `正在导出:${currentFolderName}`; // 更新状态提示
processVideos();
} else {
gen = listGen();
changeList();
}
}
function finishExport(isTerminated = false) {
isExporting = false;
exportButton.disabled = false;
exportButton.style.cursor = 'pointer';
exportButton.classList.remove('exporting');
exportButton.style.setProperty('--progress', '0%');
let currentExporting = document.querySelector('#current-exporting');
if (!isTerminated) {
if (addedFolders.size > 0) {
htmlContent += `</DL><p>\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 = `<span class="progress-text">立即下载</span>`;
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 = `
<html>
<head><meta charset="UTF-8"></head>
<body><a href="${blobUrl}" download="${fileName}">点击下载 CSV 文件</a></body>
</html>`;
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>
<head><meta charset="UTF-8"></head>
<body><a href="${blobUrl}" download="${fileName}">点击下载 HTML 文件</a></body>
</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 = `
<h2>收藏夹导出设置</h2>
<a href="https://github.com/AHCorn/Bilibili-Favlist-Export" target="_blank" class="github-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
<div id="current-exporting">点击下方按钮开始导出</div>
<div id="formatSelector">
<div class="slider"></div>
<div class="formatButton" data-format="csv">CSV 格式</div>
<div class="formatButton" data-format="html">HTML 格式</div>
</div>
<div id="csv-options">
<div class="toggle-switch">
<label for="include-title">包含标题</label>
<label class="switch">
<input type="checkbox" id="include-title" checked>
<span class="switch-slider"></span>
</label>
</div>
<div class="toggle-switch">
<label for="include-url">包含网址</label>
<label class="switch">
<input type="checkbox" id="include-url" checked>
<span class="switch-slider"></span>
</label>
</div>
<div class="toggle-switch">
<label for="include-foldername">包含收藏夹名称</label>
<label class="switch">
<input type="checkbox" id="include-foldername" checked>
<span class="switch-slider"></span>
</label>
</div>
<div class="toggle-switch">
<label for="include-created">包含收藏时间</label>
<label class="switch">
<input type="checkbox" id="include-created" checked>
<span class="switch-slider"></span>
</label>
</div>
</div>
<div id="html-options" style="display: none;">
<div class="input-group">
<input type="text" id="bookmark-title" placeholder="书签标题 (H1)">
</div>
<div class="input-group">
<input type="text" id="global-folder-name" placeholder="全局父文件夹名称">
</div>
</div>
<div class="toggle-switch">
<label for="export-current-folder-only">仅导出当前文件夹</label>
<label class="switch">
<input type="checkbox" id="export-current-folder-only">
<span class="switch-slider"></span>
</label>
</div>
<div class="toggle-switch">
<label for="filter-invalid-videos">过滤已失效视频</label>
<label class="switch">
<input type="checkbox" id="filter-invalid-videos" checked>
<span class="switch-slider"></span>
</label>
</div>
<div id="speedSelector">
<div class="speed-slider"></div>
<div class="speedButton" data-speed="slow">慢速</div>
<div class="speedButton" data-speed="normal">正常</div>
<div class="speedButton" data-speed="fast">快速</div>
</div>
<div class="button-container">
<button id="export-button">开始导出</button>
<button id="refresh-button">刷新后继续导出</button>
</div>`;
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', function() {
const newFormat = this.dataset.format;
if (newFormat !== exportFormat) {
exportFormat = newFormat;
GM_setValue('exportFormat', exportFormat);
updateFormatButtons();
}
if (hasExportedData) {
if (exportFormat === 'html' && !exportedData.html) {
exportedData.html = htmlTemplateStart
.replace("{globalFolderName}", exportedData.htmlConfig.globalFolderName)
.replace("{BOOKMARK_TITLE}", exportedData.htmlConfig.title)
.replace("{dateNow}", getCurrentTimestamp()) +
htmlContent + HTML_TEMPLATE_END;
}
exportButton.innerHTML = "立即下载";
exportButton.onclick = () => {
if (exportFormat === "csv") {
downloadCSV();
} else if (exportFormat === "html") {
if (!bookmarkTitleInput.value || !globalFolderNameInput.value) {
alert("请配置书签标题和全局父文件夹名称。");
return;
}
downloadHTML();
}
};
}
});
});
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);
});
panel.querySelector('#filter-invalid-videos').addEventListener('change', (e) => {
filterInvalidVideos = e.target.checked;
GM_setValue('filterInvalidVideos', filterInvalidVideos);
});
exportButton = panel.querySelector('#export-button');
exportButton.onclick = startExport;
overlay.addEventListener('click', hidePanel);
updateFormatButtons();
toggleOptions();
loadSavedSettings();
bookmarkTitleInput.addEventListener('input', updateHTMLConfig);
globalFolderNameInput.addEventListener('input', updateHTMLConfig);
const speedButtons = panel.querySelectorAll('.speedButton');
speedButtons.forEach(button => {
button.addEventListener('click', function() {
const newSpeed = this.dataset.speed;
const newDelay = DELAY_SPEEDS[newSpeed];
if (newDelay !== DELAY) {
if (newSpeed === 'fast') {
if (confirm('快速导出(1000ms)可能会因为页面加载速度不够导致链接错位,是否继续?')) {
DELAY = newDelay;
GM_setValue('exportDelay', DELAY);
updateSpeedButtons();
}
} else {
DELAY = newDelay;
GM_setValue('exportDelay', DELAY);
updateSpeedButtons();
}
}
});
});
}
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%)';
toggleOptions();
}
function toggleOptions() {
const csvOptions = panel.querySelector('#csv-options');
const htmlOptions = panel.querySelector('#html-options');
if (exportFormat === 'csv') {
csvOptions.style.display = 'block';
htmlOptions.style.display = 'none';
} else {
csvOptions.style.display = 'none';
htmlOptions.style.display = 'block';
}
}
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;
filterInvalidVideos = GM_getValue('filterInvalidVideos', true);
panel.querySelector('#filter-invalid-videos').checked = filterInvalidVideos;
DELAY = GM_getValue('exportDelay', DELAY_SPEEDS.normal);
updateSpeedButtons();
}
function updateSpeedButtons() {
const speedButtons = panel.querySelectorAll('.speedButton');
const currentSpeed = Object.entries(DELAY_SPEEDS).find(([_, value]) => value === DELAY)?.[0] || 'normal';
const slider = panel.querySelector('.speed-slider');
speedButtons.forEach(button => {
button.classList.toggle('selected', button.dataset.speed === currentSpeed);
});
switch(currentSpeed) {
case 'slow':
slider.style.transform = 'translateX(0)';
break;
case 'normal':
slider.style.transform = 'translateX(100%)';
break;
case 'fast':
slider.style.transform = 'translateX(200%)';
break;
}
}
function showPanel() {
panel.style.opacity = 0;
panel.style.display = 'block';
document.getElementById('panel-overlay').style.display = 'block';
if (hasExportedData) {
exportButton.innerHTML = "立即下载";
exportButton.onclick = () => {
if (exportFormat === "csv") {
downloadCSV();
} else if (exportFormat === "html") {
if (!bookmarkTitleInput.value || !globalFolderNameInput.value) {
alert("请配置书签标题和全局父文件夹名称。");
return;
}
downloadHTML();
}
};
if (exportedData.errors.length > 0) {
document.querySelector('#current-exporting').textContent = "导出完成(存在异常)";
} else {
document.querySelector('#current-exporting').textContent = "导出完成";
}
document.querySelector('#refresh-button').classList.add('show');
} else {
exportButton.innerHTML = "开始导出";
exportButton.onclick = startExport;
document.querySelector('#current-exporting').textContent = "点击下方按钮开始导出";
document.querySelector('#refresh-button').classList.remove('show');
}
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();
}
})();