// ==UserScript==
// @name B站热门视频重排
// @namespace bilibili-popular-reorder
// @version 1.0
// @description 在热门页nav-tabs添加分栏,重新排序热门视频
// @match *://www.bilibili.com/v/popular*
// @grant GM_xmlhttpRequest
// @connect api.bilibili.com
// ==/UserScript==
(function () {
'use strict';
const MAX_PAGES = 50;
const PAGE_SIZE = 20;
let videoList = [];
let isActive = false;
let myContainer = null;
let tabObserver = null;
let currentLayout = 'grid'; // grid | list
let currentSort = 'view';
let currentMinDuration = 0; // 秒
// ---- 工具函数 ----
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function formatNum(n) {
if (n >= 100000000) return (n / 100000000).toFixed(1) + '亿';
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
return String(n);
}
function formatDuration(seconds) {
if (!seconds && seconds !== 0) return '00:00';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const mm = String(m).padStart(2, '0');
const ss = String(s).padStart(2, '0');
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
}
function gmFetch(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
anonymous: false,
onload: (resp) => {
console.log(`[热门重排] ${url} status=${resp.status} body=${resp.responseText.substring(0, 200)}`);
try { resolve(JSON.parse(resp.responseText)); }
catch (e) { reject(e); }
},
onerror: (err) => {
console.error(`[热门重排] 请求失败 ${url}`, err);
reject(err);
},
});
});
}
// ---- nav-tabs 添加入口 ----
function addNavItem() {
const navTabs = document.querySelector('.nav-tabs');
if (!navTabs || document.getElementById('nav-popular-reorder')) return;
const div = document.createElement('div');
div.id = 'nav-popular-reorder';
div.className = 'nav-tabs__item';
div.innerHTML = '
';
div.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
navTabs.querySelectorAll('.nav-tabs__item').forEach(item => {
item.classList.remove('nav-tabs__item--active');
});
div.classList.add('nav-tabs__item--active');
activateReorder();
});
navTabs.appendChild(div);
navTabs.addEventListener('click', (e) => {
const target = e.target.closest('.nav-tabs__item');
if (!target || target.id === 'nav-popular-reorder') return;
restoreOriginal();
}, true);
tabObserver = new MutationObserver(() => {
const activeTab = navTabs.querySelector('.nav-tabs__item--active');
if (activeTab && activeTab.id !== 'nav-popular-reorder' && isActive) {
restoreOriginal();
}
});
tabObserver.observe(navTabs, { subtree: true, attributes: true, attributeFilter: ['class'] });
}
// ---- 激活重排视图 ----
function activateReorder() {
if (isActive) return;
isActive = true;
const container = document.querySelector('.popular-video-container.popular-list');
if (!container) return;
// 隐藏原始容器,不破坏Vue DOM
container.style.display = 'none';
// 创建自己的容器,插入到原始容器后面
myContainer = document.createElement('div');
myContainer.id = 'pr-container';
myContainer.innerHTML = buildReorderHTML();
container.parentNode.insertBefore(myContainer, container.nextSibling);
myContainer.querySelector('#pr-sort').addEventListener('change', (e) => {
currentSort = e.target.value;
renderVideos(currentSort);
});
myContainer.querySelector('#pr-duration').addEventListener('change', (e) => {
currentMinDuration = Number(e.target.value);
renderVideos(currentSort);
});
myContainer.querySelector('#pr-layout-grid').addEventListener('click', () => {
currentLayout = 'grid';
updateLayoutToggle();
applyGridLayout();
});
myContainer.querySelector('#pr-layout-list').addEventListener('click', () => {
currentLayout = 'list';
updateLayoutToggle();
applyListLayout();
});
myContainer.querySelector('#pr-refresh').addEventListener('click', () => {
videoList = [];
fetchPopularVideos();
});
if (videoList.length > 0) {
// 有缓存,直接渲染
const status = myContainer.querySelector('#pr-status');
if (status) status.style.display = 'none';
renderVideos(currentSort);
} else {
fetchPopularVideos();
}
}
function restoreOriginal() {
if (!isActive) return;
isActive = false;
// 移除"热门重排"的激活样式
const navItem = document.getElementById('nav-popular-reorder');
if (navItem) navItem.classList.remove('nav-tabs__item--active');
// 移除自己的容器
if (myContainer) {
myContainer.remove();
myContainer = null;
}
// 恢复原始容器显示
const container = document.querySelector('.popular-video-container.popular-list');
if (container) container.style.display = '';
}
function buildReorderHTML() {
return `
热门视频重排 — 按不同维度重新排序
加载中 0/最大${MAX_PAGES} 页...
`;
}
function updateLayoutToggle() {
const gridBtn = document.querySelector('#pr-layout-grid');
const listBtn = document.querySelector('#pr-layout-list');
if (currentLayout === 'grid') {
gridBtn.classList.add('pr-layout-btn--active');
listBtn.classList.remove('pr-layout-btn--active');
} else {
listBtn.classList.add('pr-layout-btn--active');
gridBtn.classList.remove('pr-layout-btn--active');
}
}
function applyGridLayout() {
const grid = document.querySelector('#pr-grid');
if (!grid) return;
grid.className = 'pr-grid--grid';
grid.style.gridTemplateColumns = '';
// 重新渲染以切换卡片模式
renderVideos(currentSort);
}
function applyListLayout() {
const grid = document.querySelector('#pr-grid');
if (!grid) return;
grid.className = 'pr-grid--list';
grid.style.gridTemplateColumns = '';
renderVideos(currentSort);
}
// ---- 数据获取(串行 + offset 游标翻页) ----
async function fetchPopularVideos() {
let allVideos = [];
let offset = '';
let loadedPages = 0;
let noMore = false;
for (let pn = 1; pn <= MAX_PAGES && !noMore; pn++) {
const url = offset
? `https://api.bilibili.com/x/web-interface/popular?ps=${PAGE_SIZE}&pn=${pn}&offset=${encodeURIComponent(offset)}`
: `https://api.bilibili.com/x/web-interface/popular?ps=${PAGE_SIZE}&pn=${pn}`;
try {
const data = await gmFetch(url);
if (data.code === 0 && data.data?.list) {
const list = data.data.list;
allVideos = allVideos.concat(list);
offset = data.data.offset || '';
// 列表为空或长度不足一页表示到底
if (list.length === 0 || list.length < PAGE_SIZE) {
noMore = true;
}
console.log(`[热门重排] pn=${pn} offset=${offset || '空'} 获取${list.length}条${noMore ? ' (到底)' : ''}`);
} else {
console.warn(`[热门重排] pn=${pn} code=${data.code} message=${data.message}`);
noMore = true;
}
} catch (e) {
console.error(`[热门重排] pn=${pn} 请求异常`, e);
noMore = true;
}
loadedPages = pn;
const status = document.querySelector('#pr-status');
if (status) {
status.textContent = `加载中 ${loadedPages} 页,已获取 ${allVideos.length} 条视频...`;
}
}
videoList = allVideos;
// 更新提示文字为真实数量
const tip = document.querySelector('#pr-tip');
if (tip) tip.textContent = `热门视频重排 — 按不同维度重新排序(共${allVideos.length}条)`;
const status = document.querySelector('#pr-status');
if (status) status.style.display = 'none';
renderVideos(currentSort);
}
// ---- 渲染 ----
function renderVideos(sortBy) {
const grid = document.querySelector('#pr-grid');
if (!grid) return;
if (videoList.length === 0) {
grid.innerHTML = '加载中...
';
return;
}
const filtered = currentMinDuration > 0
? videoList.filter(v => (v.duration || 0) >= currentMinDuration)
: videoList;
if (filtered.length === 0) {
grid.innerHTML = '没有符合时长条件的视频
';
return;
}
const sorted = [...filtered].sort((a, b) => {
const sa = a.stat || {}, sb = b.stat || {};
return (sb[sortBy] || 0) - (sa[sortBy] || 0);
});
const isList = currentLayout === 'list';
grid.innerHTML = sorted.map((v, i) => {
const pic = v.pic || '';
const title = v.title || '';
const owner = v.owner?.name || '';
const face = v.owner?.face || '';
const stat = v.stat || {};
const bvid = v.bvid || '';
const duration = formatDuration(v.duration || 0);
const rank = i + 1;
const rankColor = rank <= 3 ? '#fb7299' : '#999';
if (isList) {
return `
${duration}
${rank}
${title}
${face ? `

` : ''}
${owner}
▶ ${formatNum(stat.view || 0)}
👍 ${formatNum(stat.like || 0)}
💬 ${formatNum(stat.danmaku || 0)}
⭐ ${formatNum(stat.favorite || 0)}
`;
}
return `
${duration}
▶ ${formatNum(stat.view || 0)}
${rank}
${title}
${face ? `

` : ''}
${owner}
👍 ${formatNum(stat.like || 0)}
💬 ${formatNum(stat.danmaku || 0)}
⭐ ${formatNum(stat.favorite || 0)}
`;
}).join('');
}
// ---- 注入样式 ----
function injectStyles() {
if (document.getElementById('pr-styles')) return;
const style = document.createElement('style');
style.id = 'pr-styles';
style.textContent = `
/* ---- 布局切换按钮 ---- */
.pr-layout-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 30px;
border: none;
background: var(--bg1, #fff);
color: var(--text3, #999);
cursor: pointer;
transition: all .2s;
}
.pr-layout-btn--active {
background: var(--brand_blue, #00a1d6);
color: #fff;
}
.pr-layout-btn:hover:not(.pr-layout-btn--active) {
background: var(--bg3, #f4f5f7);
}
/* ---- 网格布局 ---- */
.pr-grid--grid {
grid-template-columns: repeat(6, 1fr);
}
@media (max-width: 1400px) {
.pr-grid--grid { grid-template-columns: repeat(5, 1fr); }
}
@media (max-width: 1100px) {
.pr-grid--grid { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 800px) {
.pr-grid--grid { grid-template-columns: repeat(3, 1fr); }
}
/* ---- 列表布局 ---- */
.pr-grid--list {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 900px) {
.pr-grid--list { grid-template-columns: 1fr; }
}
/* ---- 网格卡片 ---- */
.pr-card {
display: block;
text-decoration: none;
color: var(--text1, #18191c);
border-radius: 8px;
overflow: hidden;
background: var(--bg1, #fff);
transition: transform .25s ease, box-shadow .25s ease;
box-shadow: 0 1px 6px rgba(0,0,0,.06);
}
.pr-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,.12);
}
.pr-card__cover {
position: relative;
overflow: hidden;
aspect-ratio: 16/9;
}
.pr-card__cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform .3s ease;
}
.pr-card:hover .pr-card__cover img {
transform: scale(1.05);
}
.pr-card__duration {
position: absolute;
bottom: 6px;
right: 6px;
background: rgba(0,0,0,.7);
color: #fff;
font-size: 11px;
padding: 1px 5px;
border-radius: 3px;
line-height: 1.4;
}
.pr-card__play {
position: absolute;
bottom: 6px;
left: 6px;
background: rgba(0,0,0,.7);
color: #fff;
font-size: 11px;
padding: 1px 5px;
border-radius: 3px;
line-height: 1.4;
}
.pr-card__rank {
position: absolute;
top: 6px;
left: 6px;
color: #fff;
font-size: 12px;
font-weight: 700;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
line-height: 1;
}
.pr-card__info {
padding: 10px 10px 12px;
}
.pr-card__title {
font-size: 13px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 8px;
transition: color .2s;
}
.pr-card:hover .pr-card__title {
color: var(--brand_blue, #00a1d6);
}
.pr-card__up {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.pr-card__face {
width: 22px;
height: 22px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.pr-card__name {
font-size: 12px;
color: var(--text3, #999);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pr-card__stats {
display: flex;
gap: 10px;
font-size: 12px;
color: var(--text3, #999);
}
/* ---- 列表卡片 ---- */
.pr-card--list {
display: flex;
border-radius: 8px;
overflow: hidden;
}
.pr-card--list:hover {
transform: translateY(-2px);
}
.pr-card__cover--list {
position: relative;
overflow: hidden;
width: 240px;
min-width: 240px;
aspect-ratio: 16/9;
}
.pr-card--list .pr-card__cover--list img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform .3s ease;
}
.pr-card--list:hover .pr-card__cover--list img {
transform: scale(1.05);
}
.pr-card__info--list {
flex: 1;
padding: 16px 20px;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
gap: 8px;
}
.pr-card__title--list {
font-size: 15px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
transition: color .2s;
}
.pr-card--list:hover .pr-card__title--list {
color: var(--brand_blue, #00a1d6);
}
.pr-card__up--list {
margin-bottom: 0;
}
.pr-card--list .pr-card__face {
width: 26px;
height: 26px;
}
.pr-card--list .pr-card__name {
font-size: 13px;
}
.pr-card__stats--list {
display: flex;
gap: 14px;
font-size: 13px;
color: var(--text3, #999);
}
`;
document.head.appendChild(style);
}
// ---- 初始化 ----
injectStyles();
waitForElement('.nav-tabs', addNavItem);
})();