// ==UserScript==
// @name bilibili favlist hidden video detection
// @name:zh-CN 哔哩哔哩(B站|Bilibili)收藏夹Fix(隐藏视频检测)
// @name:zh-TW 嗶哩嗶哩(B站|Bilibili)收藏夾Fix(隱藏影片檢測)
// @namespace http://tampermonkey.net/
// @version 4
// @description detect videos in favlist that only visiable to upper
// @description:zh-CN 检测收藏夹中被up主设置为仅自己可见的视频
// @description:zh-TW 檢測收藏夾中被上傳者設定為僅自己可見的影片
// @author YTB0710
// @match https://space.bilibili.com/*
// @connect api.bilibili.com
// @grant GM_xmlhttpRequest
// @grant GM_cookie
// @grant GM_openInTab
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function () {
'use strict';
const localizedText = {
'UPDATES': {
'zh-CN': '更新内容: 修复: 切换至其他页再切换回收藏页, 脚本的功能界面区无法显示 其他: 优化脚本部分逻辑',
'zh-TW': '更新內容: 修復: 切換至其他頁再切換回收藏頁, 腳本的功能介面區無法顯示 其他: 優化腳本部分邏輯'
},
'INPUT_AV_OR_BV_HERE': {
'zh-CN': '在此输入av号或bv号',
'zh-TW': '在此輸入av號或bv號'
},
'DETECT_HIDDEN_VIDEO_WITH_PROMPT': {
'zh-CN': '检测隐藏视频(先刷新页面)',
'zh-TW': '檢測隱藏影片(先重新載入頁面)'
},
'DETECT_HIDDEN_VIDEO': {
'zh-CN': '检测隐藏视频',
'zh-TW': '檢測隱藏影片'
},
'GET_VIDEO_INFO_WITH_PROMPT': {
'zh-CN': '查询视频信息(输入bv号)',
'zh-TW': '查詢影片資訊(輸入bv號)'
},
'GET_VIDEO_INFO': {
'zh-CN': '查询视频信息',
'zh-TW': '查詢影片資訊'
},
'REMOVE_VIDEO_WITH_PROMPT': {
'zh-CN': '取消收藏(输入av号)',
'zh-TW': '取消收藏(輸入av號)'
},
'REMOVE_VIDEO': {
'zh-CN': '取消收藏',
'zh-TW': '取消收藏'
},
'ADD_VIDEO_WITH_PROMPT': {
'zh-CN': '添加收藏(输入av号)',
'zh-TW': '新增收藏(輸入av號)'
},
'ADD_VIDEO': {
'zh-CN': '添加收藏',
'zh-TW': '新增收藏'
},
'AV': {
'zh-CN': 'av号',
'zh-TW': 'av號'
},
'BV': {
'zh-CN': 'bv号',
'zh-TW': 'bv號'
},
'INPUT_AV': {
'zh-CN': '请输入av号',
'zh-TW': '請輸入av號'
},
'INPUT_BV': {
'zh-CN': '请输入bv号',
'zh-TW': '請輸入bv號'
},
'NO_HIDDEN_VIDEO_ON_THIS_PAGE': {
'zh-CN': '本页没有隐藏的视频',
'zh-TW': '本頁沒有隱藏的影片'
},
'POSITION_ON_THIS_PAGE_WITH_PROMPT': {
'zh-CN': '在本页的位置(从1开始)',
'zh-TW': '在本頁的位置(從1開始)'
},
'POSITION_ON_THIS_PAGE': {
'zh-CN': '在本页的位置',
'zh-TW': '在本頁的位置'
},
'API_RESPONSE_CONTENT': {
'zh-CN': 'b站接口响应内容',
'zh-TW': 'b站介面回應內容'
},
'REFRESH_PAGE_IF_ERROR': {
'zh-CN': '如果出现问题, 请刷新页面后重试',
'zh-TW': '如果出現問題, 請重新載入頁面後再試'
},
'COOKIE_READ_ERROR': {
'zh-CN': '无法读取cookie, 请更新tampermonkey, 前往控制台查看错误信息',
'zh-TW': '無法讀取cookie, 請更新tampermonkey, 前往控制台查看錯誤資訊'
}
};
const preferredLanguage = getPreferredLanguage();
const currentVersion = 4;
const videosPerPage = 20;
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.*/;
let onFavlistPage = false;
function getPreferredLanguage() {
const languages = navigator.languages || [navigator.language];
for (const lang of languages) {
if (lang === 'zh-CN') {
return 'zh-CN';
}
if (lang === 'zh-TW') {
return 'zh-TW';
}
if (lang === 'zh-HK') {
return 'zh-TW';
}
}
return 'zh-CN';
}
function getLocalizedText(key) {
return localizedText[key][preferredLanguage];
}
const favSidenavObserver = new MutationObserver(function (mutations, observer) {
for (let mutation of mutations) {
if (mutation.type === 'childList') {
if (!document.querySelector('div.fav-sidenav')) {
return;
}
observer.disconnect();
main();
break;
}
}
});
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(window.location.href)) {
if (!onFavlistPage) {
onFavlistPage = true;
favSidenavObserver.observe(document.body, { subtree: true, childList: true });
}
} else {
if (onFavlistPage) {
onFavlistPage = false;
favSidenavObserver.disconnect();
}
}
}
function main() {
const storedVersion = GM_getValue('version', 0);
let displayUpdate = false;
if (storedVersion !== currentVersion) {
GM_setValue('version', currentVersion);
if (storedVersion) {
displayUpdate = true;
}
}
const favSidenav = document.querySelector('div.fav-sidenav');
const usageCount = GM_getValue('usageCount', 0);
const displayPrompt = usageCount < 10 ? '_WITH_PROMPT' : '';
if (displayPrompt) {
GM_setValue('usageCount', usageCount + 1);
}
const watchLater = favSidenav.querySelector('a.watch-later');
watchLater.style.borderBottom = '1px solid #eee';
const controlsContainer = document.createElement('div');
controlsContainer.style.borderTop = '1px solid #e4e9f0';
controlsContainer.style.padding = '2px';
favSidenav.appendChild(controlsContainer);
const inputTextContainer = document.createElement('div');
inputTextContainer.style.padding = '2px';
controlsContainer.appendChild(inputTextContainer);
const inputText = document.createElement('input');
inputText.type = 'text';
if (displayPrompt) {
inputText.placeholder = getLocalizedText('INPUT_AV_OR_BV_HERE');
}
inputTextContainer.appendChild(inputText);
const buttonAContainer = document.createElement('div');
buttonAContainer.style.padding = '2px';
controlsContainer.appendChild(buttonAContainer);
const buttonA = document.createElement('button');
buttonA.type = 'button';
buttonA.innerText = getLocalizedText('DETECT_HIDDEN_VIDEO' + displayPrompt);
buttonA.addEventListener('click', function () {
clearMessage();
addMessage(getLocalizedText('REFRESH_PAGE_IF_ERROR'), 10);
const favNum = parseInt(document.querySelector('.fav-item.cur > span.num').innerText, 10);
const pager = document.querySelector('li.be-pager-next');
let currentPageExpectedNum;
if (pager && !pager.classList.contains('be-pager-disabled')) {
currentPageExpectedNum = videosPerPage;
} else {
currentPageExpectedNum = favNum % videosPerPage;
}
const lis = document.querySelectorAll('ul.fav-video-list > li');
if (lis.length === currentPageExpectedNum) {
addMessage(getLocalizedText('NO_HIDDEN_VIDEO_ON_THIS_PAGE'));
return;
}
const fid = document.querySelector('.fav-item.cur').getAttribute('fid');
GM_xmlhttpRequest({
method: 'GET',
url: `https://api.bilibili.com/x/v3/fav/resource/ids?media_id=${fid}`,
responseType: 'json',
onload: function (response) {
const datas = response.response.data;
let currentPage = 1;
if (favNum > 20) {
currentPage = parseInt(document.querySelector('li.be-pager-item-active').innerText, 10);
}
const startIndex = (currentPage - 1) * videosPerPage;
const currentPageExpectedDatas = datas.slice(startIndex, startIndex + videosPerPage);
const currentPageActualBVs = Array.from(lis).map(li => li.getAttribute('data-aid'));
const hiddenDatas = currentPageExpectedDatas.filter(data => !currentPageActualBVs.includes(data.bvid));
hiddenDatas.forEach(hiddenData => {
addMessage(`${getLocalizedText('POSITION_ON_THIS_PAGE' + displayPrompt)}: ${currentPageExpectedDatas.findIndex(data => data.bvid === hiddenData.bvid) + 1}`);
addMessage(`${getLocalizedText('AV')}: ${hiddenData.id}`);
addMessage(`${getLocalizedText('BV')}: ${hiddenData.bvid}`);
});
}
});
});
buttonAContainer.appendChild(buttonA);
const buttonBContainer = document.createElement('div');
buttonBContainer.style.padding = '2px';
controlsContainer.appendChild(buttonBContainer);
const buttonB = document.createElement('button');
buttonB.type = 'button';
buttonB.innerText = getLocalizedText('GET_VIDEO_INFO' + displayPrompt);
buttonB.addEventListener('click', function () {
clearMessage();
const bv = inputText.value;
if (!bvRegex.test(bv)) {
addMessage(getLocalizedText('INPUT_BV'));
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 });
});
buttonBContainer.appendChild(buttonB);
const buttonCContainer = document.createElement('div');
buttonCContainer.style.padding = '2px';
controlsContainer.appendChild(buttonCContainer);
const buttonC = document.createElement('button');
buttonC.type = 'button';
buttonC.innerText = getLocalizedText('REMOVE_VIDEO' + displayPrompt);
buttonC.addEventListener('click', function () {
clearMessage();
GM_cookie.list({ name: 'bili_jct' }, function (cookies, error) {
if (!error) {
let av = inputText.value;
if (startsWithAVRegex.test(av)) {
av = av.slice(2);
}
if (!avRegex.test(av)) {
addMessage(getLocalizedText('INPUT_AV'));
return;
}
const 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}`;
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.bilibili.com/x/v3/fav/resource/batch-del',
data: data,
headers: {
'Content-Length': data.length,
'Content-Type': 'application/x-www-form-urlencoded'
},
onload: function (response) {
const json = response.response;
addMessage(`${getLocalizedText('API_RESPONSE_CONTENT')}:`);
addMessage(json, 10);
}
});
} else {
console.error(error);
addMessage(getLocalizedText('COOKIE_READ_ERROR'));
}
});
});
buttonCContainer.appendChild(buttonC);
const buttonDContainer = document.createElement('div');
buttonDContainer.style.padding = '2px';
controlsContainer.appendChild(buttonDContainer);
const buttonD = document.createElement('button');
buttonD.type = 'button';
buttonD.innerText = getLocalizedText('ADD_VIDEO' + displayPrompt);
buttonD.addEventListener('click', function () {
clearMessage();
GM_cookie.list({ name: 'bili_jct' }, function (cookies, error) {
if (!error) {
let av = inputText.value;
if (startsWithAVRegex.test(av)) {
av = av.slice(2);
}
if (!avRegex.test(av)) {
addMessage(getLocalizedText('INPUT_AV'));
return;
}
const 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}`;
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.bilibili.com/x/v3/fav/resource/deal',
data: data,
headers: {
'Content-Length': data.length,
'Content-Type': 'application/x-www-form-urlencoded'
},
onload: function (response) {
const json = response.response;
addMessage(`${getLocalizedText('API_RESPONSE_CONTENT')}:`);
addMessage(json, 10);
}
});
} else {
console.error(error);
addMessage(getLocalizedText('COOKIE_READ_ERROR'));
}
});
});
buttonDContainer.appendChild(buttonD);
const textContainer = document.createElement('div');
textContainer.style.padding = '2px';
controlsContainer.appendChild(textContainer);
if (displayUpdate) {
setTimeout(() => {
addMessage(getLocalizedText('UPDATES'));
}, 300);
}
function addMessage(info, px = 12) {
const p = document.createElement('p');
p.innerText = info;
p.style.fontSize = `${px}px`;
textContainer.appendChild(p);
p.scrollIntoView({ behavior: 'instant', block: 'nearest' });
}
function clearMessage() {
while (textContainer.firstChild) {
textContainer.removeChild(textContainer.firstChild);
}
}
}
})();