Greasy Fork is available in English.

Bilibili Search Filter By Time

add time filter to bilibili search results

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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)
    }
})();