BiliBili自动添加视频收藏
// ==UserScript==
// @name BiliBili自动添加视频收藏
// @version 0.5.1
// @license GPL-3
// @namespace https://github.com/AliubYiero/TamperMonkeyScripts
// @run-at document-start
// @author Yiero
// @homepage https://github.com/AliubYiero/TamperMonkeyScripts
// @description 进入视频页面后, 自动添加视频到收藏夹中.
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/s/video/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_info
// @connect api.bilibili.com
// @icon https://www.bilibili.com/favicon.ico
// ==/UserScript==
/* ==UserConfig==
配置项:
favouriteTitle:
title: 指定收藏夹标题
description: 更改指定收藏夹标题
type: text
default: fun
userUid:
title: 用户uid
description: 设置用户uid
type: text
default: ""
==/UserConfig== */
function getElement(parent, selector, timeout = 0) {
return new Promise((resolve => {
let result = parent.querySelector(selector);
if (result) return resolve(result);
let timer;
const mutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
if (mutationObserver) {
const observer = new mutationObserver((mutations => {
for (let mutation of mutations) {
for (let addedNode of mutation.addedNodes) {
if (addedNode instanceof Element) {
result = addedNode.matches(selector) ? addedNode : addedNode.querySelector(selector);
if (result) {
observer.disconnect();
timer && clearTimeout(timer);
setTimeout((() => resolve(result)), 300);
}
}
}
}
}));
observer.observe(parent, {
childList: true,
subtree: true
});
if (timeout > 0) {
timer = setTimeout((() => {
observer.disconnect();
return resolve(null);
}), timeout);
}
} else {
const listener = e => {
if (e.target instanceof Element) {
result = e.target.matches(selector) ? e.target : e.target.querySelector(selector);
if (result) {
parent.removeEventListener("DOMNodeInserted", listener, true);
timer && clearTimeout(timer);
return resolve(result);
}
}
};
parent.addEventListener("DOMNodeInserted", listener, true);
if (timeout > 0) {
timer = setTimeout((() => {
parent.removeEventListener("DOMNodeInserted", listener, true);
return resolve(null);
}), timeout);
}
}
}));
}
const userUidConfig = {
key: "userUid"
};
const UserConfigs = {
"\u914d\u7f6e\u9879": {
favouriteTitle: {
title: "\u6307\u5b9a\u6536\u85cf\u5939\u6807\u9898",
description: "\u66f4\u6539\u6307\u5b9a\u6536\u85cf\u5939\u6807\u9898",
type: "text",
default: "fun"
},
userUid: {
title: "\u7528\u6237uid",
description: "\u8bbe\u7f6e\u7528\u6237uid",
type: "text",
default: ""
}
}
};
class GMStorageExtra extends Storage {
constructor() {
super();
}
static get length() {
return this.keys().length;
}
static getItem(key, defaultValue, group) {
(() => {})(this.createKey(key, group));
return GM_getValue(this.createKey(key, group), defaultValue);
}
static hasItem(key, group) {
return Boolean(this.getItem(key, group));
}
static setItem(key, value, group) {
GM_setValue(this.createKey(key, group), value);
}
static removeItem(key, group) {
GM_deleteValue(this.createKey(key, group));
}
static clear() {
const keyList = GM_listValues();
for (const key of keyList) {
GM_deleteValue(key);
}
}
static key(index) {
return this.keys()[index];
}
static keys() {
return GM_listValues();
}
static groups() {
const keyList = this.keys();
return keyList.map((key => {
const splitKeyList = key.split(".");
if (splitKeyList.length === 2) {
return splitKeyList[0];
}
return "";
})).filter((item => item));
}
static createKey(key, group) {
if (group) {
return `${group}.${key}`;
}
for (let groupName in UserConfigs) {
const configGroup = UserConfigs[groupName];
for (let configKey in configGroup) {
if (configKey === key) {
return `${groupName}.${key}`;
}
}
}
return key;
}
}
const getUserUid = async () => {
let userUid = GMStorageExtra.getItem(userUidConfig.key, "");
if (!userUid) {
const selector = 'a.header-entry-mini[href^="//space.bilibili.com/"]';
await getElement(document, selector, 6e4);
const userDom = document.querySelector(selector);
if (!userDom) {
return "";
}
userUid = new URL(userDom.href).pathname.split("/")[1];
}
return Promise.resolve(userUid);
};
const setUserUid = uid => {
GMStorageExtra.setItem(userUidConfig.key, uid);
};
const favouriteTitleConfig = {
key: "favouriteTitle",
title: "fun"
};
const getFavouriteTitle = () => GMStorageExtra.getItem(favouriteTitleConfig.key, favouriteTitleConfig.title);
const setFavouriteTitle = title => {
GMStorageExtra.setItem(favouriteTitleConfig.key, title);
};
const hasFavouriteTitle = () => GMStorageExtra.hasItem(favouriteTitleConfig.key);
const codeConfig = {
XOR_CODE: 23442827791579n,
MASK_CODE: 2251799813685247n,
MAX_AID: 1n << 51n,
BASE: 58n,
data: "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"
};
function bvToAv(bvid) {
const {MASK_CODE: MASK_CODE, XOR_CODE: XOR_CODE, data: data, BASE: BASE} = codeConfig;
const bvidArr = Array.from(bvid);
[bvidArr[3], bvidArr[9]] = [ bvidArr[9], bvidArr[3] ];
[bvidArr[4], bvidArr[7]] = [ bvidArr[7], bvidArr[4] ];
bvidArr.splice(0, 3);
const tmp = bvidArr.reduce(((pre, bvidChar) => pre * BASE + BigInt(data.indexOf(bvidChar))), 0n);
return Number(tmp & MASK_CODE ^ XOR_CODE);
}
const checkScriptCatEnvironment = () => GM_info.scriptHandler === "Tampermonkey";
const sleep = timeoutPerSecond => new Promise((resolve => {
setTimeout(resolve, timeoutPerSecond * 1e3);
}));
async function freshListenerPushState(callback, delayPerSecond = 1) {
let _pushState = window.history.pushState.bind(window.history);
window.history.pushState = function() {
setTimeout(callback, delayPerSecond * 1e3);
return _pushState.apply(this, arguments);
};
}
const requestConfig = {
baseURL: "https://api.bilibili.com",
csrf: new URLSearchParams(document.cookie.split("; ").join("&")).get("bili_jct") || ""
};
const xhrRequest = (url, method, data) => {
if (!url.startsWith("http")) {
url = requestConfig.baseURL + url;
}
const xhr = new XMLHttpRequest;
xhr.open(method, url);
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
return new Promise(((resolve, reject) => {
xhr.addEventListener("load", (() => {
const response = JSON.parse(xhr.response);
if (response.code !== 0) {
return reject(response.message);
}
return resolve(response.data);
}));
xhr.addEventListener("error", (() => reject(xhr.status)));
xhr.send(new URLSearchParams(data));
}));
};
const api_collectVideoToFavorite = (videoId, favoriteId) => {
const formData = {
rid: videoId,
type: "2",
add_media_ids: favoriteId,
csrf: requestConfig.csrf
};
return xhrRequest("/x/v3/fav/resource/deal", "POST", formData);
};
const api_createFavorites = favTitle => xhrRequest("/x/v3/fav/folder/add", "POST", {
title: favTitle,
privacy: 1,
csrf: requestConfig.csrf
});
function request(url, method = "GET", paramOrData, GMXmlHttpRequestConfig = {}) {
if (!url.startsWith("http")) {
url = requestConfig.baseURL + url;
}
if (paramOrData && method === "GET") {
url += "?" + new URLSearchParams(paramOrData).toString();
} else if (paramOrData && method === "POST") {
GMXmlHttpRequestConfig.data = JSON.stringify(paramOrData);
}
return new Promise(((resolve, reject) => {
const newGMXmlHttpRequestConfig = {
timeout: 2e4,
...GMXmlHttpRequestConfig,
url: url,
method: method,
onload(response) {
resolve(JSON.parse(response.responseText));
},
onerror(error) {
reject(error);
},
ontimeout() {
reject(new Error("Request timed out"));
}
};
GM_xmlhttpRequest(newGMXmlHttpRequestConfig);
}));
}
const api_listAllFavorites = async upUid => {
const res = await request("/x/v3/fav/folder/created/list-all", "GET", {
up_mid: upUid
});
if (res.code !== 0) {
throw new Error(res.message);
}
return res.data.list;
};
const api_sortFavorites = async favoriteIdList => xhrRequest("/x/v3/fav/folder/sort", "POST", {
sort: favoriteIdList.toString(),
csrf: requestConfig.csrf
});
const getReadFavouriteList = async userUid => {
const favoriteList = await api_listAllFavorites(userUid);
const favouriteTitle = getFavouriteTitle();
const readFavouriteList = favoriteList.filter((favoriteInfo => favoriteInfo.title.match(new RegExp(`^${favouriteTitle}\\d+$`))));
readFavouriteList.sort(((a, b) => {
const aIndex = Number(a.title.slice(favouriteTitle.length));
const bIndex = Number(b.title.slice(favouriteTitle.length));
return bIndex - aIndex;
}));
return readFavouriteList;
};
const menuList = [ {
title: "\u8f93\u5165\u60a8\u7684uid",
onClick: async () => {
const uid = prompt("\u8bf7\u8f93\u5165\u60a8\u7684\u7528\u6237uid (\u9ed8\u8ba4\u5c06\u4ece\u9875\u9762\u4e2d\u83b7\u53d6uid)\n\u5982\u679c\u8bbe\u7f6e\u4e86\u7528\u6237uid\u4f1a\u8ba9\u811a\u672c\u54cd\u5e94\u901f\u5ea6\u66f4\u5feb, \u4e0d\u7528\u7b49\u5f85\u9875\u9762\u8f7d\u5165\u83b7\u53d6uid\n(\u5982\u679c\u60a8\u4e0d\u77e5\u9053uid\u662f\u4ec0\u4e48, \u8bf7\u4e0d\u8981\u968f\u610f\u8f93\u5165)\n(\u7528\u6237uid\u662f\u60a8\u7684\u4e3b\u9875\u4e0a\u7f51\u5740\u7684\u4e00\u4e32\u6570\u5b57 'https://space.bilibili.com/<uid>')", await getUserUid());
if (!uid) {
return;
}
setUserUid(uid);
}
}, {
title: "\u8bbe\u7f6e\u6536\u85cf\u5939\u6807\u9898",
onClick: () => {
if (hasFavouriteTitle()) {
setFavouriteTitle(getFavouriteTitle());
}
const title = prompt("\u8bf7\u8f93\u5165\u6536\u85cf\u5939\u6807\u9898 (\u9ed8\u8ba4\u4f7f\u7528 'fun'\u4f5c\u4e3a\u6536\u85cf\u5939\u6807\u9898)\n", getFavouriteTitle());
if (!title) {
return;
}
setFavouriteTitle(title);
}
} ];
const registerMenu = () => {
menuList.forEach((menuInfo => {
const {title: title, onClick: onClick} = menuInfo;
GM_registerMenuCommand(title, onClick);
}));
};
const config = {
MAX_MEDIA_COUNT: 1e3
};
const checkFavoriteIsFull = favoriteInfo => favoriteInfo.media_count === config.MAX_MEDIA_COUNT;
const getVideoAvId = () => {
const urlPathNameList = new URL(window.location.href).pathname.split("/");
let videoId = urlPathNameList.find((urlPathName => urlPathName.startsWith("BV1") || urlPathName.startsWith("av")));
if (!videoId) {
throw new Error("\u6ca1\u6709\u83b7\u53d6\u5230\u89c6\u9891id");
}
if (videoId.startsWith("BV1")) {
videoId = String(bvToAv(videoId));
}
if (videoId.startsWith("av")) {
videoId = videoId.slice(2);
}
return videoId;
};
const addVideoToFavorite = async (videoId, latestFavorite) => {
const latestFavoriteId = String(latestFavorite.id);
const res = await api_collectVideoToFavorite(videoId, latestFavoriteId);
const successfullyAdd = (res == null ? void 0 : res.success_num) === 0;
if (!successfullyAdd) {
return;
}
(() => {})(`\u5f53\u524d\u89c6\u9891\u5df2\u6dfb\u52a0\u81f3\u6536\u85cf\u5939 [${latestFavorite.title}]`);
};
const createNewFavorite = title => api_createFavorites(title);
const sortOlderFavoritesToLast = async userUid => {
const favoriteList = await api_listAllFavorites(userUid);
const favoriteTitle = getFavouriteTitle();
let readFavouriteList = favoriteList.filter((favoriteInfo => favoriteInfo.title.startsWith(favoriteTitle)));
let otherFavouriteList = favoriteList.filter((favoriteInfo => !favoriteInfo.title.startsWith(favoriteTitle)));
readFavouriteList.sort(((a, b) => {
const aIndex = Number(a.title.slice(favoriteTitle.length));
const bIndex = Number(b.title.slice(favoriteTitle.length));
return bIndex - aIndex;
}));
if (readFavouriteList[0].media_count >= config.MAX_MEDIA_COUNT) {
throw new Error("The latest favorite folder is full.");
}
const latestFavourite = readFavouriteList[0];
readFavouriteList = readFavouriteList.slice(1);
const defaultFavourite = otherFavouriteList[0];
otherFavouriteList = otherFavouriteList.slice(1);
const newFavoriteIdList = [ defaultFavourite, latestFavourite, ...otherFavouriteList, ...readFavouriteList ].map((info => info.id));
api_sortFavorites(newFavoriteIdList);
};
const api_isFavorVideo = () => request("/x/v2/fav/video/favoured", "GET", {
aid: getVideoAvId()
}).then((res => {
if (res.code !== 0) {
throw new Error(res.message);
}
return res.data.favoured;
}));
const autoAddVideoToFavourites = async () => {
let isFavorVideo = await api_isFavorVideo();
(() => {})("\u5f53\u524d\u89c6\u9891\u5df2\u7ecf\u88ab\u6536\u85cf:", isFavorVideo, `av${getVideoAvId()}`);
if (isFavorVideo) {
return;
}
const userUid = await getUserUid();
if (!userUid) {
throw new Error("\u83b7\u53d6\u7528\u6237uid\u5931\u8d25");
}
const readFavouriteList = await getReadFavouriteList(userUid);
if (!readFavouriteList.length) {
const favoriteTitle = getFavouriteTitle();
await createNewFavorite(favoriteTitle + "1");
await autoAddVideoToFavourites();
return;
}
const videoId = getVideoAvId();
const latestFavourite = readFavouriteList[0];
const isFullInFavorite = checkFavoriteIsFull(readFavouriteList[0]);
if (!isFullInFavorite) {
await addVideoToFavorite(videoId, latestFavourite);
}
if (isFullInFavorite) {
const favoriteTitle = getFavouriteTitle();
const latestFavouriteId = Number(latestFavourite.title.slice(favoriteTitle.length));
await createNewFavorite(`${favoriteTitle}${latestFavouriteId + 1}`);
await sleep(1);
await sortOlderFavoritesToLast(userUid);
await autoAddVideoToFavourites();
return;
}
isFavorVideo = await api_isFavorVideo();
if (!isFavorVideo) {
throw new Error("\u6536\u85cf\u5931\u8d25");
}
const favButtonSelector = ".video-fav.video-toolbar-left-item:not(.on)";
const favButtonDom = await getElement(document, favButtonSelector);
if (favButtonDom) {
favButtonDom.classList.add("on");
}
};
!checkScriptCatEnvironment() && registerMenu();
autoAddVideoToFavourites();
freshListenerPushState((async () => {
await autoAddVideoToFavourites();
}), 5);