// ==UserScript==
// @name b站首页推荐
// @namespace kasw
// @version 5.3
// @description 网页端首页推荐视频
// @author kaws
// @match *://www.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico
// @compatible chrome
// @compatible firefox
// @compatible safari
// @source https://github.com/kawS/bilibili-recommend-app
// @include https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png?*
// @connect app.bilibili.com
// @connect api.bilibili.com
// @connect passport.bilibili.com
// @connect www.mcbbs.net
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_setClipboard
// @run-at document-idle
// @require https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.min.js
// @license MIT
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
const isNewTest = $('#i_cecream').find('.bili-feed4').length > 0 ? true : false;
const itemHeight = isNewTest ? $('.recommended-swipe').next('.recommended-card').height() : $('.bili-grid').eq(0).find('.bili-video-card').height();
let $list = null;
let isWait = false;
let isLoading = true;
let options = {
clientWidth: $(window).width(),
sizes: null,
timeoutKey: 2592000000,
refresh: 1,
oneItemHeight: itemHeight,
listHeight: itemHeight * 4 + 20 * 3,
accessKey: GM_getValue('biliAppHomeKey'),
dateKey: GM_getValue('biliAppHomeKeyDate'),
isShowDanmaku: GM_getValue('biliAppDanmaku') || false,
isShowRec: GM_getValue('biliAppRec') || false
}
function init(){
if(location.href.startsWith('https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png?')){
window.stop();
return window.top.postMessage(location.href, 'https://www.bilibili.com')
}
window.localStorage['bilibili_player_force_DolbyAtmos&8K&HDR'] = 1;
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15'
});
if(location.pathname != '/') return;
setSize(options.clientWidth, options.isShowRec);
initStyle();
intiHtml();
initEvent();
checkAccessKey();
getRecommendList();
}
function setSize(width, setRow){
let row = setRow ? 5 : 4;
if(width <= 1100){
options.sizes = 4 * row
}
if(width > 1100 && width <= 1700){
options.sizes = 5 * row
}
if(width > 1700 && width < 2200){
options.sizes = 6 * row
}
if(width >= 2200){
options.sizes = 7 * row
}
if(isNewTest){
if(width <=1400){
options.sizes = 4 * row
}else{
options.sizes = 5 * row
}
}
}
function initStyle(){
const style = `
`;
$('head').append(style)
}
function intiHtml(){
const $position = isNewTest ? $('.feed2').find('.recommended-container') : $('.bili-grid').eq(1);
const $fullpage = $('#i_cecream');
const html = `
`;
if($fullpage.length <= 0) return;
if(options.isShowRec){
if(isNewTest){
$fullpage.find('.feed2').before(`
${html}
`)
}else{
$fullpage.find('.bili-header').after(`${html}`);
}
$('#scrollwrap').next().hide()
}else{
$position.after(html);
}
$list = $('#recommend-list');
}
function initEvent(){
$('#JaccessKey').on('click', function(){
const $this = $(this);
let type = $this.text().trim();
if(isWait) return;
isWait = true
if(type == '删除授权'){
$this.find('span').text('获取授权');
delAccessKey()
}
if(type == '获取授权' || type == '重新获取授权'){
$this.find('span').text('获取中...');
getAccessKey($this)
}
return false
})
$('#Jrefresh, #JrefreshRight').on('click', function(){
if($('.load-state').length > 0) return;
const $this = $(this);
const reg = /(rotate\([\-\+]?((\d+)(deg))\))/i;
let $svg = $this.find('svg');
let css = $svg.attr('style');
let wts = css.match(reg);
$svg.css('transform', `rotate(${parseFloat(wts[3]) + 360}deg)`);
options.clientWidth = $(window).width();
options.oneItemHeight = isNewTest ? $('.recommended-swipe').next('.recommended-card').height() : $('.bili-grid').eq(0).find;
options.listHeight = $('#recommend-list').height();
setSize(options.clientWidth);
getRecommendList();
return false
})
$list.on('mouseenter', '.bili-video-card__image', function(e){
e.stopPropagation();
const $this = $(this);
let rect = e.currentTarget.getBoundingClientRect();
if($this.data('go') == 'av'){
// $this.find('.bili-watch-later').show();
$this.find('.v-inline-player').addClass('mouse-in visible');
getPreviewImage($this, e.clientX - rect.left);
if(options.isShowDanmaku){
$this.find('.v-inline-danmaku').addClass('mouse-in visible');
getPreviewDanmaku($this)
}
}
}).on('mouseleave', '.bili-video-card__image', function(e){
e.stopPropagation();
const $this = $(this);
if($this.data('go') == 'av'){
// $this.find('.bili-watch-later').hide();
$this.find('.v-inline-player, .v-inline-danmaku').removeClass('mouse-in visible');
}
}).on('mousemove', '.bili-video-card__image', function(e){
e.stopPropagation();
const $this = $(this);
let rect = e.currentTarget.getBoundingClientRect();
if($this.data('go') == 'av'){
if($this[0].pvData){
setPosition($this, e.clientX - rect.left, $this[0].pvData)
}
}
}).on('click', '#Jwatch', function(){
const $this = $(this);
if(isWait) return;
isWait = true;
watchlater($this);
return false
}).on('click', '.dislike .dl', function(){
const $this = $(this);
if(isWait) return;
isWait = true;
dislike($this);
return false
}).on('click', '#Jreturn', function(){
const $this = $(this);
if(isWait) return;
isWait = true;
dislike($this, true);
return false
}).on('click', '#Jbbdown', function(){
const $this = $(this);
let id = $this.data('id');
GM_setClipboard(`BBDown -app -token ${options.accessKey} -mt -ia -p ALL "${id}"`);
toast('复制BBDown命令行成功')
return false
}).on('click', '.more', function(){
const $this = $(this);
const $wp = $this.closest('.bili-video-card__wrap');
$wp.find('.ctrl').css({
'height': $wp.height()
}).show();
return false
}).on('mouseleave', '.ctrl', function(){
const $this = $(this);
if($this.find('.dlike').length > 0){
return
}
$this.hide()
})
$('#JShowDanmaku').on('click', function(){
const $this = $(this);
const $inp = $this.find('input');
let val = JSON.parse($inp.val());
options.isShowDanmaku = !val;
GM_setValue('biliAppDanmaku', options.isShowDanmaku);
$inp.val(options.isShowDanmaku);
if(options.isShowDanmaku){
$this.addClass('is-checked')
}else{
$this.removeClass('is-checked')
}
return false
})
$('#JShowRec').on('click', function(){
const $this = $(this);
const $inp = $this.find('input');
let val = JSON.parse($inp.val());
options.isShowRec = !val;
GM_setValue('biliAppRec', options.isShowRec);
$inp.val(options.isShowRec);
if(options.isShowRec){
$this.addClass('is-checked')
}else{
$this.removeClass('is-checked')
}
toast('2秒后刷新页面,请稍后!', function(){
location.reload()
})
return false
})
if(options.isShowRec){
$(window).on('scroll', function(){
const $this = $(this);
if(isNewTest && options.isShowRec){
if($this.scrollTop() > $('.bili-header').height()){
if($('.header-channel').is(':hidden')){
$('.header-channel').fadeIn()
}
}else{
if(!$('.header-channel').is(':hidden')){
$('.header-channel').hide()
}
}
}
if(($this.scrollTop() + options.oneItemHeight * 3) > ($(document).height() - $(window).height())){
if(isLoading) return;
isLoading = true;
options.clientWidth = $(window).width();
setSize(options.clientWidth, true);
getRecommendList()
}
})
}
}
function toast(msg, cb, duration = 2000){
const $toast = $(`${msg}
`);
$toast.appendTo($('body'));
setTimeout(() => {
$toast.remove();
typeof cb == 'function' && cb()
}, duration)
}
function showLoading(minHeight){
$list.prepend(`
`)
}
function delAccessKey(){
isWait = false;
options.accessKey = null;
options.dateKey = null;
GM_deleteValue('biliAppHomeKey');
GM_deleteValue('biliAppHomeKeyDate');
toast('删除授权成功');
}
async function getAccessKey($el){
let url = null;
let res = null;
let data = null;
try {
res = await fetch('https://passport.bilibili.com/login/app/third?appkey=27eb53fc9058f8c3&api=https%3A%2F%2Fwww.mcbbs.net%2Ftemplate%2Fmcbbs%2Fimage%2Fspecial_photo_bg.png&sign=04224646d1fea004e79606d3b038c84a', {
method: 'GET',
credentials: 'include'
})
} catch (error) {
toast(error)
}
try {
data = await res.json();
} catch (error) {
toast(error)
}
if (data.code || !data.data) {
$el.find('span').text('获取授权');
toast(data.msg || data.message || data.code)
} else if (!data.data.has_login) {
$el.find('span').text('获取授权');
toast('你必须登录B站之后才能使用授权')
} else if (!data.data.confirm_uri) {
$el.find('span').text('获取授权');
toast('无法获得授权网址')
} else {
url = data.data.confirm_uri
}
if(url == null){
isWait = false;
return
}
const $iframe = $(``);
$iframe.appendTo($('body'));
let timeout = setTimeout(() => {
$iframe.remove();
$el.find('span').text('获取授权');;
toast('获取授权超时')
}, 5000);
window.onmessage = ev => {
if (ev.origin != 'https://www.mcbbs.net' || !ev.data) {
isWait = false;
return
}
const key = ev.data.match(/access_key=([0-9a-z]{32})/);
if (key) {
GM_setValue('biliAppHomeKey', options.accessKey = key[1]);
GM_setValue('biliAppHomeKeyDate', options.dateKey = +new Date());
toast('获取授权成功');
$el.find('span').text('删除授权');;
clearTimeout(timeout);
$iframe.remove();
} else {
toast('没有获得匹配的密钥')
}
}
isWait = false;
}
function checkAccessKey(){
const nowDate = +new Date();
if(!options.dateKey) return;
if(options.dateKey == -1){
$('#JaccessKey').find('span').text('重新获取授权');
return
}
if(nowDate - options.dateKey > options.timeoutKey){
$('#JaccessKey').find('span').text('重新获取授权');
GM_setValue('biliAppHomeKeyDate', options.dateKey = -1);
GM_deleteValue('biliAppHomeKey');
options.accessKey = null;
}
}
function getRecommend(url, type){
const errmsg = '获取推荐视频失败';
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: res => {
try {
const rep = JSON.parse(res.response);
if (rep.code != 0) {
if(/鉴权失败/.test(rep.message)){
delAccessKey();
$('#JaccessKey').find('span').text('重新获取授权');
return
}
reject(errmsg)
}
if(type == 'new'){
resolve(rep.data.item)
}else{
resolve(rep.data)
}
} catch(e) {
reject(errmsg)
}
},
onerror: e => {
reject(errmsg)
}
})
})
}
async function getRecommendList(){
if(options.refresh == 1){
!isNewTest && $list.height(options.listHeight)
showLoading(options.listHeight);
}else{
if($list.attr('style')){
$list.removeAttr('style')
}
if(!options.isShowRec){
showLoading(options.listHeight);
}
}
const token = options.accessKey ? '&access_key=' + options.accessKey : '';
const url1 = `https://api.bilibili.com/x/web-interface/index/top/rcmd?fresh_type=3&version=1&ps=10&fresh_idx=${options.refresh}&fresh_idx_1h=${options.refresh}`;
const url2 = 'https://app.bilibili.com/x/feed/index?appkey=27eb53fc9058f8c3&build=1&mobi_app=android&idx=';
const url3 = 'https://app.bilibili.com/x/v2/feed/index?build=70600100&mobi_app=iphone&idx=';
let result = null;
let data = [];
let list = null;
if(options.isShowRec){
// 4-24 5-30 6-36 7-42
if(options.sizes > 36){
for(let i=0;i<6;i++){
let uri = url2 + i + ((Date.now() / 1000).toFixed(0)) + token;
// console.log(uri);
await getRecommend(uri).then(d => {
data.push(d.config ? getDataV2(d.items) : d)
}).catch(err => {
i--;
console.log(err)
})
}
}else{
for(let i=0;i<5;i++){
let uri = url2 + i + ((Date.now() / 1000).toFixed(0)) + token;
// console.log(uri);
await getRecommend(uri).then(d => {
data.push(d.config ? getDataV2(d.items) : d)
}).catch(err => {
i--;
console.log(err)
})
}
}
}else{
// 4-16 5-20 6-24 7-28
if(options.sizes > 20){
for(let i=0;i<4;i++){
let uri = url2 + i + ((Date.now() / 1000).toFixed(0)) + token;
// console.log(uri);
await getRecommend(uri).then(d => {
data.push(d.config ? getDataV2(d.items) : d)
}).catch(err => {
i--;
console.log(err)
})
}
}else{
// result = Promise.all([getRecommend(url1, 'new'), getRecommend(url2)])
for(let i=0;i<3;i++){
let uri = url2 + i + ((Date.now() / 1000).toFixed(0)) + token;
// console.log(uri);
await getRecommend(uri).then(d => {
data.push(d.config ? getDataV2(d.items) : d)
}).catch(err => {
i--;
console.log(err)
})
}
}
}
// if(options.isShowRec){
// data[0] = new2old(data[0]);
// data[2] = new2old(data[2]);
// options.isShowRec && (data[4] = new2old(data[4]))
// }else{
// data[0] = new2old(data[0])
// }
// console.log(data);
// for(let i=0;i {
// return item.card_type == 'small_cover_v2'
return item.goto == 'av'
})
}
function new2old(data){
return data.map((item) => {
return {
autoplay: 1,
cid: item.cid,
cover: item.pic,
ctime: item.pubdate,
danmaku: item.stat.danmaku,
desc: `${item.stat.danmaku}弹幕`,
duration: item.duration,
face: item.owner.face,
goto: item.goto,
idx: item.id,
like: item.stat.like,
mid: item.owner.mid,
name: item.owner.name,
param: item.id,
play: item.stat.view,
title: item.title,
tname: '',
uri: item.uri,
rcmd_reason: {
content: item.rcmd_reason?.reason_type == 1 ? '已关注' : item.rcmd_reason?.content || ''
}
}
})
}
function unique(data){
const arr = data[0].concat(data[1], data[2] || [], data[3] || [], data[4] || [], data[5] || []);
let result = [];
let cidList = {};
for(let item of arr){
if(!cidList[item.cid]){
result.push(item);
cidList[item.cid] = true
}
}
return result.sort(function(){
return Math.random() - 0.5
})
}
function updateRecommend(list){
let html = '';
for(let i=0;i
`;
}
if(options.isShowRec){
$list.append(html);
$('.load-state').remove();
setTimeout(() => {
isLoading = false;
if($(document).height() - $(window).height() <= 0){
getRecommendList()
}
}, 300)
}else{
$list.html(html)
}
}
function formatNumber(input, format = 'number'){
if (format == 'time') {
let second = input % 60;
let minute = Math.floor(input / 60);
let hour;
if (minute > 60) {
hour = Math.floor(minute / 60);
minute = minute % 60;
}
if (second < 10) second = '0' + second;
if (minute < 10) minute = '0' + minute;
return hour ? `${hour}:${minute}:${second}` : `${minute}:${second}`
} else {
return input > 9999 ? `${(input / 10000).toFixed(1)}万` : input || 0
}
}
function returnDateTxt(time){
if (!time) return '';
let date = new Date(time * 1000);
let year = date.getFullYear();
let month = date.getMonth() + 1;
let day = date.getDate();
// return `· ${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day}`
return `· ${month}-${day}`
}
function token(){
try {
return document.cookie.match(/bili_jct=([0-9a-fA-F]{32})/)[1]
} catch(e) {
return ''
}
}
async function watchlater($el){
const aid = $el.data('aid');
let type = $el.hasClass('del') ? 'del' : 'add';
let res = null;
let data = null;
try {
res = await fetch(`https://api.bilibili.com/x/v2/history/toview/${type}`, {
method: 'POST',
credentials: 'include',
headers: {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: `aid=${aid}&csrf=${token()}`
})
} catch (error) {
toast(error)
}
try {
data = await res.json();
} catch (error) {
toast(error)
}
isWait = false;
if(data.code == 0){
// if(type == 'add'){
// $el.addClass('del').find('span').text('移除');
// $el.find('svg').html('');
// toast('添加成功')
// }else{
// $el.removeClass('del').find('span').text('稍后再看');
// $el.find('svg').html('');
// toast('移除成功')
// }
if(type == 'add'){
$el.addClass('del').text('移除视频');
toast('添加成功')
}else{
$el.removeClass('del').text('稍后再看');
toast('移除成功')
}
}else{
toast(data.message)
}
}
async function getPreviewImage($el, e){
const aid = $el.data('aid');
let pvData = $el[0].pvData;
if(!pvData){
let res = null;
let data = null;
try {
res = await fetch(`https://api.bilibili.com/pvideo?aid=${aid}`);
} catch (error) {
toast(error)
}
try {
data = await res.json();
} catch (error) {
toast(error)
}
pvData = $el[0].pvData = data.data;
}
setPosition($el, e, pvData)
}
async function getPreviewDanmaku($el){
const aid = $el.data('aid');
let danmakuData = $el[0].danmakuData;
if(!danmakuData){
let res = null;
let data = null;
try {
res = await fetch(`https://api.bilibili.com/x/v2/dm/ajax?aid=${aid}`);
} catch (error) {
toast(error)
}
try {
data = await res.json();
} catch (error) {
toast(error)
}
danmakuData = $el[0].danmakuData = data.data
}
setDanmakuRoll($el, danmakuData);
}
function setPosition($el, mouseX, pvData){
const $tarDom = $el.find('.v-inline-player');
// const $duration = $el.data('duration');
const $pvbox = $tarDom.find('pv-box');
const width = $tarDom.width();
const height = $tarDom.height();
const sizeX = width * pvData.img_x_len;
const sizeY = height * pvData.img_y_len;
const onePageImgs = pvData.img_x_len * pvData.img_y_len;
const rIndexList = pvData.index.slice(1);
const pageSize = Math.ceil(rIndexList.length / onePageImgs);
let percent = mouseX / width;
if (percent < 0) percent = 0;
if (percent > 1) percent = 1;
const durIndex = Math.floor(percent * rIndexList.length)
const page = Math.floor(durIndex / (pvData.img_x_len * pvData.img_y_len));
const imgUrl = pvData.image[page];
const imgIndex = durIndex - page * onePageImgs;
const x = ((imgIndex - 1) % pvData.img_x_len) * width;
// const y = Math.floor(imgIndex / (pvData.img_x_len)) * (width * pvData.img_y_size / pvData.img_x_size);
const y = Math.floor(imgIndex / (pvData.img_x_len)) * height;
const imgY = (Math.floor(imgIndex / pvData.img_x_len)) + 1;
const imgX = imgIndex - (imgY - 1) * pvData.img_x_len;
const bar = percent * 100;
if($pvbox.length > 0){
$pvbox.css({
'background': `url(https:${imgUrl}) ${x < 0 ? 0 : -x}px ${y < 0 ? 0 : -y}px no-repeat`
})
$pvbox.next().css({
'width': `${bat}%`
})
return
}
$tarDom.html(`
`)
}
function setDanmakuRoll($el, danmakuData){
if(danmakuData.length <= 0) return;
const $tarDom = $el.find('.v-inline-danmaku');
let $items = $tarDom.find('p');
let outWidth = $tarDom.width();
let lastWait = new Array(5).fill(600);
let defaultMoveOpts = {
pageSize: 5,
size: Math.ceil(danmakuData.length / 5),
defaultHeight: 18,
topSalt: 5,
dur: 5
}
if($items.length > 0) $tarDom.empty();
for(let i = 0;i < danmakuData.length;i++){
let options = {
channel: i % defaultMoveOpts.pageSize,
startPosX: outWidth,
startPosY: i % 5 * defaultMoveOpts.defaultHeight + defaultMoveOpts.topSalt
};
let $html = $(`${danmakuData[i]}
`);
let wait = (lastWait[options.channel] + (Math.floor(Math.random() * 1000 + 100))) / 1000;
$tarDom.append($html);
options.width = $html.width();
options.moveX = options.width + outWidth;
options.dur = (options.width + outWidth) / (outWidth / defaultMoveOpts.dur);
$html.css({
'transform': `translateX(-${options.moveX}px)`,
'transition': `transform ${options.dur}s linear ${wait}s`,
'opacity': 1
})
lastWait[options.channel] = (wait + options.dur * 0.6) * 1000
}
}
function dislike($el, isReturn){
const errmsg = '减少推荐内容请求失败';
const token = options.accessKey ? '&access_key=' + options.accessKey : '';
const reason = {
'4': 'UP主',
'1': '不感兴趣',
'12': '此类内容过多',
'13': '推荐过'
}
const $wp = $el.closest('.dislike');
const params = {
'goto': $el.data('goto'),
'id': $el.data('id'),
'mid': $el.data('mid'),
'reason_id': $el.data('rsid'),
'rid': $el.data('rid'),
'tag_id': $el.data('tagid')
}
let url = `https://app.bilibili.com/x/feed/dislike`;
if(isReturn){
url += '/cancel'
}
url += `?appkey=27eb53fc9058f8c3&build=5000000&goto=${params.goto}&id=${params.id}&mid=${params.mid}&reason_id=${params.reason_id}&rid=${params.rid}&tag_id=${params.tag_id}` + token;
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: res => {
try {
const rep = JSON.parse(res.response);
if (rep.code != 0) {
toast(errmsg)
}
if(isReturn){
$wp.find('.over').hide().find('.reason').text('');
$wp.find('.ready').css({
'display': 'flex'
});
$wp.removeClass('dlike');
toast('撤销成功')
}else{
$wp.find('.ready').hide();
$wp.find('.over').css({
'display': 'flex'
}).find('.reason').text($el.text());
$wp.addClass('dlike');
if($wp.closest('.ctrl').is(':hidden')){
$wp.closest('.ctrl').show()
}
toast('减少推荐成功')
}
} catch(e) {
toast(errmsg)
}
isWait = false;
},
onerror: e => {
isWait = false;
toast(errmsg)
}
})
}
init()
})();