// ==UserScript==
// @name Bili - 搜索后,进行二次排序
// @description b站视频搜索,先用官方综合排序,再进行二次排序
// @namespace https://bilibili.com/
// @version 3.0
// @author 会飞的蛋蛋面
// @license none
// @match *://search.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.8/axios.min.js
// @run-at document-end
// ==/UserScript==
;(async () => {
"use strict"
const loadedVideos = new Set() // 用于存储已加载视频的ID,避免重复
const currentYear = new Date().getFullYear()
// DOM 初始化和按钮事件绑定
function initDOM() {
const buttonsHtml = $(
`
<div class="conditions-order flex_between my-div" style="height: 50px;">
<div class="search-condition-row" data-v-1276c9ec>
<button id="btn-totalrank" class="vui_button vui_button--tab vui_button--active mr_sm" data-sort-type="totalrank">二次排序</button>
<button id="btn-play" class="vui_button vui_button--tab mr_sm" data-sort-type="play">最多播放</button>
<button id="btn-pubdate" class="vui_button vui_button--tab mr_sm" data-sort-type="pubdate">最新发布</button>
<button id="btn-danmaku" class="vui_button vui_button--tab mr_sm" data-sort-type="danmaku">最多弹幕</button>
<button id="btn-duration" class="vui_button vui_button--tab mr_sm" data-sort-type="duration">最长时长</button>
</div>
</div>
`,
)
buttonsHtml.insertAfter($(".conditions-order").first())
return buttonsHtml.find(".vui_button")
}
// 主逻辑
function main() {
const $filters = initDOM()
// 添加每个按钮的 onclick 事件
$("#btn-totalrank").on("click", function () {
activateFilter($filters, $(this))
const { $videoList, videoItems } = getVideoItems()
videoItems.sort((a, b) => {
const scoreA = parseInt($(a).find(".bili-video-card__stats--item").first().attr("data-rank-score"), 10) || 0
const scoreB = parseInt($(b).find(".bili-video-card__stats--item").first().attr("data-rank-score"), 10) || 0
return scoreB - scoreA
})
$videoList.empty().append(videoItems)
})
$("#btn-play").on("click", function () {
activateFilter($filters, $(this))
const { $videoList, videoItems } = getVideoItems()
videoItems.sort(
(a, b) =>
convertToNumber($(b).find(".bili-video-card__stats--item span").first().text()) -
convertToNumber($(a).find(".bili-video-card__stats--item span").first().text()),
)
$videoList.empty().append(videoItems)
})
$("#btn-pubdate").on("click", function () {
activateFilter($filters, $(this))
const { $videoList, videoItems } = getVideoItems()
videoItems.sort((a, b) => {
const dateTextB = $(b).find(".bili-video-card__info--date").text().trim().replace("·", "").trim()
const dateTextA = $(a).find(".bili-video-card__info--date").text().trim().replace("·", "").trim()
return getPublishDateFromText(dateTextB) - getPublishDateFromText(dateTextA)
})
$videoList.empty().append(videoItems)
})
// 解析并获取发布日期的时间戳
function getPublishDateFromText(dateText) {
if (!dateText) return 0
const parts = dateText.split("-")
let year, month, day
if (parts.length === 3) {
// 包含年份,例如 "2023-12-25"
;[year, month, day] = parts.map(Number)
} else if (parts.length === 2) {
// 不包含年份,例如 "12-25",假设是当前年份
year = currentYear
;[month, day] = parts.map(Number)
} else {
return 0
}
if (isNaN(year) || isNaN(month) || isNaN(day)) return 0
const date = new Date(year, month - 1, day)
return date.getTime()
}
$("#btn-danmaku").on("click", function () {
activateFilter($filters, $(this))
const { $videoList, videoItems } = getVideoItems()
videoItems.sort(
(a, b) =>
convertToNumber($(b).find(".bili-video-card__stats--item").eq(1).find("span").last().text()) -
convertToNumber($(a).find(".bili-video-card__stats--item").eq(1).find("span").last().text()),
)
$videoList.empty().append(videoItems)
})
$("#btn-duration").on("click", function () {
activateFilter($filters, $(this))
const { $videoList, videoItems } = getVideoItems()
videoItems.sort(
(a, b) =>
convertDurationToSeconds($(b).find(".bili-video-card__stats__duration").text()) -
convertDurationToSeconds($(a).find(".bili-video-card__stats__duration").text()),
)
$videoList.empty().append(videoItems)
})
// 转换时长字符串为秒数
function convertDurationToSeconds(duration) {
if (!duration) return 0
const parts = duration.split(":").map(part => parseInt(part, 10))
let seconds = 0
if (parts.length === 3) {
seconds = parts[0] * 3600 + parts[1] * 60 + parts[2]
} else if (parts.length === 2) {
seconds = parts[0] * 60 + parts[1]
}
return seconds
}
// 添加“更多”按钮
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
const $container = $(".vui_pagenation--btns")
if ($container.length && !$container.data("more-button-added")) {
const moreButtonEl = $('<button class="vui_button vui_pagenation--btn vui_pagenation--btn-side more">更多</button>')
$container.append(moreButtonEl)
$container.data("more-button-added", true)
moreButtonEl.on("click", () => loadMoreVideos($filters))
}
}
})
})
observer.observe(document.body, { childList: true, subtree: true })
}
// 初始化页面并收集初始视频ID
main()
collectInitialVideoIDs()
// 获取视频列表和视频项
function getVideoItems() {
const $videoList = $(".video-list").first()
const videoItems = $videoList.children(".col_3.col_xs_1_5.col_md_2.col_xl_1_7.mb_x40").toArray()
return { $videoList, videoItems }
}
// 收集初始视频ID
function collectInitialVideoIDs() {
$(".bili-video-card__wrap.__scale-wrap > a").each((_, element) => {
const videoUrl = $(element).attr("href")
const match = videoUrl.match(/\/video\/(av\d+|BV[a-zA-Z0-9]+)/)
if (match) {
loadedVideos.add(match[1])
}
})
}
// 激活选中的筛选按钮
function activateFilter($filters, $activeFilter) {
$filters.removeClass("vui_button--active")
$activeFilter.addClass("vui_button--active")
}
// 转换数字字符串为数值
function convertToNumber(text) {
let num = text.replace(/,/g, "").replace("万", "0000")
if (text.includes("万")) {
num = parseFloat(text.replace("万", "")) * 10000
} else {
num = parseFloat(text)
}
return isNaN(num) ? 0 : num
}
// 加载更多视频
let addedPage = 1 // 默认第2页
function loadMoreVideos($filters) {
const activeSortType = $filters.filter(".vui_button--active").data("sort-type")
const currentPage = parseInt($(".vui_pagenation--btns .vui_button--active-blue.vui_pagenation--btn-num").text(), 10)
// 动态调整order参数
const order = activeSortType || "totalrank"
axios
.get("https://api.bilibili.com/x/web-interface/search/type", {
params: {
context: "",
page: currentPage + addedPage,
order: order,
keyword: $(".search-input-el").val(),
duration: 0,
tids_2: "",
__refresh__: true,
_extra: "",
search_type: "video",
tids: 0,
highlight: 1,
single_column: 0,
},
headers: { Accept: "application/json, text/plain, */*" },
withCredentials: true,
})
.then(response => {
const results = response.data.data.result
addedPage++
results.forEach(videoData => {
if (loadedVideos.has(videoData.bvid)) return // 视频已经加载过了
const $videoElement = createVideoElement(videoData)
$(".video-list").first().append($videoElement)
loadedVideos.add(videoData.bvid)
})
})
.catch(error => {
console.error("加载更多视频时出错:", error)
})
}
// 创建视频元素
function createVideoElement(videoData) {
const formatNumber = num => (num >= 10000 ? (num / 10000).toFixed(1) + "万" : num.toString())
const formatDate = timestamp => {
if (!timestamp) return "未知日期"
const date = new Date(timestamp * 1000)
const year = date.getFullYear() === currentYear ? "" : date.getFullYear() + "-"
const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, "0")
return `${year}${month}-${day}`
}
const formatDuration = duration => {
if (!duration) return "00:00"
const parts = duration.split(":").map(part => parseInt(part, 10))
let formatted = ""
if (parts.length === 3) {
const [h, m, s] = parts
formatted = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`
} else if (parts.length === 2) {
const [m, s] = parts
formatted = `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`
}
return formatted
}
const DATA_V_MAIN = "data-v-28552bfb"
const DATA_V_SUB = "data-v-4caf9c8c"
return $(`
<div class="col_3 col_xs_1_5 col_md_2 col_xl_1_7 mb_x40" ${DATA_V_MAIN}>
<div class="bili-video-card" ${DATA_V_MAIN} ${DATA_V_SUB}>
<div class="hide bili-video-card__skeleton" ${DATA_V_SUB}>
<div class="bili-video-card__skeleton--cover" ${DATA_V_SUB}></div>
<div class="bili-video-card__skeleton--info" ${DATA_V_SUB}>
<div class="bili-video-card__skeleton--right" ${DATA_V_SUB}>
<p class="bili-video-card__skeleton--text" ${DATA_V_SUB}></p>
<p class="bili-video-card__skeleton--text short" ${DATA_V_SUB}></p>
<p class="bili-video-card__skeleton--light" ${DATA_V_SUB}></p>
</div>
</div>
</div>
<div class="bili-video-card__wrap __scale-wrap" ${DATA_V_SUB}>
<a href="${videoData.arcurl}" target="_blank" ${DATA_V_SUB} data-mod="search-card">
<div class="bili-video-card__image __scale-player-wrap" ${DATA_V_SUB}>
<div class="bili-video-card__image--wrap" ${DATA_V_SUB}>
<picture class="v-img bili-video-card__cover" ${DATA_V_SUB}>
<img src="${videoData.pic}" alt="${videoData.title}" loading="lazy">
</picture>
<div class="bili-video-card__mask" ${DATA_V_SUB}>
<div class="bili-video-card__stats" ${DATA_V_SUB}>
<div class="bili-video-card__stats--left" ${DATA_V_SUB}>
<span class="bili-video-card__stats--item" ${DATA_V_SUB}>
<svg class="bili-video-card__stats--icon" ${DATA_V_SUB} fill="#ffffff" viewBox="0 0 24 24" width="24" height="24">
<!-- Play Icon SVG Path -->
<path d="M12 4.99805C9.48178 4.99805 7.283 5.12616 5.73089 5.25202C4.65221 5.33949 3.81611 6.16352 3.72 7.23254C3.60607 8.4998 3.5 10.171 3.5 11.998C3.5 13.8251 3.60607 15.4963 3.72 16.76355C3.81611 17.83255 4.65221 18.6566 5.73089 18.7441C7.283 18.8699 9.48178 18.998 12 18.998C14.5185 18.998 16.7174 18.8699 18.2696 18.74405C19.3481 18.65655 20.184 17.8328 20.2801 16.76405C20.394 15.4973 20.5 13.82645 20.5 11.998C20.5 10.16965 20.394 8.49877 20.2801 7.23205C20.184 6.1633 19.3481 5.33952 18.2696 5.25205C16.7174 5.12618 14.5185 4.99805 12 4.99805zM5.60965 3.75693C7.19232 3.62859 9.43258 3.49805 12 3.49805C14.5677 3.49805 16.8081 3.62861 18.3908 3.75696C20.1881 3.90272 21.6118 5.29278 21.7741 7.09773C21.8909 8.3969 22 10.11405 22 11.998C22 13.88205 21.8909 15.5992 21.7741 16.8984C21.6118 18.7033 20.1881 20.09335 18.3908 20.23915C16.8081 20.3675 14.5677 20.498 12 20.498C9.43258 20.498 7.19232 20.3675 5.60965 20.2392C3.81206 20.0934 2.38831 18.70295 2.22603 16.8979C2.10918 15.5982 2 13.8808 2 11.998C2 10.1153 2.10918 8.39787 2.22603 7.09823C2.38831 5.29312 3.81206 3.90269 5.60965 3.75693z" fill="currentColor"></path>
<path d="M14.7138 10.96875C15.50765 11.4271 15.50765 12.573 14.71375 13.0313L11.5362 14.8659C10.74235 15.3242 9.75 14.7513 9.75001 13.8346L9.75001 10.1655C9.75001 9.24881 10.74235 8.67587 11.5362 9.13422L14.7138 10.96875z" fill="currentColor"></path>
</svg>
<span ${DATA_V_SUB}>${formatNumber(videoData.play)}</span>
</span>
<span class="bili-video-card__stats--item" ${DATA_V_SUB}>
<svg class="bili-video-card__stats--icon" ${DATA_V_SUB}>
<use xlink:href="#widget-video-danmaku"></use>
</svg>
<span ${DATA_V_SUB}>${formatNumber(videoData.danmaku)}</span>
</span>
</div>
<span class="bili-video-card__stats__duration" ${DATA_V_SUB}>${formatDuration(videoData.duration)}</span>
</div>
</div>
</div>
</div>
<div class="bili-video-card__info __scale-disable" ${DATA_V_SUB}>
<div class="bili-video-card__info--right" ${DATA_V_SUB}>
<h3 class="bili-video-card__info--tit" ${DATA_V_SUB}>${videoData.title}</h3>
<p class="bili-video-card__info--bottom" ${DATA_V_SUB}>
<a class="bili-video-card__info--owner" href="//space.bilibili.com/${
videoData.mid
}" target="_blank" ${DATA_V_SUB} data-mod="search-card" data-idx="all" data-ext="click">
<svg class="bili-video-card__info--author-ico mr_2" ${DATA_V_SUB}>
<use xlink:href="#widget-up"></use>
</svg>
<span class="bili-video-card__info--author" ${DATA_V_SUB}>${videoData.author}</span>
<span class="bili-video-card__info--date" ${DATA_V_SUB}> · ${videoData.pubdate ? formatDate(videoData.pubdate) : "未知日期"}</span>
</a>
</p>
</div>
</div>
</a>
</div>
</div>
`)
}
})()