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