// ==UserScript==
// @name bilibili favlist hidden video detection
// @name:zh-CN 哔哩哔哩(B站|Bilibili)收藏夹Fix(隐藏视频检测)
// @name:zh-TW 嗶哩嗶哩(B站|Bilibili)收藏夾Fix(隱藏影片檢測)
// @namespace http://tampermonkey.net/
// @version 6
// @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 bilibili.com
// @grant GM_openInTab
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_cookie
// ==/UserScript==
(function () {
'use strict';
const localizedText = {
'UPDATES': {
'zh-CN': '更新内容:<br>其他: 优化脚本部分逻辑',
'zh-TW': '更新內容:<br>其他: 優化腳本部分邏輯'
},
'VIDEOS_PER_PAGE': {
'zh-CN': '每页展示视频数量',
'zh-TW': '每頁顯示影片數量'
},
'VIDEOS_PER_PAGE_WITH_PROMPT': {
'zh-CN': '每页最多可以展示的视频数量(一般为36或40)',
'zh-TW': '每頁最多可以顯示的影片數量(一般為36或40)'
},
'INPUT_AV_OR_BV_HERE': {
'zh-CN': '在此输入av号或bv号',
'zh-TW': '在此輸入av號或bv號'
},
'DETECT_HIDDEN_VIDEO': {
'zh-CN': '检测隐藏视频',
'zh-TW': '檢測隱藏影片'
},
'GET_VIDEO_INFO': {
'zh-CN': '查询视频信息',
'zh-TW': '查詢影片資訊'
},
'REMOVE_VIDEO': {
'zh-CN': '取消收藏',
'zh-TW': '取消收藏'
},
'ADD_VIDEO': {
'zh-CN': '添加收藏',
'zh-TW': '新增收藏'
},
'INPUT_VIDEOS_PER_PAGE_AND_RETRY': {
'zh-CN': '请手动设置每页展示视频数量, 然后再次点击按钮',
'zh-TW': '請手動設定每頁顯示影片數量, 然後再次點擊按鈕'
},
'CHECK_VIDEOS_PER_PAGE_AND_RETRY': {
'zh-CN': '请检查预设的每页展示视频数量是否正确, 然后再次点击按钮',
'zh-TW': '請檢查預設的每頁顯示影片數量是否正確, 然後再次點擊按鈕'
},
'INPUT_AV': {
'zh-CN': '请输入av号',
'zh-TW': '請輸入av號'
},
'INPUT_BV': {
'zh-CN': '请输入bv号',
'zh-TW': '請輸入bv號'
},
'WHAT_TO_DO_IF_ERROR': {
'zh-CN': '如果出现问题, 以下操作或许能解决: 检查每页展示视频数量是否正确, 切换至按最近收藏排序, 刷新页面',
'zh-TW': '如果出現問題, 以下操作或許能解決: 檢查每頁顯示影片數量是否正確, 切換至按最近收藏排序, 重新載入頁面'
},
'REFRESH_PAGE_IF_ERROR': {
'zh-CN': '如果出现问题, 刷新页面或许能解决',
'zh-TW': '如果出現問題, 重新載入頁面或許能解決'
},
'NO_HIDDEN_VIDEO_ON_THIS_PAGE': {
'zh-CN': '本页没有隐藏的视频',
'zh-TW': '本頁沒有隱藏的影片'
},
'POSITION_ON_THIS_PAGE': {
'zh-CN': '在本页的位置',
'zh-TW': '在本頁的位置'
},
'POSITION_ON_THIS_PAGE_WITH_PROMPT': {
'zh-CN': '在本页的位置(从1开始)',
'zh-TW': '在本頁的位置(從1開始)'
},
'AV': {
'zh-CN': 'av号',
'zh-TW': 'av號'
},
'BV': {
'zh-CN': 'bv号',
'zh-TW': 'bv號'
},
'API_RESPONSE_CONTENT': {
'zh-CN': 'b站接口响应内容:',
'zh-TW': 'b站介面回應內容:'
},
'FID_NOT_FOUND_ERROR': {
'zh-CN': '无法获取当前收藏夹的fid, 刷新页面或许能解决',
'zh-TW': '無法獲取當前收藏夾的fid, 重新載入頁面或許能解決'
},
'INVALID_VIDEOS_PER_PAGE_ERROR': {
'zh-CN': '每页展示视频数量不正确, 请重新输入',
'zh-TW': '每頁顯示影片數量不正確, 請重新輸入'
},
'REQUEST_TIMEOUT_ERROR': {
'zh-CN': '请求超时, 请重试',
'zh-TW': '請求逾時, 請重試'
},
'COOKIE_READ_ERROR': {
'zh-CN': '无法读取cookie, 更新tampermonkey或许能解决',
'zh-TW': '無法讀取cookie, 更新tampermonkey或許能解決'
},
'UNKNOWN_ERROR': {
'zh-CN': '发生未知错误, 请反馈该问题',
'zh-TW': '發生未知錯誤, 請反饋該問題'
},
};
const preferredLanguage = getPreferredLanguage();
const currentVersion = 6;
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 pagenationCountRegex = /共 (\d+) 页 \/ (\d+) 个/;
const fidFromURLRegex = /fid=(\d+)/;
const bvFromURLRegex = /video\/(\w{12})/;
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 sideObserver = new MutationObserver(function (mutations, observer) {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
if (document.querySelector('div.favlist-aside')) {
observer.disconnect();
main(true);
return;
}
if (document.querySelector('div.fav-sidenav')) {
observer.disconnect();
main(false);
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 });
}
} else {
if (onFavlistPage) {
onFavlistPage = false;
sideObserver.disconnect();
}
}
}
function main(newFreshSpace) {
let videosPerPage = newFreshSpace ? 50000 : 20;
const storedVersion = GM_getValue('version', 0);
let displayUpdate = false;
if (storedVersion !== currentVersion) {
GM_setValue('version', currentVersion);
if (storedVersion) {
displayUpdate = true;
}
}
const usageCount = GM_getValue(newFreshSpace ? 'usageCountNewFreshSpace' : 'usageCount', 0);
const displayPrompt = usageCount < 10 ? '_WITH_PROMPT' : '';
if (displayPrompt) {
GM_setValue(newFreshSpace ? 'usageCountNewFreshSpace' : 'usageCount', usageCount + 1);
}
const divSide = document.querySelector(newFreshSpace ? 'div.favlist-aside' : 'div.fav-sidenav');
if (!newFreshSpace) {
divSide.querySelector('a.watch-later').style.borderBottom = '1px solid #eee';
}
const divControls = document.createElement('div');
divControls.style.padding = newFreshSpace ? '2px 0' : '2px';
if (!newFreshSpace) {
divControls.style.borderTop = '1px solid #e4e9f0';
}
divSide.appendChild(divControls);
let inputTextA;
if (newFreshSpace) {
const divVideosPerPage = document.createElement('div');
divVideosPerPage.style.padding = '2px 0';
divControls.appendChild(divVideosPerPage);
const labelA = document.createElement('label');
labelA.innerText = getLocalizedText('VIDEOS_PER_PAGE' + displayPrompt);
labelA.style.lineHeight = '1';
divVideosPerPage.appendChild(labelA);
inputTextA = document.createElement('input');
inputTextA.type = 'text';
inputTextA.style.boxSizing = 'content-box';
inputTextA.style.height = '16px';
inputTextA.style.width = '20px';
inputTextA.style.padding = '4px';
inputTextA.style.border = '1px solid #ccc';
inputTextA.style.borderRadius = '3px';
inputTextA.style.fontSize = '16px';
inputTextA.style.lineHeight = '1';
labelA.insertAdjacentElement('beforeend', inputTextA);
}
const divInputTextB = document.createElement('div');
divInputTextB.style.padding = newFreshSpace ? '2px 0' : '2px';
divControls.appendChild(divInputTextB);
const inputTextB = document.createElement('input');
inputTextB.type = 'text';
inputTextB.style.boxSizing = 'content-box';
inputTextB.style.height = newFreshSpace ? '16px' : '14px';
inputTextB.style.width = newFreshSpace ? '170px' : '150px';
inputTextB.style.padding = newFreshSpace ? '4px' : '3px';
inputTextB.style.border = '1px solid #ccc';
inputTextB.style.borderRadius = '3px';
inputTextB.style.fontSize = newFreshSpace ? '16px' : '14px';
inputTextB.style.lineHeight = '1';
inputTextB.placeholder = getLocalizedText('INPUT_AV_OR_BV_HERE');
divInputTextB.appendChild(inputTextB);
const divButtonA = document.createElement('div');
divButtonA.style.padding = newFreshSpace ? '2px 0' : '2px';
divControls.appendChild(divButtonA);
const buttonA = document.createElement('button');
buttonA.type = 'button';
buttonA.innerText = getLocalizedText('DETECT_HIDDEN_VIDEO');
buttonA.style.padding = newFreshSpace ? '4px' : '3px';
buttonA.style.border = '1px solid #ccc';
buttonA.style.borderRadius = '3px';
buttonA.style.fontSize = newFreshSpace ? '16px' : '14px';
buttonA.style.lineHeight = '1';
buttonA.style.cursor = 'pointer';
buttonA.addEventListener('click', function () {
try {
clearMessage();
let fid;
if (newFreshSpace) {
const fidFromURLMatch = location.href.match(fidFromURLRegex);
if (fidFromURLMatch) {
fid = fidFromURLMatch[1];
} else {
addMessage(getLocalizedText('FID_NOT_FOUND_ERROR'));
return;
}
} else {
fid = document.querySelector('.fav-item.cur').getAttribute('fid');
}
let currentPageActualVideos;
if (newFreshSpace) {
currentPageActualVideos = document.querySelectorAll('div.bili-video-card__wrap');
} else {
currentPageActualVideos = document.querySelectorAll('li.small-item');
}
if (newFreshSpace) {
// const pagenationBtnNums = document.querySelectorAll('button.vui_pagenation--btn-num');
// const totalPages = parseInt(pagenationBtnNums[pagenationBtnNums.length - 1].innerText, 10);
// const totalVideos = parseInt(document.querySelector('.vui_sidebar-item--active > div.vui_sidebar-item-right').innerText, 10);
const spanPagenationGoCount = document.querySelector('span.vui_pagenation-go__count');
if (spanPagenationGoCount) {
const pagenationCountMatch = spanPagenationGoCount.innerText.match(pagenationCountRegex);
const totalPages = parseInt(pagenationCountMatch[1], 10);
const totalVideos = parseInt(pagenationCountMatch[2], 10);
if (totalPages !== 1) {
videosPerPage = parseInt(inputTextA.value, 10);
if (isNaN(videosPerPage)) {
if (currentPageActualVideos.length <= 36) {
inputTextA.value = 36;
} else if (currentPageActualVideos.length <= 40) {
inputTextA.value = 40;
} else {
addMessage(getLocalizedText('INPUT_VIDEOS_PER_PAGE_AND_RETRY'));
return;
}
addMessage(getLocalizedText('CHECK_VIDEOS_PER_PAGE_AND_RETRY'));
return;
}
if (videosPerPage < Math.ceil(totalVideos / totalPages) || (videosPerPage > Math.floor((totalVideos - 1) / (totalPages - 1)))) {
addMessage(getLocalizedText('INVALID_VIDEOS_PER_PAGE_ERROR'));
return;
}
}
}
}
let currentPage;
if (newFreshSpace) {
const pagenation = document.querySelector('button.vui_pagenation--btn-num.vui_button--active');
if (!pagenation) {
currentPage = 1;
} else {
currentPage = parseInt(pagenation.innerText, 10);
}
} else {
currentPage = parseInt(document.querySelector('li.be-pager-item-active > a').innerText, 10);
}
if (newFreshSpace) {
addMessage(getLocalizedText('WHAT_TO_DO_IF_ERROR'), 11);
} else {
addMessage(getLocalizedText('REFRESH_PAGE_IF_ERROR'), 10);
}
GM_xmlhttpRequest({
method: 'GET',
url: `https://api.bilibili.com/x/v3/fav/resource/ids?media_id=${fid}`,
timeout: 3000,
responseType: 'json',
onload: function (response) {
try {
const currentFavlistAVBVs = response.response.data;
const startIndex = (currentPage - 1) * videosPerPage;
const currentPageExpectedAVBVs = currentFavlistAVBVs.slice(startIndex, startIndex + videosPerPage);
let currentPageActualBVs;
if (newFreshSpace) {
currentPageActualBVs = Array.from(currentPageActualVideos).map(video => video.querySelector('a.bili-cover-card').getAttribute('href').match(bvFromURLRegex)[1]);
} else {
currentPageActualBVs = Array.from(currentPageActualVideos).map(video => video.getAttribute('data-aid'));
}
const hiddenAVBVs = currentPageExpectedAVBVs.filter(currentPageExpectedAVBV => !currentPageActualBVs.includes(currentPageExpectedAVBV.bvid));
if (!hiddenAVBVs.length) {
addMessage(getLocalizedText('NO_HIDDEN_VIDEO_ON_THIS_PAGE'));
return;
}
hiddenAVBVs.forEach(hiddenAVBV => {
addMessage(`${getLocalizedText('POSITION_ON_THIS_PAGE' + displayPrompt)}: ${currentPageExpectedAVBVs.findIndex(currentPageExpectedAVBV => currentPageExpectedAVBV.bvid === hiddenAVBV.bvid) + 1}`);
addMessage(`${getLocalizedText('AV')}: ${hiddenAVBV.id}`);
addMessage(`${getLocalizedText('BV')}: ${hiddenAVBV.bvid}`);
});
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
},
onerror: function (response) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(response, newFreshSpace ? 11 : 10);
console.error(response);
},
ontimeout: function (response) {
addMessage(getLocalizedText('REQUEST_TIMEOUT_ERROR'));
addMessage(response, newFreshSpace ? 11 : 10);
console.error(response);
}
});
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
});
divButtonA.appendChild(buttonA);
const divButtonB = document.createElement('div');
divButtonB.style.padding = newFreshSpace ? '2px 0' : '2px';
divControls.appendChild(divButtonB);
const buttonB = document.createElement('button');
buttonB.type = 'button';
buttonB.innerText = getLocalizedText('GET_VIDEO_INFO');
buttonB.style.padding = newFreshSpace ? '4px' : '3px';
buttonB.style.border = '1px solid #ccc';
buttonB.style.borderRadius = '3px';
buttonB.style.fontSize = newFreshSpace ? '16px' : '14px';
buttonB.style.lineHeight = '1';
buttonB.style.cursor = 'pointer';
buttonB.addEventListener('click', function () {
try {
const bv = inputTextB.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 });
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
});
divButtonB.appendChild(buttonB);
const divButtonC = document.createElement('div');
divButtonC.style.padding = newFreshSpace ? '2px 0' : '2px';
divControls.appendChild(divButtonC);
const buttonC = document.createElement('button');
buttonC.type = 'button';
buttonC.innerText = getLocalizedText('REMOVE_VIDEO');
buttonC.style.padding = newFreshSpace ? '4px' : '3px';
buttonC.style.border = '1px solid #ccc';
buttonC.style.borderRadius = '3px';
buttonC.style.fontSize = newFreshSpace ? '16px' : '14px';
buttonC.style.lineHeight = '1';
buttonC.style.cursor = 'pointer';
buttonC.addEventListener('click', function () {
try {
GM_cookie.list({ name: 'bili_jct' }, function (cookies, error) {
if (!error) {
try {
let av = inputTextB.value;
if (startsWithAVRegex.test(av)) {
av = av.slice(2);
}
if (!avRegex.test(av)) {
addMessage(getLocalizedText('INPUT_AV'));
return;
}
let fid;
if (newFreshSpace) {
const fidFromURLMatch = location.href.match(fidFromURLRegex);
if (fidFromURLMatch) {
fid = fidFromURLMatch[1];
} else {
addMessage(getLocalizedText('FID_NOT_FOUND_ERROR'));
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}`;
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.bilibili.com/x/v3/fav/resource/batch-del',
data: data,
timeout: 3000,
headers: {
'Content-Length': data.length,
'Content-Type': 'application/x-www-form-urlencoded'
},
onload: function (response) {
try {
const json = response.response;
addMessage(getLocalizedText('API_RESPONSE_CONTENT'));
addMessage(json, newFreshSpace ? 11 : 10);
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
},
onerror: function (response) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(response, newFreshSpace ? 11 : 10);
console.error(response);
},
ontimeout: function (response) {
addMessage(getLocalizedText('REQUEST_TIMEOUT_ERROR'));
addMessage(response, newFreshSpace ? 11 : 10);
console.error(response);
}
});
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
} else {
addMessage(getLocalizedText('COOKIE_READ_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
});
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
});
divButtonC.appendChild(buttonC);
const divButtonD = document.createElement('div');
divButtonD.style.padding = newFreshSpace ? '2px 0' : '2px';
divControls.appendChild(divButtonD);
const buttonD = document.createElement('button');
buttonD.type = 'button';
buttonD.innerText = getLocalizedText('ADD_VIDEO');
buttonD.style.padding = newFreshSpace ? '4px' : '3px';
buttonD.style.border = '1px solid #ccc';
buttonD.style.borderRadius = '3px';
buttonD.style.fontSize = newFreshSpace ? '16px' : '14px';
buttonD.style.lineHeight = '1';
buttonD.style.cursor = 'pointer';
buttonD.addEventListener('click', function () {
try {
GM_cookie.list({ name: 'bili_jct' }, function (cookies, error) {
if (!error) {
try {
let av = inputTextB.value;
if (startsWithAVRegex.test(av)) {
av = av.slice(2);
}
if (!avRegex.test(av)) {
addMessage(getLocalizedText('INPUT_AV'));
return;
}
let fid;
if (newFreshSpace) {
const fidFromURLMatch = location.href.match(fidFromURLRegex);
if (fidFromURLMatch) {
fid = fidFromURLMatch[1];
} else {
addMessage(getLocalizedText('FID_NOT_FOUND_ERROR'));
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}`;
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.bilibili.com/x/v3/fav/resource/deal',
data: data,
timeout: 3000,
headers: {
'Content-Length': data.length,
'Content-Type': 'application/x-www-form-urlencoded'
},
onload: function (response) {
try {
const json = response.response;
addMessage(getLocalizedText('API_RESPONSE_CONTENT'));
addMessage(json, newFreshSpace ? 11 : 10);
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
},
onerror: function (response) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(response, newFreshSpace ? 11 : 10);
console.error(response);
},
ontimeout: function (response) {
addMessage(getLocalizedText('REQUEST_TIMEOUT_ERROR'));
addMessage(response, newFreshSpace ? 11 : 10);
console.error(response);
}
});
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
} else {
addMessage(getLocalizedText('COOKIE_READ_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
});
} catch (error) {
addMessage(getLocalizedText('UNKNOWN_ERROR'));
addMessage(error, newFreshSpace ? 11 : 10);
console.error(error);
}
});
divButtonD.appendChild(buttonD);
const divMessage = document.createElement('div');
divMessage.style.padding = newFreshSpace ? '2px 0' : '2px';
divMessage.style.lineHeight = '1.5';
divControls.appendChild(divMessage);
if (displayUpdate) {
setTimeout(() => {
addMessage(getLocalizedText('UPDATES'));
}, 300);
}
function addMessage(msg, px = newFreshSpace ? 13 : 12) {
const p = document.createElement('p');
p.innerHTML = msg;
p.style.fontSize = `${px}px`;
divMessage.appendChild(p);
p.scrollIntoView({ behavior: 'instant', block: 'nearest' });
}
function clearMessage() {
while (divMessage.firstChild) {
divMessage.removeChild(divMessage.firstChild);
}
}
}
})();