Greasy Fork

Greasy Fork is available in English.

哔哩哔哩收藏夹导出

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

目前为 2024-02-21 提交的版本。查看 最新版本

// ==UserScript==
// @name         哔哩哔哩收藏夹导出
// @namespace    https://github.com/AHCorn/Bilibili-Favlist-Export
// @icon         https://www.bilibili.com/favicon.ico
// @version      1.1
// @description  导出哔哩哔哩收藏夹为 CSV 或 HTML 文件,以便导入 Raindrop 或 Firefox。
// @author       AHCorn
// @match        http*://space.bilibili.com/*/*
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_registerMenuCommand
// ==/UserScript==


(function () {
    'use strict';

    const DELAY = 2000;
    let csvHeaderOptions = {
        title: "\uFEFFtitle",
        url: "url",
        foldername: "folder"
    };
    let csvHeaderActive = ["\uFEFFtitle", "url", "folder"];
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>\n<DL><p>`;
    const HTML_TEMPLATE_END = `</DL><p>`;
    let htmlContent = "";
    let globalParentFolderName = "";
    let csvInclude = {
        title: true,
        url: true,
        foldername: true
    };
    const PANEL_STYLE = `
    #Panel {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 320px;
        height: 360px;
        background-color: white;
        border: 1px solid #fff;
        border-radius: 10px;
        box-shadow: 0 0 20px rgba(0,0,0,0.2);
        z-index: 9999;
        display: none;
        transition: opacity 0.3s ease-in-out;
    }
    #progress {
        position: absolute;
        top: 260px;
        left: 35px;
        width: 250px;
        height: 30px;
        border: 1px solid #2196F3;
        border-radius: 10px;
        overflow: hidden;
    }
    #progress > div {
        width: 0%;
        height: 100%;
        background-color: #ff6161;
        transition: width 0.3s ease-in-out;
    }
    #button, #backButton, #confirmButton {
        position: absolute;
        bottom: 25px;
        width: 100px;
        height: 30px;
        background-color: #2196F3;
        border: none;
        border-radius: 10px;
        color: white;
        font-weight: bold;
        cursor: pointer;
        transition: background-color 0.3s ease-in-out;
    }
    #button:hover, #backButton:hover, #confirmButton:hover {
        background-color: #64B5F6;
    }
    #button:active, #backButton:active, #confirmButton:active {
        background-color: #1976D2;
    }
#button {
    width:255px;
        margin-left: 35px;

}
#confirmButton {
    left: calc(50% - 125px);
    display:none!important;
}
    #backButton {
        left: 35px;
    }
    #cancel {
        position: absolute;
        top: 10px;
        right: 10px;
        width: 20px;
        height: 20px;
        border: none;
        border-radius: 50%;
        background-color: #F44336;
        color: white;
        font-size: 16px;
        font-weight: bold;
        text-align: center;
        line-height: 20px;
        cursor: pointer;
        transition: background-color 0.3s ease-in-out;
    }
    #cancel:hover {
        background-color: #EF5350;
    }
    #cancel:active {
        background-color: #E53935;
    }
    #tip, #inputTip {
        position: absolute;
        top: 25px;
        left: 35px;
        width: 250px;
        height: 20px;
        color: #2196F3;
        font-size: 15px;
        font-family: Arial, sans-serif;
        text-align: center;
        line-height: 20px;
    }
    #inputTip {
        top: 235px;
    }
    #formatSelector {
        position: absolute;
        top: 60px;
        left: 35px;
        width: 250px;
        height: 40px;
        background-color: #f0f0f0;
        border: 1px solid #2196F3;
        border-radius: 20px;
        display: flex;
        align-items: center;
        justify-content: space-around;
        padding: 5px;
        box-sizing: border-box;
    }
    .csvOptionButton {
        display: block;
        width: 90%;
        margin: 5px auto;
        padding: 10px;
        border: 1px solid #007bff;
        border-radius: 5px;
        background-color: white;
        color: #3596ff;
        cursor: pointer;
        font-size: 16px;
        transition: all 0.3s ease;
    }
    .csvOptionButton.selected {
    transition: font-weight 1s ease-in-out;
        background-color: #3596ff;
        color: white;
    }
    .formatButton {
    z-index:1;
        display: inline-block;
        width: 110px;
        height: 30px;
        line-height: 30px;
        text-align: center;
        border: none;
        border-radius: 15px;
        background-color: transparent;
        cursor: pointer;
        transition: color 0.3s ease-in-out;
    }
    .formatButton.selected {
        font-weight: bold;
        color: #FFF;
    }
    .slider {
        position: absolute;
        left: 5px;
        top: 5px;
        background-color: #2196F3;
        border-radius: 15px;
        transition: left 0.3s ease-in-out;
        width: 110px;
        height: 30px;
        z-index: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        color: white;
        font-weight: bold;
    }
    .folderInputSection input {
    width:205px!important;
    margin:6px;
    padding:18px!important;
    }
    .folderInputSection, .csvOptionsSection {
        position: absolute;
        top: 110px;
        left: 35px;
        width: 250px;
        display: none;
    }
    .csvOptionsSection > .csvOptionButton {
    width:250px!important;
    }
    .folderInputSection > input, .csvOptionsSection > .csvOptionButton {
        padding: 9px;

        border: 1px solid #ccc;
        border-radius: 5px;
        font-size: 14px;
    }
    .successModal {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 300px;
        padding: 20px;
        background-color: white;
        border: 1px solid #2196F3;
        border-radius: 10px;
        box-shadow: 0 0 20px rgba(0,0,0,0.3);
        z-index: 10000;
        text-align: center;
        display: none;
    }
    `;

    let gen = listGen();
    let panel = null;
    let progress = null;
    let button = null;
    let confirmButton = null;
    let cancel = null;
    let tip = null;
    let inputTip = null;
    let formatSelector = null;
    let slider = null;
    let folderInputSection = null;
    let bookmarkTitleInput = null;
    let globalFolderNameInput = null;
    let csvOptionsSection = null;
    let lastAddedFolderName = "";
    let totalPage = 0;
    let currentPage = 0;
    let isExporting = false;
    let 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 += `</DL><p>\n`;
        }
        htmlContent += `<DT><H3 ADD_DATE="${dateNow}" LAST_MODIFIED="${dateNow}">${folderName}</H3>\n<DL><p>\n`;
        lastAddedFolderName = folderName; 
    }
}

    function addHTMLBookmark(folderName, title, url) {
        addHTMLFolder(folderName);
        let dateNow = getCurrentTimestamp();
        htmlContent += `<DT><A HREF="${url}" ADD_DATE="${dateNow}" LAST_MODIFIED="${dateNow}">${title}</A>\n`;
    }

function generateCSVLine(folderName, title, url) {
    let parts = [];
    if (csvInclude.title) parts.push(escapeCSV(title));
    if (csvInclude.url) parts.push(escapeCSV(url));
    if (csvInclude.foldername) parts.push(escapeCSV(folderName));
    return parts.join(',');
}


    function getVideosFromPage() {
        var results = [];
        var folderName = getFolderName().replace(/\//g, '\\');
        $(".fav-video-list > li > a.title").each(function () {
            var title = $(this).text().replace(/,/g, '');
            if (title !== "已失效视频") {
                var url = 'https:' + $(this).attr("href");
                results.push(generateCSVLine(folderName, title, url));
                if (exportFormat === "html") {
                    addHTMLBookmark(folderName, title, url);
                }
            }
        });
        return results.join('\n');
    }

    function processVideos () {
        if (isExporting) {
            csvContent += getVideosFromPage () + '\n';
            currentPage++;
            updateProgress ();
            if ($(".be-pager-next:visible").length == 0) {
                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) {
        let list = gen.next().value;
        if (list) {
            list.click();
            setTimeout(() => {
                totalPage = parseInt($(".be-pager-total").text().match(/\d+/)[0]);
                currentPage = 0;
                updateProgress();
                updateTip();
                processVideos();
            }, DELAY);
        } else {
            isExporting = false;
            button.textContent = "立即下载";
            button.disabled = false;
            button.onclick = () => {
                if (exportFormat === "csv") {
                    downloadCSV();
                } else if (exportFormat === "html") {
                    downloadHTML();
                }
                button.textContent = "开始导出";
                button.disabled = true;
                setTimeout(() => {
                    button.disabled = false;
                }, 3000);
            };

        }
    }
}


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 = `
<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() {
    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>
<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 = "Panel";
        document.body.appendChild(panel);

        formatSelector = document.createElement("div");
        formatSelector.id = "formatSelector";
        slider = document.createElement("div");
        slider.className = "slider";
        formatSelector.appendChild(slider);
        let csvButton = document.createElement("div");
        csvButton.className = "formatButton selected";
        csvButton.textContent = "CSV";
        csvButton.addEventListener("click", () => {
            exportFormat = "csv";
            slider.style.left = "9px";
            csvButton.classList.add("selected");
            htmlButton.classList.remove("selected");
            folderInputSection.style.display = "none";
            csvOptionsSection.style.display = "block";
            confirmButton.style.display = "block";
        });
        let htmlButton = document.createElement("div");
        htmlButton.className = "formatButton";
        htmlButton.textContent = "HTML";
        htmlButton.addEventListener("click", () => {
            exportFormat = "html";
            slider.style.left = "126px";
            htmlButton.classList.add("selected");
            csvButton.classList.remove("selected");
            folderInputSection.style.display = "block";
            csvOptionsSection.style.display = "none";
            confirmButton.style.display = "block";
        });
        formatSelector.appendChild(csvButton);
        formatSelector.appendChild(htmlButton);
        panel.appendChild(formatSelector);

        folderInputSection = document.createElement("div");
        folderInputSection.className = "folderInputSection";
        bookmarkTitleInput = document.createElement("input");
        bookmarkTitleInput.placeholder = "书签标题 (H1)";
        folderInputSection.appendChild(bookmarkTitleInput);
        globalFolderNameInput = document.createElement("input");
        globalFolderNameInput.placeholder = "全局父文件夹名称";
        folderInputSection.appendChild(globalFolderNameInput);
        panel.appendChild(folderInputSection);

        csvOptionsSection = document.createElement("div");
        csvOptionsSection.className = "csvOptionsSection";
        csvOptionsSection.style.display = "block";
        ["title", "url", "foldername"].forEach((option) => {
            let button = document.createElement("button");
            button.className = "csvOptionButton" + (csvInclude[option] ? " selected" : "");
            button.textContent = option;
            button.onclick = () => {
                csvInclude[option] = !csvInclude[option];
                button.classList.toggle("selected");
                updateCSVHeader();
                csvContent = csvHeaderActive.join(",") + "\n";
            };
            csvOptionsSection.appendChild(button);
        });
        panel.appendChild(csvOptionsSection);

        confirmButton = document.createElement("button");
        confirmButton.id = "confirmButton";
        confirmButton.textContent = "确认";
        confirmButton.style.display = "none";
        confirmButton.onclick = () => {
            if (exportFormat === "html" && (!bookmarkTitleInput.value || !globalFolderNameInput.value)) {
                alert("请配置书签标题和全局父文件夹名称。");
                return;
            }
            showSuccessModal("设置已保存成功!");
        };
        panel.appendChild(confirmButton);
    }

    function createProgress() {
        progress = document.createElement("div");
        progress.id = "progress";
        let bar = document.createElement("div");
        progress.appendChild(bar);
        panel.appendChild(progress);
    }

    function createButton() {
        button = document.createElement("button");
        button.id = "button";
        button.textContent = "开始导出";
        button.onclick = () => {
            if (exportFormat === "html" && (!bookmarkTitleInput.value || !globalFolderNameInput.value)) {
                alert("请配置书签标题和全局父文件夹名称。");
                return;
            }
            button.disabled = true;
            button.textContent = "导出中...";
            isExporting = true;
            htmlContent = "";
            csvContent = csvHeaderActive.join(",") + "\n";
            changeList();
        };
        panel.appendChild(button);
    }

    function createCancel() {
        cancel = document.createElement("button");
        cancel.id = "cancel";
        cancel.textContent = "×";
        cancel.onclick = () => {
            isExporting = false;
            button.disabled = false;
            button.textContent = "开始导出";
            hidePanel();
        };
        panel.appendChild(cancel);
    }

    function createTip() {
        tip = document.createElement("div");
        tip.id = "tip";
        tip.textContent = "当前正在导出:";
        panel.appendChild(tip);
    }
//废弃
    function showSuccessModal(message) {
        let modal = document.createElement("div");
        modal.className = "successModal";
        modal.textContent = message;
        document.body.appendChild(modal);
        modal.style.display = "block";
        setTimeout(() => {
            modal.style.opacity = 0;
            setTimeout(() => {
                document.body.removeChild(modal);
            }, 500);
        }, 2000);
    }

    function showPanel() {
        panel.style.opacity = 0;
        panel.style.display = "block";
        setTimeout(() => {
            panel.style.opacity = 1;
        }, 0);
        confirmButton.style.display = exportFormat === "html" ? "block" : "block";
    }

    function hidePanel() {
        panel.style.opacity = 0;
        setTimeout(() => {
            panel.style.display = "none";
        }, 300);
    }

    function updateProgress() {
        let percentage = Math.round(currentPage / totalPage * 100) + "%";
        progress.querySelector("div").style.width = percentage;
        progress.title = percentage;
    }

    function updateTip() {
        let folderName = getFolderName();
        tip.textContent = "当前正在导出:" + folderName;
    }

    function init() {
        GM_addStyle(PANEL_STYLE);
        createPanel();
        createProgress();
        createButton();
        createCancel();
        createTip();
      updateCSVHeader();
        GM_registerMenuCommand("导出 Bilibili 收藏夹", showPanel);
    }

    if (location.href.includes("https://space.bilibili.com/") && location.href.includes("/favlist")) {
        init();
    }
})();