Bilibili Search Filter By Time

add time filter to bilibili search results

// ==UserScript==
// @name         Bilibili Search Filter By Time
// @namespace    https://github.com/KID-joker/userscript
// @version      1.2.0
// @supportURL   https://github.com/KID-joker/userscript/issues
// @description  add time filter to bilibili search results
// @author       KID-joker
// @match        https://search.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico?v=1
// @resource css https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/flatpickr.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/build/md5.min.js
// @grant        GM_log
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    // 设置样式
    let css = `
        @media (max-width: 1099.9px) {
            #i_cecream .video-list-item {
                display:block!important;
            }
        }
        @media (max-width: 1439.9px) {
            #i_cecream .video-list-item {
                display:block!important;
            }
        }
        @media (max-width: 1699.9px) {
            #i_cecream .video-list-item {
                display:block!important;
            }
        }
        @media (max-width: 1919.9px) {
            #i_cecream .video-list-item {
                display:block!important;
            }
        }
        @media (max-width: 2199.9px) {
            #i_cecream .video-list-item {
                display:block!important;
            }
        }
    `
    GM_addStyle(css)
    GM_addStyle(GM_getResourceText('css'));

    // 获取过滤日期
    function getQueryObject(url) {
        url = url == null ? unsafeWindow.location.href : url
        const search = url.substring(url.lastIndexOf('?') + 1)
        const obj = {}
        const reg = /([^?&=]+)=([^?&=]*)/g
        search.replace(reg, (rs, $1, $2) => {
            const name = decodeURIComponent($1)
            let val = decodeURIComponent($2)
            val = String(val)
            obj[name] = val
            return rs
        })
        return obj
    }
    let date = 'none';
    let dateRange = [];
    function getDate() {
        let queryObj = getQueryObject();
        date = queryObj.date || 'none';
        dateRange = queryObj.date_range || [];
        if (date !== 'none') {
            dateRange = dateRange.split('_');
        }

        updateComponent();
    }

    // 返回json结果
    let responseJson = null;
    // 过滤的结果
    let result = [];
    // 日期过滤的页码
    let actualPage = 1;
    // 显示数量
    let actualPageSize = 21;
    // 已经显示的数量
    let showSize = 0;
    // b站对应页面
    let requestPage = 1;
    // 数量
    let pageSize = 0;
    // 没有更多数据
    let finished = false;
    // 最大页码
    let maxPage = 1;
    // 自定义日期选择弹窗
    let fp = null;
    // b站请求超时限制
    const timeout = 10000;
    let startFetch = 0;

    // 重写fetch,拦截fetch请求
    const originFetch = fetch;
    unsafeWindow.fetch = async function (url, options) {
        startFetch = Date.now();
        // 只针对视频搜索接口
        let params = options && options.params;
        if (url.indexOf('x/web-interface/wbi/search/type') > -1 && params.search_type === 'video' && date !== 'none') {
            // 暂停上报
            const originReportObserver = unsafeWindow.reportObserver;
            unsafeWindow.reportObserver = null;
            actualPage = params.page;
            pageSize = params.page_size;
            if (result.length < actualPage * actualPageSize) {
                await requestData(url, options);
            }
            let responseResult = [];
            // 保证有数据显示
            do {
                responseResult = result.slice(showSize, showSize + actualPageSize);
                if(responseResult.length == 0) {
                    showSize = Math.max(0, showSize - actualPageSize);
                }
            } while(responseResult.length == 0 && result.length > 0);
            showSize += responseResult.length;
            let response = new Response();
            response.json = function () {
                return new Promise(resolve => {
                    responseJson.data.page = +actualPage;
                    responseJson.data.result = responseResult;
                    resolve(responseJson);
                })
            }
            setTimeout(() => {
                hidePagenationBtn();
                changePagenationBtn();
            }, 200);
            unsafeWindow.reportObserver = originReportObserver;
            return response;
        } else {
            return originFetch(url, options);
        }
    }

    // 获取vue实例、vue-router实例
    let app = null, router = null, route = null;
    document.addEventListener('DOMContentLoaded', function () {
        app = document.querySelector('#i_cecream').__vue_app__;
        router = app.config.globalProperties.$router;
        route = app.config.globalProperties.$route;
        if (route.name === 'video') {
            insertComponent();
        } else {
            removeComponent();
        }
        router.afterEach(route => {
            if (route.name === 'video') {
                insertComponent();
            } else {
                removeComponent();
            }
        })
        // const vnode = route.matched.find(ele => ele.name == 'video').instances.default._;

        // 重写replace方法,拦截跳转,更新route,初始化数据
        const routerReplace = router.replace;
        router.replace = function (toRoute) {
            // 筛选条件改变
            if (!toRoute.query.date || toRoute.query.date === 'none' || !toRoute.query.page) {
                route = toRoute;
                result = [];
                actualPage = 1;
                showSize = 0;
                requestPage = 1;
                pageSize = 0;
                finished = false;
                return routerReplace.call(this, toRoute);
            }
        }

        // 获取时间筛选
        getDate();
        if (date !== 'none') {
            let searchBtn = document.querySelector('.search-button');
            searchBtn.click();
        }
    })

    // 插入日期过滤组件
    function insertComponent() {
        if (document.querySelector('#date-search-conditions')) {
            return;
        }
        let element = document.createElement('div');
        element.id = 'date-search-conditions';
        element.className = 'search-condition-row';
        element.addEventListener('click', clickDateCondition);
        let fragment = document.createDocumentFragment();
        let list = [{
            name: 'none',
            title: '时间不限'
        }, {
            name: 'day',
            title: '过去1天内'
        }, {
            name: 'week',
            title: '过去1周内'
        }, {
            name: 'month',
            title: '过去1月内'
        }, {
            name: 'year',
            title: '过去1年内'
        }, {
            name: 'custom',
            title: '自定日期范围'
        }]
        list.forEach(function (ele) {
            let button = document.createElement('button');
            button.id = `date-condition-${ele.name}`;
            button.textContent = ele.title;
            button.className = 'vui_button vui_button--tab mt_sm mr_sm';
            button.dataset.datecondition = ele.name;
            fragment.appendChild(button);
        });
        element.appendChild(fragment);
        document.querySelector('.more-conditions').appendChild(element);
    }
    // 移除日期过滤
    function removeComponent() {
        const dateCondition = document.querySelector('#date-search-conditions')
        if (dateCondition) {
            document.querySelector('.more-conditions').removeChild(dateCondition);
        }
    }
    // 更新日期按钮状态
    function updateComponent() {
        const dateCondition = document.querySelector('#date-search-conditions')
        if (dateCondition) {
            [...dateCondition.children].forEach(btn => {
                if (btn.dataset.datecondition == date) {
                    btn.classList.add("vui_button--active")
                } else {
                    btn.classList.remove("vui_button--active");
                }
            })
        }

        const customBtn = document.querySelector('#date-condition-custom');
        if(customBtn) {
            if(date == 'custom') {
                customBtn.textContent = `${formatTime(dateRange[0])}至${formatTime(dateRange[1])}`;
            } else {
                customBtn.textContent = '自定日期范围';
            }
        }
    }

    function routerGo(query) {
        router.replace({
            'name': 'video',
            query
        });
        setTimeout(() => {
            getDate();

            let firstPagenationBtn = document.querySelector('.vui_pagenation--btn-num');
            if(firstPagenationBtn) {
                showSize = 0;
                // 当前为第一页,点击不生效
                if(firstPagenationBtn.classList.contains("vui_button--active")) {
                    let searchBtn = document.querySelector('.search-button');
                    searchBtn.click();
                } else {
                    firstPagenationBtn.click();
                }
            }
        }, 0);
    }

    // 日期过滤点击事件
    function clickDateCondition(evt) {
        let datecondition = evt.target.dataset.datecondition;
        if (datecondition === 'none') {
            // 时间不限
            let { date, date_range, ...query } = route.query;
            routerGo(query);
        } else if (datecondition === 'custom') {
            // 自定义日期范围,弹出日期选择弹窗
            if (!fp) {
                fp = evt.target.flatpickr({
                    clickOpens: false,
                    maxDate: 'today',
                    mode: 'range',
                    onChange: function (selectedDates) {
                        if (selectedDates.length == 2) {
                            let startTime = +selectedDates[0];
                            let endTime = +selectedDates[1];
                            if (startTime == endTime) {
                                endTime += 86400000;
                            }
                            endTime = Math.min(Date.now(), endTime);
                            filterByDate(datecondition, startTime, endTime);
                        }
                    }
                });
            }
            fp.open();
        } else if (datecondition) {
            // 固定日期范围选择
            let endTime = Date.now();
            let timeMap = {
                'day': 86400000,
                'week': 604800000,
                'month': 2592000000,
                'year': 31536000000
            }
            filterByDate(datecondition, endTime - timeMap[datecondition], endTime);
        }
    }

    function filterByDate(datecondition, startTime, endTime) {
        let { page, o, ...query } = route.query;
        query.date = datecondition;
        query.date_range = `${Math.floor(startTime / 1000)}_${Math.floor(endTime / 1000)}`;
        routerGo(query);
    }

    // 隐藏分页按钮
    function hidePagenationBtn() {
        if (date !== 'none') {
            let pagenationBtnList = document.querySelectorAll('.vui_pagenation--btn-num');
            if (pagenationBtnList.length > 0) {
                for (let btn of pagenationBtnList) {
                    btn.style.display = 'none';
                }
            }
            let pagenationText = document.querySelector('.vui_pagenation--extend');
            if (pagenationText) {
                pagenationText.style.display = 'none';
            }
        }
    }
    // 修改下一页按钮状态
    function changePagenationBtn() {
        let pagenationParent = document.querySelector('.vui_pagenation--btns');
        if (pagenationParent) {
            let nextPagenation = pagenationParent.lastChild;
            if (finished && actualPage === maxPage) {
                nextPagenation.className += ' vui_button--disabled';
                nextPagenation.setAttribute('disabled', 'disabled')
            } else {
                nextPagenation.className = nextPagenation.className.replace(' vui_button--disabled', '');
                nextPagenation.removeAttribute('disabled');
            }
        }
    }

    // 请求数据保存
    async function requestData(url, options) {
        while (true) {
            const query = getQueryObject(url);
            query.page = requestPage;
            // 应该是浏览的偏移量,必须跟页码数量保持一致,不然会有重复数据
            query.dynamic_offset = (requestPage - 1) * pageSize;
            // 请求加密
            Object.assign(query, encWbi(query, encWbiKeys));
            const urlObj = new URL(url);
            url = `${urlObj.origin + urlObj.pathname}?${new URLSearchParams(query)}`;
            let _responseJson = await originFetch(url, options).then(response => {
                return response.json();
            }).catch(err => {
                return {
                    error: true
                }
            });
            if(_responseJson.error) {
                return;
            }
            if (_responseJson.data && _responseJson.data.result) {
                if (_responseJson.data.result.length < pageSize) {
                    finished = true;
                    maxPage = actualPage;
                }
                responseJson = _responseJson;
                let list = responseJson.data.result.filter(ele => ele.pubdate >= dateRange[0] && ele.pubdate <= dateRange[1]);
                result = result.concat(list);
            } else {
                finished = true;
                maxPage = actualPage;
            }
            requestPage++;
            /**
             * finished 没有更多数据了
             * result.length >= actualPage * actualPageSize 满足显示个数
             * (Date.now() - startFetch) > 0.8 * timeout 避免超时
             */
            if (finished || result.length >= actualPage * actualPageSize  || (Date.now() - startFetch) > 0.8 * timeout) {
                return;
            } else {
                let time = Math.round(Math.random() * 400) + 600;
                await delay(time);
            }
        }
    }

    // 防止请求频繁,被封ip
    function delay(n) {
        return new Promise(function (resolve) {
            setTimeout(resolve, n);
        });
    }

    function formatTime(timestamp) {
        let date = new Date(timestamp * 1000);
        return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
    }

    // 请求加密
    const encWbiKeys = {
        wbiImgKey: "76e91e21c4df4e16af9467fd6f3e1095",
        wbiSubKey: "ddfca332d157450784b807c59cd7921e"
    }
    function encWbi(st, dt) {
        dt || (dt = {});
        var Et = getWbiKey(dt),
            St = Et.imgKey,
            wt = Et.subKey;
        if (St && wt) {
            for (var xt = getMixinKey(St + wt), kt = Math.round(Date.now() / 1e3), Ht = Object.assign({}, st, {
                wts: kt
            }), Wt = Object.keys(Ht).sort(), zt = [], Xt = /[!'\(\)*]/g, Qt = 0; Qt < Wt.length; Qt++) {
                var Zt = Wt[Qt],
                    an = Ht[Zt];
                an && typeof an == "string" && (an = an.replace(Xt, "")), an != null && zt.push("".concat(
                    encodeURIComponent(Zt), "=").concat(encodeURIComponent(an)))
            }
            var mn = zt.join("&"),
                bn = md5(mn + xt);
            return {
                w_rid: bn,
                wts: kt.toString()
            }
        }
        return null
    }
    function getWbiKey(st) {
        if (st.useAssignKey) return {
            imgKey: st.wbiImgKey,
            subKey: st.wbiSubKey
        };
        var dt = getLocal("wbi_img_url"),
            Et = getLocal("wbi_sub_url"),
            St = dt ? getKeyFromURL(dt) : st.wbiImgKey,
            wt = Et ? getKeyFromURL(Et) : st.wbiSubKey;
        return {
            imgKey: St,
            subKey: wt
        }
    }
    function getLocal(st) {
        try {
            return localStorage.getItem(st)
        } catch (dt) {
            return null
        }
    }
    function getKeyFromURL(st) {
        return st.substring(st.lastIndexOf("/") + 1, st.length).split(".")[0]
    }
    function getMixinKey(st) {
        var dt = [46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39,
            12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63,
            57, 62, 11, 36, 20, 34, 44, 52],
            Et = [];
        return dt.forEach(function (St) {
            st.charAt(St) && Et.push(st.charAt(St))
        }), Et.join("").slice(0, 32)
    }
})();