BiliBili自动添加视频收藏

进入视频页面后, 自动添加视频到收藏夹中.

目前為 2024-03-13 提交的版本,檢視 最新版本

// ==UserScript==
// @name	BiliBili自动添加视频收藏
// @version	0.3.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	CAT_userConfig
// @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 = () => {
	let isScriptCatEnvironment = false;
	try {
		CAT_userConfig.toString();
		isScriptCatEnvironment = true;
	}
	catch ( e ) {
		isScriptCatEnvironment = false;
	}
	return isScriptCatEnvironment;
};

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 = upUid => request( '/x/v3/fav/folder/created/list-all', 'GET', {
	up_mid: upUid,
} ).then( ( res => {
	if ( res.code !== 0 ) {
		throw new Error( res.message );
	}
	return res.data.list;
} ) );

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 checkFavoriteIsFull = favoriteInfo => favoriteInfo.media_count === 1e3;

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 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();
	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 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();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址