BiliBili-文字搜索列表

(感谢闲鱼买家ZEP的有偿定制) 用文字列表的方式展示B站搜索结果,方便按各列排序。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                BiliBili-TextSearchList
// @name:zh-CN          BiliBili-文字搜索列表
// @namespace           https://github.com/Mehver
// @version             1.0
// @description         (Thanks to ZEP's paid customization) Display Bilibili search results in a text list, which is convenient for sorting by each column.
// @description:zh-CN   (感谢闲鱼买家ZEP的有偿定制) 用文字列表的方式展示B站搜索结果,方便按各列排序。
// @sponsor             ZEP
// @author              https://github.com/Mehver
// @icon                data:image/png;base64,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
// @match               http*://search.bilibili.com/*
// @license             MPL-2.0
// @license^            Mozilla Public License 2.0
// @charset		        UTF-8
// @homepageURL         https://github.com/SynRGB/BiliBili-TextSearchList
// @contributionURL     https://github.com/SynRGB/BiliBili-TextSearchList
// @copyright           Copyright © 2022-PRESENT, Mehver (https://github.com/Mehver)
// @grant               GM_addStyle
// @grant               GM_getResourceText
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_registerMenuCommand
// @resource            DataTablesCSS https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css
// ==/UserScript==

let table_font_size = GM_getValue('table_font_size', 16);

GM_registerMenuCommand('设置表格字体大小', async () => {
    let newFontSize = prompt('请输入新的字体大小(单位px):', table_font_size);
    if (newFontSize) {
        table_font_size = newFontSize;
        GM_setValue('table_font_size', table_font_size);
        alert('字体大小已更新!请刷新页面以查看更改。');
    }
});

//////////////////////////////////////
//////////// DataTables //////////////
let cssTxt = GM_getResourceText("DataTablesCSS");
GM_addStyle(cssTxt);
let head = document.head || document.getElementsByTagName('head')[0];
let link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = 'https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css';
head.appendChild(link);
(function() {
    let jQueryScript = document.createElement("script");
    jQueryScript.src = "https://code.jquery.com/jquery-3.6.0.min.js";
    jQueryScript.onload = () => {
        let dtScript = document.createElement("script");
        dtScript.src = "https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js";
        // 加载完成后首次运行
        dtScript.onload = () => {
            main();
        };
        document.body.appendChild(dtScript);
    };
    document.body.appendChild(jQueryScript);
})();
//////////// DataTables //////////////
//////////////////////////////////////

//////////////////////////////////////
/////////////// 触发器 ////////////////
// 延时避免在 dtScript 和 jQueryScript 加载完成前就运行
setTimeout(function() {
    // if DOM changed, re-run the script
    let observer = new window.MutationObserver(function (mutations) {
        mutations.forEach(function () {
            const biliResultsTable = document.querySelector('#biliResultsTable');
            if (biliResultsTable === null) {
                main();
            }
        });
    });
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
}, 1000);
/////////////// 触发器 ////////////////
//////////////////////////////////////

/////////////////////////////////////
/////////////// main ////////////////
function main() {
    // Create table with thead for DataTables
    let table = document.createElement('table');
    table.id = "biliResultsTable";
    let thead = document.createElement('thead');
    let tbody = document.createElement('tbody');
    let header = ["发布日期", "时长", "标题", "播放量", "UP主"];
    let trHead = document.createElement('tr');
    header.forEach(text => {
        let th = document.createElement('th');
        th.textContent = text;
        trHead.appendChild(th);
    });
    thead.appendChild(trHead);

    setTimeout(function() {
        let videoCards = document.querySelectorAll('.bili-video-card');
        videoCards.forEach(videoCard => {
            let title = videoCard.querySelector('.bili-video-card__info--tit')?.textContent.trim();
            let up = videoCard.querySelector('.bili-video-card__info--author')?.textContent.trim();
            let playCount = videoCard.querySelector('.bili-video-card__stats--item > span')?.textContent.trim();
            let danmakuCount = videoCard.querySelectorAll('.bili-video-card__stats--item > span')[1]?.textContent.trim();
            let duration = videoCard.querySelector('.bili-video-card__stats__duration')?.textContent.trim();
            let date = videoCard.querySelector('.bili-video-card__info--date')?.textContent.trim().replace('· ', '');
            let link_video = videoCard.querySelectorAll('a')[0].getAttribute('href');
            // UP主的link在targetDiv1页面有概率玄学报错,应该是异步逻辑的问题,用try之后基本正常
            let link_up;
            try { link_up = videoCard.querySelectorAll('a')[2].getAttribute('href'); } catch (e) {}

            let tr = document.createElement('tr');

            // 确保没有为空的数据
            if (
                (title !== undefined) &&
                (up !== undefined) &&
                (playCount !== undefined) &&
                (danmakuCount !== undefined) &&
                (duration !== undefined) &&
                (date !== undefined) &&
                (link_video !== undefined)
            ) {
                [date, duration, title, playCount, up].forEach(text => {
                    let td = document.createElement('td');
                    td.textContent = text;
                    tr.appendChild(td);
                });
                let tdTitle = tr.querySelector('td:nth-child(3)');
                tdTitle.innerHTML = `<a href="${link_video}" target="_blank">${title}</a>`;
                let tdUp = tr.querySelector('td:nth-child(5)');
                tdUp.innerHTML = `<a href="${link_up}" target="_blank">${up}</a>`;
                // b230815.02 时长加粗
                tr.querySelector('td:nth-child(2)').style.fontWeight = 'bold';
                // b230815.02 标题用 `#00AEEC` 颜色
                tr.querySelector('td:nth-child(3)').style.color = '#00AEEC';
                tbody.appendChild(tr);
            }
        });
        // console.log(videoCards);
        table.appendChild(thead);
        table.appendChild(tbody);

        // 回调获取异步数据,适用于下方 targetDiv2 的异步加载
        if (tbody.childElementCount === 0) {
            main();
            return;
        }

        // 搜索第一页的 DOM 结构与其他页不同,直接用这种方式即可匹配
        let targetDiv1 = document.querySelector("#i_cecream > div > div:nth-child(2) > div.search-content.search-content--gray > div > div > div > div.video.i_wrapper.search-all-list");
        if (targetDiv1 !== null) {
            targetDiv1.innerHTML = '';
            targetDiv1.appendChild(table);
        } else {
            // 匹配除第一页外的其他 DOM 结构,第一页返回的结果是一次性全部加载的,而其他页是异步加载的
            let targetDiv2_rm = document.querySelector("#i_cecream > div > div:nth-child(2) > div.search-content--gray.search-content > div > div > div.video-list.row");
            targetDiv2_rm.innerHTML = '';
            let targetDiv2_bott = document.querySelector("#i_cecream > div > div:nth-child(2) > div.search-content--gray.search-content > div > div > div.flex_center.mt_x50.mb_lg");
            targetDiv2_bott.parentNode.insertBefore(table, targetDiv2_bott);
            // 每次翻页时,上一页的表格不会自动被覆盖而是叠加在一起,所以需要手动删除
            let tables = document.querySelectorAll('#biliResultsTable');
            if (tables.length > 1) {
                tables.forEach(table => {
                    if (table !== tables[0]) {
                        table.remove();
                    }
                });
            }
        }


        // DataTables 的自定义排序算法
        $.fn.dataTable.ext.type.order['duration-sort-pre'] = function (d) {return convertDurationToSeconds(d);};
        $.fn.dataTable.ext.type.order['playcount-sort-pre'] = function (d) {return convertPlayCount(d);};
        $.fn.dataTable.ext.type.order['date-sort-pre'] = function (d) {return convertDate(d);};
        // Initialize DataTables
        $(table).DataTable({
            "paging": false,
            "searching": false,
            "info": false,
            "columnDefs": [
                { "type": "playcount-sort", "targets": 3 },
                { "type": "duration-sort", "targets": 1 },
                { "type": "date-sort", "targets": 0 }
            ]
        });

        // b230815.02 去掉底边横线
        GM_addStyle("table.dataTable.no-footer { border-bottom: 0px none !important; }");
        // b230815.02 去掉表头横线 (因CSS复杂,所以创建白色色块覆盖)
        GM_addStyle(".dataTable thead th { border-bottom: 0px none !important; }");
        // b230815.02 调大字号
        GM_addStyle(`.dataTable { font-size: ${table_font_size}px !important; }`);

    }, 100);
}
/////////////// main ////////////////
/////////////////////////////////////

///////////////////////////////////
/////// DataTable 的排序算法 ////////
function convertDurationToSeconds(duration) {
    let parts = duration.split(':').map(part => parseInt(part, 10));
    if (parts.length === 3) {
        return parts[0] * 3600 + parts[1] * 60 + parts[2];
    } else if (parts.length === 2) {
        return parts[0] * 60 + parts[1];
    } else {
        return NaN;
    }
}
function convertPlayCount(playCount) {
    if (playCount.includes('万')) {
        return parseFloat(playCount.replace('万', '')) * 10000;
    } else {
        return parseInt(playCount, 10);
    }
}
function convertDate(date) {
    const now = new Date();
    if (date.includes('小时前')) {
        const hoursAgo = parseFloat(date.replace('小时前', ''));
        return now - hoursAgo * 3600 * 1000; // Convert hours to milliseconds
    }
    if (date === "昨天") {
        return now - 24 * 3600 * 1000; // 24 hours in milliseconds
    }
    if (date.includes('-')) {
        const parts = date.split('-').map(part => {
            return part.padStart(2, '0');
        });
        // If only month and day are given, use the current year.
        if (parts.length === 2) {
            parts.unshift(now.getFullYear().toString());
        }
        // Create a new Date object and return its time value in milliseconds
        return new Date(parts.join('-')).getTime();
    }
}
/////// DataTable 的排序算法 ////////
///////////////////////////////////

console.log("JS script BiliBili-TextSearchList (BiliBili-文字搜索列表) loaded. See more details at https://github.com/SynRGB/BiliBili-TextSearchList");