// ==UserScript==
// @name bilibili favlist hidden video detection
// @name:zh-CN 哔哩哔哩(B站|Bilibili)收藏夹Fix (检测隐藏视频)
// @name:zh-TW 哔哩哔哩(B站|Bilibili)收藏夹Fix (检测隐藏视频)
// @namespace http://tampermonkey.net/
// @version 8
// @description detect videos in favlist that only visiable to upper
// @description:zh-CN 检测收藏夹中被UP主设置为仅自己可见的视频
// @description:zh-TW 检测收藏夹中被UP主设置为仅自己可见的视频
// @author YTB0710
// @match https://space.bilibili.com/*
// @connect bilibili.com
// @grant GM_openInTab
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_cookie
// ==/UserScript==
(function () {
'use strict';
const AVRegex = /^[1-9]\d*$/;
const BVRegex = /^BV[A-Za-z0-9]{10}$/;
const startsWithAVRegex = /^av/i;
const favlistURLRegex = /https:\/\/space\.bilibili\.com\/\d+\/favlist.*/;
const getFidFromURLRegex = /fid=(\d+)/;
const getBVFromURLRegex = /video\/(\w+)/;
let onFavlistPage = false;
let newFreshSpace;
let classAppendNewFreshSpace;
let divMessageHeightFixed = false;
let detectionScope = GM_getValue('detectionScope', 'page');
const sideObserver = new MutationObserver((_mutations, observer) => {
if (document.querySelector('div.favlist-aside')) {
observer.disconnect();
newFreshSpace = true;
classAppendNewFreshSpace = '-newFreshSpace';
main();
return;
}
if (document.querySelector('div.fav-sidenav')) {
observer.disconnect();
newFreshSpace = false;
classAppendNewFreshSpace = '';
main();
return;
}
});
checkURL();
const originalPushState = history.pushState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
checkURL();
};
const originalReplaceState = history.replaceState;
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
checkURL();
};
window.addEventListener('popstate', checkURL);
function checkURL() {
if (favlistURLRegex.test(location.href)) {
if (!onFavlistPage) {
onFavlistPage = true;
sideObserver.observe(document.body, { subtree: true, childList: true, attributes: false, characterData: false });
}
} else {
if (onFavlistPage) {
onFavlistPage = false;
sideObserver.disconnect();
}
}
}
function main() {
let pageSize;
if (newFreshSpace) {
pageSize = window.innerWidth < 1760 ? 40 : 36;
} else {
pageSize = 20;
}
const style = document.createElement('style');
style.textContent = `
.detect-div {
padding: 2px;
}
.detect-div-newFreshSpace {
padding: 2px 0;
}
.detect-inputText, .detect-inputText-newFreshSpace {
box-sizing: content-box;
border: 1px solid #cccccc;
line-height: 1;
}
.detect-inputText {
width: 140px;
height: 14px;
border-radius: 2px;
padding: 2px;
font-size: 14px;
}
.detect-inputText-newFreshSpace {
width: 160px;
height: 16px;
border-radius: 3px;
padding: 3px;
font-size: 16px;
}
.detect-button, .detect-button-newFreshSpace {
border: 1px solid #cccccc;
line-height: 1;
cursor: pointer;
}
.detect-button {
border-radius: 2px;
padding: 2px;
font-size: 14px;
}
.detect-button-newFreshSpace {
border-radius: 3px;
padding: 3px;
font-size: 16px;
}
.detect-label, .detect-label-newFreshSpace {
line-height: 1;
}
.detect-divMessage, .detect-divMessage-newFreshSpace {
overflow-y: auto;
background-color: #eeeeee;
line-height: 1.5;
scrollbar-width: none;
}
.detect-divMessage {
margin: 2px;
}
.detect-divMessage-heightFixed {
height: 280px;
}
.detect-divMessage::-webkit-scrollbar {
display: none;
}
.detect-divMessage-newFreshSpace {
margin: 2px 0;
}
.detect-divMessage-heightFixed-newFreshSpace {
height: 320px;
}
.detect-divMessage-newFreshSpace::-webkit-scrollbar {
display: none;
}
`;
document.head.appendChild(style);
const divSide = document.querySelector(newFreshSpace ? 'div.favlist-aside' : 'div.fav-sidenav');
if (!newFreshSpace && divSide.querySelector('a.watch-later')) {
divSide.querySelector('a.watch-later').style.borderBottom = '1px solid #eeeeee';
}
const divControls = document.createElement('div');
divControls.classList.add('detect-div' + classAppendNewFreshSpace);
if (!newFreshSpace) {
divControls.style.borderTop = '1px solid #e4e9f0';
}
divSide.appendChild(divControls);
const divInputTextAVBV = document.createElement('div');
divInputTextAVBV.classList.add('detect-div' + classAppendNewFreshSpace);
divControls.appendChild(divInputTextAVBV);
const inputTextAVBV = document.createElement('input');
inputTextAVBV.type = 'text';
inputTextAVBV.classList.add('detect-inputText' + classAppendNewFreshSpace);
inputTextAVBV.placeholder = '输入AV号或BV号';
divInputTextAVBV.appendChild(inputTextAVBV);
const divButtonDetect = document.createElement('div');
divButtonDetect.classList.add('detect-div' + classAppendNewFreshSpace);
divButtonDetect.setAttribute('title',
'地址: https://api.bilibili.com/x/v3/fav/resource/ids?media_id={收藏夹fid}\n' +
'数据: 当前收藏夹内前1000个视频中除去番剧等版权视频之外的所有视频的AV号和BV号 (按最近收藏排序)\n' +
'B站在新版个人空间中尚未使用该接口, 旧版个人空间下线后, 该接口可能随之失效, 此功能将无法使用。\n' +
'建议您使用 哔哩哔哩(B站|Bilibili)收藏夹Fix (备份视频信息) 备份收藏夹内尚未被隐藏的视频的信息, 如果您备份得足够充分, 上述的接口失效后您仍能找出收藏夹内被隐藏的视频。\n' +
'该脚本的地址: https://gf.qytechs.cn/scripts/521668');
divControls.appendChild(divButtonDetect);
const buttonDetect = document.createElement('button');
buttonDetect.type = 'button';
buttonDetect.classList.add('detect-button' + classAppendNewFreshSpace);
buttonDetect.textContent = '检测隐藏视频';
buttonDetect.addEventListener('click', async () => {
try {
clearMessage();
let fid;
if (newFreshSpace) {
const getFidFromURLMatch = location.href.match(getFidFromURLRegex);
if (getFidFromURLMatch) {
fid = parseInt(getFidFromURLMatch[1], 10);
} else {
addMessage('无法获取当前收藏夹的fid, 刷新页面可能有帮助', false, true);
return;
}
} else {
fid = document.querySelector('.fav-item.cur').getAttribute('fid');
}
const response = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
// url: `https://api.bilibili.com/x/v3/fav/resource/ids?media_id=${fid}&platform=web`,
url: `https://api.bilibili.com/x/v3/fav/resource/ids?media_id=${fid}`,
timeout: 5000,
responseType: 'json',
onload: (res) => resolve(res),
onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/ids', res.error]),
ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/ids'])
});
});
const AVBVsFavlistAll = response.response.data;
if (detectionScope === 'page') {
let pageNumber;
if (newFreshSpace) {
const pagenation = document.querySelector('button.vui_pagenation--btn-num.vui_button--active');
if (!pagenation) {
pageNumber = 1;
} else {
pageNumber = parseInt(pagenation.innerText, 10);
}
} else {
pageNumber = parseInt(document.querySelector('li.be-pager-item-active > a').innerText, 10);
}
const startIndex = (pageNumber - 1) * pageSize;
const AVBVsPageAll = AVBVsFavlistAll.slice(startIndex, startIndex + pageSize);
const videosPageVisable = document.querySelectorAll(newFreshSpace ? 'div.items__item' : 'li.small-item');
let BVsPageVisable;
if (newFreshSpace) {
BVsPageVisable = Array.from(videosPageVisable).map(el => el.querySelector('a').getAttribute('href').match(getBVFromURLRegex)[1]);
} else {
BVsPageVisable = Array.from(videosPageVisable).map(el => el.getAttribute('data-aid'));
}
const AVBVsPageHidden = AVBVsPageAll.filter(el => !BVsPageVisable.includes(el.bvid));
if (!AVBVsPageHidden.length) {
addMessage('没有找到隐藏的视频');
return;
}
AVBVsPageHidden.forEach(el => {
addMessage(`在当前页的位置: ${AVBVsPageAll.findIndex(ele => ele.bvid === el.bvid) + 1}`);
addMessage(`AV号: ${el.id}`);
addMessage(`BV号: ${el.bvid}`);
});
} else {
let pageNumber = 1;
let BVsFavlistVisable = [];
while (true) {
const response = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
// url: `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${fid}&pn=${pageNumber}&ps=40&platform=web`,
url: `https://api.bilibili.com/x/v3/fav/resource/list?media_id=${fid}&pn=${pageNumber}&ps=40`,
timeout: 5000,
responseType: 'json',
onload: (res) => resolve(res),
onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/list', res.error]),
ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/list'])
});
});
if (!response.response.data.info.media_count) {
addMessage('没有找到隐藏的视频');
return;
}
if (response.response.data.medias) {
BVsFavlistVisable.push(...response.response.data.medias.map(el => el.bvid));
}
addMessage(`已获取可见视频个数: ${BVsFavlistVisable.length}`, true);
if (!response.response.data.has_more) {
break;
}
if (pageNumber === 25) {
addMessage('仅能获取按最近收藏排序的前1000个视频', false, true);
break;
}
pageNumber++;
}
const AVBVsFavlistHidden = AVBVsFavlistAll.filter(el => !BVsFavlistVisable.includes(el.bvid));
if (!AVBVsFavlistHidden.length) {
addMessage('没有找到隐藏的视频');
return;
}
let count = 1;
AVBVsFavlistHidden.forEach(el => {
addMessage(`第 ${count++} 个:`);
addMessage(`AV号: ${el.id}`);
addMessage(`BV号: ${el.bvid}`);
});
}
} catch (error) {
if (error instanceof Error) {
catchUnknownError(error);
} else {
addMessage(error[0], false, true);
for (let i = 1; i < error.length; i++) {
addMessage(error[i], true);
}
}
}
});
divButtonDetect.appendChild(buttonDetect);
const divSwitchDetectionScope1 = document.createElement('div');
divSwitchDetectionScope1.classList.add('detect-div' + classAppendNewFreshSpace);
divSwitchDetectionScope1.setAttribute('title',
'检测当前页的隐藏视频, 必须将排序方式指定为按最近收藏排序。\n' +
'如果您刚刚在当前收藏夹添加或移除了视频, 请刷新页面后再检测当前页的隐藏视频。\n' +
'如果当前收藏夹内含有番剧, 电影等版权视频, 请将其移出该收藏夹。\n' +
'接口返回的数据中只包含按最近收藏排序的前1000个视频。如果某个隐藏视频在这个范围之外, 请将该视频之前的一些视频移出该收藏夹。\n' +
'如果您正在使用的其他脚本或插件修改了视频封面和标题的链接地址, 请将其关闭后刷新页面再使用此功能。');
divControls.appendChild(divSwitchDetectionScope1);
const labelDetectionScope1 = document.createElement('label');
labelDetectionScope1.classList.add('detect-label' + classAppendNewFreshSpace);
labelDetectionScope1.textContent = '检测当前页';
divSwitchDetectionScope1.appendChild(labelDetectionScope1);
const radioDetectionScope1 = document.createElement('input');
radioDetectionScope1.type = 'radio';
radioDetectionScope1.name = 'detectionScope';
radioDetectionScope1.value = 'page';
radioDetectionScope1.checked = detectionScope === 'page' ? true : false;
radioDetectionScope1.addEventListener('change', () => {
try {
detectionScope = 'page';
GM_setValue('detectionScope', detectionScope);
} catch (error) {
catchUnknownError(error);
}
});
labelDetectionScope1.insertAdjacentElement('afterbegin', radioDetectionScope1);
const divSwitchDetectionScope2 = document.createElement('div');
divSwitchDetectionScope2.classList.add('detect-div' + classAppendNewFreshSpace);
divSwitchDetectionScope2.setAttribute('title',
'地址: https://api.bilibili.com/x/v3/fav/resource/list?media_id={收藏夹fid}&pn={页码}&ps=40\n' +
'数据: 当前收藏夹内除去番剧等版权视频之外的可见视频的BV号 (一次请求最多能获取到40个)\n' +
'检测当前收藏夹的隐藏视频, 需要花费一定时间获取当前收藏夹内所有可见视频的BV号。\n' +
'接口返回的数据中只包含按最近收藏排序的前1000个视频。如果某个隐藏视频在这个范围之外, 请将该视频之前的一些视频移出该收藏夹。');
divControls.appendChild(divSwitchDetectionScope2);
const labelDetectionScope2 = document.createElement('label');
labelDetectionScope2.classList.add('detect-label' + classAppendNewFreshSpace);
labelDetectionScope2.textContent = '检测当前收藏夹';
divSwitchDetectionScope2.appendChild(labelDetectionScope2);
const radioDetectionScope2 = document.createElement('input');
radioDetectionScope2.type = 'radio';
radioDetectionScope2.name = 'detectionScope';
radioDetectionScope2.value = 'favlist';
radioDetectionScope2.checked = detectionScope === 'favlist' ? true : false;
radioDetectionScope2.addEventListener('change', () => {
try {
detectionScope = 'favlist';
GM_setValue('detectionScope', detectionScope);
} catch (error) {
catchUnknownError(error);
}
});
labelDetectionScope2.insertAdjacentElement('afterbegin', radioDetectionScope2);
const divButtonJump = document.createElement('div');
divButtonJump.classList.add('detect-div' + classAppendNewFreshSpace);
divButtonJump.setAttribute('title',
'在文本框内输入某个视频的BV号后, 点击此按钮, 将会跳转至该视频在各个第三方网站的页面。');
divControls.appendChild(divButtonJump);
const buttonJump = document.createElement('button');
buttonJump.type = 'button';
buttonJump.classList.add('detect-button' + classAppendNewFreshSpace);
buttonJump.textContent = '查询视频信息';
buttonJump.addEventListener('click', () => {
try {
const BV = inputTextAVBV.value;
if (!BVRegex.test(BV)) {
addMessage('请输入BV号', false, true);
return;
}
GM_openInTab(`https://www.biliplus.com/video/${BV}`, { active: true, insert: false, setParent: true });
GM_openInTab(`https://xbeibeix.com/video/${BV}`, { insert: false, setParent: true });
GM_openInTab(`https://www.jijidown.com/video/${BV}`, { insert: false, setParent: true });
} catch (error) {
catchUnknownError(error);
}
});
divButtonJump.appendChild(buttonJump);
const divButtonRemove = document.createElement('div');
divButtonRemove.classList.add('detect-div' + classAppendNewFreshSpace);
divButtonRemove.setAttribute('title',
'在文本框内输入某个视频的AV号后, 点击此按钮, 将会从当前收藏夹中移除该视频。');
divControls.appendChild(divButtonRemove);
const buttonRemove = document.createElement('button');
buttonRemove.type = 'button';
buttonRemove.classList.add('detect-button' + classAppendNewFreshSpace);
buttonRemove.textContent = '取消收藏';
buttonRemove.addEventListener('click', () => {
try {
GM_cookie.list({ name: 'bili_jct' }, async (cookies, error) => {
if (error) {
addMessage('无法读取cookie, 更新Tampermonkey可能有帮助', false, true);
console.error(error);
return;
}
try {
let AV = inputTextAVBV.value;
if (startsWithAVRegex.test(AV)) {
AV = AV.slice(2);
}
if (!AVRegex.test(AV)) {
addMessage('请输入AV号', false, true);
return;
}
let fid;
if (newFreshSpace) {
const getFidFromURLMatch = location.href.match(getFidFromURLRegex);
if (getFidFromURLMatch) {
fid = parseInt(getFidFromURLMatch[1], 10);
} else {
addMessage('无法获取当前收藏夹的fid, 刷新页面可能有帮助', false, true);
return;
}
} else {
fid = document.querySelector('.fav-item.cur').getAttribute('fid');
}
const csrf = cookies[0].value;
const data = `resources=${AV}%3A2&media_id=${fid}&platform=web&csrf=${csrf}`;
const response = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'POST',
url: 'https://api.bilibili.com/x/v3/fav/resource/batch-del',
data: data,
timeout: 5000,
headers: {
'Content-Length': data.length,
'Content-Type': 'application/x-www-form-urlencoded'
},
onload: (res) => resolve(res),
onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/batch-del', res.error]),
ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/batch-del'])
});
});
addMessage('B站接口响应内容:');
addMessage(response.response, true);
} catch (error) {
if (error instanceof Error) {
catchUnknownError(error);
} else {
addMessage(error[0], false, true);
for (let i = 1; i < error.length; i++) {
addMessage(error[i], true);
}
}
}
});
} catch (error) {
catchUnknownError(error);
}
});
divButtonRemove.appendChild(buttonRemove);
const divButtonAdd = document.createElement('div');
divButtonAdd.classList.add('detect-div' + classAppendNewFreshSpace);
divButtonAdd.setAttribute('title',
'在文本框内输入某个视频的AV号后, 点击此按钮, 将会添加该视频到当前收藏夹的首位。');
divControls.appendChild(divButtonAdd);
const buttonAdd = document.createElement('button');
buttonAdd.type = 'button';
buttonAdd.classList.add('detect-button' + classAppendNewFreshSpace);
buttonAdd.textContent = '添加收藏';
buttonAdd.addEventListener('click', () => {
try {
GM_cookie.list({ name: 'bili_jct' }, async (cookies, error) => {
if (error) {
addMessage('无法读取cookie, 更新Tampermonkey可能有帮助', false, true);
console.error(error);
return;
}
try {
let AV = inputTextAVBV.value;
if (startsWithAVRegex.test(AV)) {
AV = AV.slice(2);
}
if (!AVRegex.test(AV)) {
addMessage('请输入AV号', false, true);
return;
}
let fid;
if (newFreshSpace) {
const getFidFromURLMatch = location.href.match(getFidFromURLRegex);
if (getFidFromURLMatch) {
fid = parseInt(getFidFromURLMatch[1], 10);
} else {
addMessage('无法获取当前收藏夹的fid, 刷新页面可能有帮助', false, true);
return;
}
} else {
fid = document.querySelector('.fav-item.cur').getAttribute('fid');
}
const csrf = cookies[0].value;
const data = `rid=${AV}&type=2&add_media_ids=${fid}&csrf=${csrf}`;
const response = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'POST',
url: 'https://api.bilibili.com/x/v3/fav/resource/deal',
data: data,
timeout: 5000,
headers: {
'Content-Length': data.length,
'Content-Type': 'application/x-www-form-urlencoded'
},
onload: (res) => resolve(res),
onerror: (res) => reject(['请求失败', 'api.bilibili.com/x/v3/fav/resource/deal', res.error]),
ontimeout: () => reject(['请求超时', 'api.bilibili.com/x/v3/fav/resource/deal'])
});
});
addMessage('B站接口响应内容:');
addMessage(response.response, true);
} catch (error) {
if (error instanceof Error) {
catchUnknownError(error);
} else {
addMessage(error[0], false, true);
for (let i = 1; i < error.length; i++) {
addMessage(error[i], true);
}
}
}
});
} catch (error) {
catchUnknownError(error);
}
});
divButtonAdd.appendChild(buttonAdd);
const divMessage = document.createElement('div');
divMessage.classList.add('detect-divMessage' + classAppendNewFreshSpace);
divControls.appendChild(divMessage);
function addMessage(msg, smallFontSize, border) {
let px;
if (smallFontSize) {
px = newFreshSpace ? 11 : 10;
} else {
px = newFreshSpace ? 13 : 12;
}
const p = document.createElement('p');
p.innerHTML = msg;
p.style.fontSize = `${px}px`;
if (border) {
p.style.borderTop = '1px solid #ff0000';
}
divMessage.appendChild(p);
if (divMessageHeightFixed) {
divMessage.scrollTop = divMessage.scrollHeight;
} else {
if (newFreshSpace) {
if (divMessage.scrollHeight > 320) {
divMessage.classList.add('detect-divMessage-heightFixed-newFreshSpace');
divMessageHeightFixed = true;
divMessage.scrollTop = divMessage.scrollHeight;
}
} else {
if (divMessage.scrollHeight > 280) {
divMessage.classList.add('detect-divMessage-heightFixed');
divMessageHeightFixed = true;
divMessage.scrollTop = divMessage.scrollHeight;
}
}
}
divMessage.scrollIntoView({ behavior: 'instant', block: 'nearest' });
}
function clearMessage() {
while (divMessage.firstChild) {
divMessage.removeChild(divMessage.firstChild);
}
divMessage.classList.remove('detect-divMessage-heightFixed' + classAppendNewFreshSpace);
divMessageHeightFixed = false;
}
function catchUnknownError(error) {
addMessage('发生未知错误', false, true);
addMessage(error.stack, true);
console.error(error);
}
}
})();