// ==UserScript==
// @name Bilibili 收藏夹 JSON 查看/保存
// @namespace https://space.bilibili.com/398910090
// @version 2.0
// @description 仅在非私密收藏夹界面可用,私密收藏夹不可用,可查看和保存收藏夹界面的json,可点击收藏夹视频卡片菜单的查看json按钮来查看单个视频的json数据
// @author Ace
// @match https://space.bilibili.com/*/favlist*
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(() => {
'use strict';
const cfg = {
api: 'https://api.bilibili.com/x/v3/fav/resource/list',
ps: 40
};
const getMid = () => new URLSearchParams(location.search).get('fid');
const buildUrl = (mid, pn = 1) =>
`${cfg.api}?media_id=${mid}&pn=${pn}&ps=${cfg.ps}&keyword=&order=mtime&type=0&tid=0&platform=web&web_location=333.1387`;
const getHeaders = () => ({
'User-Agent': navigator.userAgent,
'Referer': location.href
});
const fetchJSON = async (mid, pn = 1) => {
const res = await fetch(buildUrl(mid, pn), { headers: getHeaders() });
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || '接口错误');
return json;
};
const fetchAll = async (mid) => {
const list = [];
let pn = 1, hasMore = true;
while (hasMore) {
const { data } = await fetchJSON(mid, pn);
list.push(...data.medias);
hasMore = data.has_more;
pn++;
}
return { code: 0, data: { medias: list } };
};
const openTab = (obj) => {
const html = `
JSON 浏览器
`;
const w = window.open('', '_blank');
w.document.write(html);
w.document.close();
};
const saveFile = (obj) => {
const name = prompt('文件名:', GM_getValue('fname', 'favlist.json')) || 'favlist.json';
GM_setValue('fname', name);
const blob = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
const u = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), { href: u, download: name });
a.click();
URL.revokeObjectURL(u);
};
const addJsonButton = () => {
const observer = new MutationObserver(() => {
document.querySelectorAll('.bili-video-card__info--right').forEach((menu) => {
if (!menu.querySelector('.view-json-btn')) {
const btn = document.createElement('button');
btn.textContent = '查看 JSON 数据';
btn.className = 'view-json-btn';
btn.style.cssText = 'margin-left: 10px; cursor: pointer; color: #00a1d6; border: none; background: none;';
btn.onclick = async () => {
const videoCard = menu.closest('.bili-video-card');
const bvid = videoCard?.querySelector('.bili-video-card__info--tit a')?.href.match(/\/video\/(BV\w+)/)?.[1];
if (!bvid) return alert('无法获取视频 bvid');
const mid = getMid();
if (!mid) return alert('无法获取当前收藏夹 ID');
try {
const allData = await fetchAll(mid);
const videoData = allData.data.medias.find((item) => item.bvid === bvid);
if (!videoData) return alert('未找到匹配的视频 JSON 数据');
openTab(videoData);
} catch (e) {
alert(e.message);
}
};
menu.appendChild(btn);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
};
const observeDynamicMenu = () => {
const done = new WeakSet();
const injectBtn = (card) => {
// 等菜单真正出现再塞按钮
const tryInject = () => {
const menu = document.querySelector('.bili-card-dropdown-popper');
if (!menu || done.has(menu)) return;
const btn = document.createElement('div');
btn.className = 'bili-card-dropdown-popper__item';
btn.textContent = '🔍 查看JSON';
btn.style.cssText = 'color:#00a1d6;cursor:pointer;white-space:nowrap;';
btn.onclick = async () => {
const bvid = card.dataset.bsbBvid
|| card.querySelector('a[href*="/video/BV"]')?.href.match(/BV\w+/)?.[0];
if (!bvid) return alert('无法获取 bvid');
const mid = getMid();
if (!mid) return alert('无法获取收藏夹 ID');
try {
const allData = await fetchAll(mid);
const item = allData.data.medias.find(m => m.bvid === bvid);
if (!item) return alert('未找到该视频 JSON');
openTab(item);
} catch (e) {
alert(e.message);
}
};
menu.appendChild(btn);
done.add(menu);
};
// 每 50ms 检查一次,最多 1 秒
let t = 0;
const id = setInterval(() => {
if (document.querySelector('.bili-card-dropdown-popper') || t++ > 20) {
clearInterval(id);
tryInject();
}
}, 50);
};
/* 悬停即注入,不用点击 */
document.body.addEventListener('mouseenter', (e) => {
const card = e.target.closest('.bili-video-card');
if (card) injectBtn(card);
}, true);
};
/* ---------- 入口(每次点击都重新读取 fid) ---------- */
GM_registerMenuCommand('📖 查看 JSON(当前页)', async () => {
const mid = getMid();
if (!mid) return alert('无法获取当前收藏夹 ID');
try { openTab(await fetchJSON(mid)); } catch (e) { alert(e.message); }
});
GM_registerMenuCommand('💾 保存 JSON(全部)', async () => {
const mid = getMid();
if (!mid) return alert('无法获取当前收藏夹 ID');
try { saveFile(await fetchAll(mid)); } catch (e) { alert(e.message); }
});
// 启动时添加 JSON 按钮
addJsonButton();
// 启动时监听动态菜单
observeDynamicMenu();
})();