// ==UserScript==
// @name Bilibili视频观看历史记录
// @namespace Bilibili-video-History
// @version 1.3.3-temp
// @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js
// @resource toastr_js https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js
// @resource toastr_css https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css
// @grant GM_getResourceText
// @description 记录并提示Bilibili已观看或已访问但未观看视频记录
// @author DreamNya
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/v/*
// @match https://t.bilibili.com/*
// @match https://space.bilibili.com/*
// @match https://www.bilibili.com
// @match https://www.bilibili.com/?*
// @match https://www.bilibili.com/account/history
// @match https://www.bilibili.com/watchlater/*
// @match https://search.bilibili.com/*
// @match https://www.bilibili.com/medialist/play/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addStyle
// @run-at document-end
// @license MIT
// @downloadURL none
// ==/UserScript==
/* globals
jQuery, $,
__INITIAL_STATE__
*/
/*
前言:
1、本脚本纯原创,编写之前已尽全力搜索但未找到相似功能脚本,求人不如求自己,故自己动手编写此脚本。
2、作者本人非码农纯小白,没有系统学习过代码,所有代码纯靠baidu自学,此脚本代码可能存在诸多不合理之处。
2.5、由于懒得想变量名、函数名、故直接使用中文为主、英文为辅的变量名、函数名(随心所欲,气死不负责)
3、本脚本使用了Tampermonkey(油猴)内置函数,完全依赖Tampermonkey,仅在Chromium+Tampermonkey v4.14版本测试正常使用,其余环境均未进行测试。
4、如发现各种问题或BUG欢迎与作者联系。
5、本脚本仅记录普通视频观看记录,番剧、直播、漫画等不在脚本记录对象范围内。
6、本脚本主要是自用,其次是练手写写JS,最后才是发布分享,不保证后续更新。
免责声明:
本脚本完全免费、开源。作者不保证脚本不存在bug。如使用本脚本时因bug、使用不当等原因引起的任何损失、纠纷需用户自行承担,否则请勿使用本脚本。
原理:
通过Tampermonkey内置函数记录观看信息;使用jQuery每秒读取页面元素比对已记录观看信息返回观看记录结果。
(就这么简单,但我从来没见其他人做过。)
所有存储信息均保存在本地(准确来说是Tampermonkey存储目录),如换浏览器、换电脑后仍想保留之前观看记录需要自行备份导出导入存储信息(Tampermonkey自带的云同步似乎也可以自动做到)。
功能:
1、记录Bilibili已观看或已访问记录(包括观看类型、观看时长、观看百分比、观看时间、视频标题)
2、在视频页提示详细观看记录(第一次访问不会提示,仅在第二次访问后在视频页左下角进行提示)
2.5、左下角提示标签右键单击则直接删除本条观看记录,左键单击则直接跳转播放上次观看进度(已访问则无效果)
3、在首页、分区、UP主视频空间内实时提示简略观看记录(仅提示已观看+观看百分比或已访问。)
(4)、如配合【Bilibili Evolved】亦可在关注动态中提示简略观看记录
已知问题:
暂无
更新计划:
增加脚本可视化操作面板,开放部分自定义设置功能,开放历史记录列表(目前仅能从Tampermonkey脚本-存储中手动查看)(计划下个版本更新)
更新记录:
V1.3.3-temp(2022-7-23): 临时性更新,修复bug,目前无暇维护,代码写了一半,很多没完善,原定的正式更新又要咕2~3周了
a.支持稍后观看页面 //更新后失效 暂时废弃
b.支持视频内推荐视频记录对比 //不太稳定,可能随时失效
c.引入toaster替换原有view标签 //未正式启用
d.修复多P正则替换bug
e.更新css适配新版界面
f.修复bug
V1.3.2-temp(2022.7.2)
a.临时性更新,进一步优化记录观看时间
b.优化代码css,提高兼容性
(本脚本更新周期可能会非常长,但不会放弃。
预计下次正式更新大概率为7月中下旬,计划有一波大更新,需要重写大量代码)
V1.3.1-temp(2022.7.1)
a.临时性更新,适配新版B站播放器,现在能够正常记录观看时间
b.修复跳转页面未传参bug,优化逻辑
(css问题由于作者工作繁忙,无暇维护,目前只能凑合使用。
本脚本更新周期可能会非常长,但不会放弃。
预计下次正式更新大概率为7月中下旬,计划有一波大更新,需要重写大量代码)
V1.3(2022-2-19):
a.重写大量代码(原本准备发布的代码丢失 被迫重写…… 写着写着发现 咦 这里竟然可以重新改进),优化逻辑
setInterval改写为setTimeout嵌套,取消原600秒显示限制,现无显示上限
优化小标签代码、CSS(计划下个版本可自定义小标签内容)
优化MutationObserver代码,现在能准确判断及记录单页内跳转后的观看信息
将原先1000ms判断一次小标签改为3000ms(计划下个版本可自定义判断间隔),防止原先偶尔出现无法加载评论区与视频推荐bug(经查明本脚本与官方stardust.js冲突,具体原因未查明(stardust.js被混淆了 懒得逆向=-=))
b.适配发布时新版UI、Bilibili Evolved V2.1.4
V1.2(2021-10-5):
a.脚本现在支持搜索页面
b.优化小标签代码
c.新增多P小标签信息提示(现在悬浮在小标签上会显示所有分P观看信息)
d.修复观看百分比计算bug及观看时间跳转bug
e.修复偶尔不记录观看信息bug(可能仍存在问题)
f.若干细小优化
V1.1(2021-9-4):
a.修复视频选集bug
b.增加视频选集多P独立观看信息记录
c.现在bilibili视频页内点击相关视频跳转页面也能正常记录观看信息了(*感谢DevSplash大佬提供的方法)
d.若干细小优化
e.更换jQuery CDN
v1.0(2021-8-28):
a.首次公开发布
*/
const debug=true;
GM_addStyle (`
.BvH-tag{position: absolute;margin: .5em!important;padding: 0 5px!important;height: 20px;line-height: 20px;border-radius: 4px;color: #fff;font-style: normal;font-size: 12px;background-color: rgb(122 134 234 / 70%);z-index:108;}
.BvH-tag-small{position: absolute;margin: .2em!important;padding: 0 4px!important;height: 18px;line-height: 18px;border-radius: 4px;color: #fff;font-style: normal;font-size: 10px;background-color: rgb(122 134 234 / 70%);z-index:108;}
.BvH-tag-big{position: absolute;margin: .5em!important;padding: 0 5px!important;height: 22px;line-height: 23px;border-radius: 4px;color: #fff;font-style: normal;font-size: 14px;background-color: rgb(122 134 234 / 70%);z-index:108;}
`);
/*
unsafeWindow.eval(GM_getResourceText("toastr_js")); //引入toastr.js
GM_addStyle(GM_getResourceText("toastr_css").replace(/width\: 300px\;/g,"")); //引入toastr.css 并修改
*/
let record_p=GM_listValues().filter(i=>i.indexOf("?p=")>-1);
//unsafeWindow.$=$;
function 视频页初始化(){
!function 等待页面加载(){
let startTime=new Date().getTime()
if(typeof __INITIAL_STATE__ !="undefined" && __INITIAL_STATE__.videoData){
console.log("BvH debug: 加载成功")
main()
}else{
let now=new Date().getTime()
let diff=now-startTime
if(diff>10000){
console.log("BvH debug: 等待页面加载超时"+unsafeWindow.location.href)
}else
{
setTimeout(等待页面加载,100)
}
}
}
}
function main(){
function 获取cid(){
return __INITIAL_STATE__.videoData.pages[__INITIAL_STATE__.p-1].cid
}
let info=__INITIAL_STATE__
let videoData=info.videoData
let page=info.p
let _cid=videoData.pages[page-1].cid
let 标题=videoData.title
let pages=videoData.pages
if(pages.length>1){
标题+="_"+pages[page-1].part
}
let BV,BV记录,BV类型,BV时间,页面类型,观看时长,总时长,观看百分比
let mark=document.querySelector("video").played.length==0?false:true
let 当前页面=unsafeWindow.location.href
let 跳转标记=true
let temp=0
let video_src
let uuid=function (){ //随机标识符 debug用
let random_string="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
let result=""
for (let i=0;i<5;++i){
result += random_string.charAt(Math.floor(Math.random() * random_string.length))
}
return result
}()
获取当前页面()
if(debug){
console.log("BvH debug:"+unsafeWindow)
console.log("BvH debug:"+BV)
}
(页面类型=="video" || 页面类型=="medialist") && BV && 调用播放页面函数()
function 调用播放页面函数(){
if(BV类型){
views(BV类型,观看时长,观看百分比,BV时间,BV)
}else{
GM_setValue(BV,["已访问",,,time(),标题])
}
let 播放绑定计时器=setInterval(()=>{
if($("bwp-video[src]").length>0){
console.log("BvH log:已绑定bwp-video")
clearInterval(播放绑定计时器)
video_src=$("bwp-video[src]").attr("src")
alert("请反馈给作者:bwp-video[src]"+BV)
if($("bwp-video[src]")[0].currentTime>"0.001" && mark==false){
alert("BvH Error:$(\"bwp-video[src]\")[0].currentTime"+$("bwp-video[src]")[0].currentTime+" "+mark.toString())
mark=true
}
$("bwp-video[src]").on('play',()=>{
记录观看("bwp-video[src]")
})
}
if($("video[src]").length>0){
console.log("BvH log:已绑定video")
clearInterval(播放绑定计时器)
video_src=$("video[src]").attr("src")
if($("video[src]")[0].currentTime>"0.001" && mark==false){
alert("BvH Error:$(\"video[src]\")[0].currentTime+"+$("video[src]")[0].currentTime+" "+mark.toString())
mark=true
}
setTimeout(视频内BV对比初始化,3000)
$("video[src]").on('play',()=>{
记录观看("video[src]")
})
}
},100)
mark && 记录观看("if(mark)")
unsafeWindow.onbeforeunload = function () {
console.log("BvH log:onbeforeunload"+BV类型)
switch(BV类型){
case "已观看":
mark && 记录观看("onbeforeunload",true)
break
case "已访问":
BV && GM_setValue(BV,["已访问",,,time(),标题])
break
case "已删除":
break
default:
console.log("BvH onbeforeunload Error:"+BV类型)
}
}
let duration
let current
let observer = new MutationObserver(function (m) {
m.forEach(function(e){
let target_className=e.target.className
if(target_className=='bpx-player-ctrl-time-duration' ||target_className=='bpx-player-ctrl-time-current'){
e.removedNodes.forEach(function(ee){
if(target_className=='bpx-player-ctrl-time-duration' && ee.data!="00:00"){
duration=ee.data
}
if(target_className=='bpx-player-ctrl-time-current'){
current=ee.data
}
if(duration && current && video_src!=$("video[src]").attr("src")){
if(BV类型=="已观看" && mark==true && BV){
观看时长=current
总时长=duration
观看百分比=Math.round(观看时长.split(":").reverse().map((item,index)=>(item*=Math.pow(60,index))).reduce((total,item)=>(total+=item))/总时长.split(":").reverse().map((item,index)=>(item*=Math.pow(60,index))).reduce((total,item)=>(total+=item))*100)+"%"
GM_setValue(BV,[BV类型,观看时长,观看百分比,time(),标题])
}
current=null
duration=null
console.log("BvH log:LINK START")
$("#view").remove()
跳转标记=false
observer.disconnect()
!function 跳转判断(旧页面){
if(/((BV|bv)[A-Za-z0-9]+(\?p=[0-9]+)?)|(av\d+(\?p=[0-9]+)?)/g.exec(unsafeWindow.location.href)[0].replace(/\?p\=1$/,"")==旧页面){
setTimeout(()=>{
跳转判断(旧页面)
},100)
console.log("BvH log:link link")
}else{
console.log("BvH log:LINK END")
main(false)
}
}(BV)
}
})
}
})
})
let 播放器绑定=setInterval(()=>{
if($("#bilibili-player").length>0){
clearInterval(播放器绑定)
observer.observe($("#bilibili-player")[0], {
childList: true,
subtree: true
})
}
},100)
//随时可能失效
function 视频内BV对比初始化(){
if(!$(".rec-list div:first")[0].__vue__){
alert("BvH error:$(\".rec-list div:first\")[0].__vue__")
return
}
let selector=$(".rec-list div:first")[0].__vue__.$parent.$children
let 视频内BV=[selector[selector.length==20?0:20]._data.link.match(/BV[0-9a-zA-Z]+/)[0]]
for(let i=1;i<8;i++){
视频内BV.push(selector[i]._data.link.match(/BV[0-9a-zA-Z]+/)[0])
}
console.log(视频内BV)
!function 视频内BV对比(){
视频内BV.forEach((href,index)=>{
let text=GM_getValue(href)
let href_p=record_p.filter(item=>(item.indexOf(href)>-1))
if(text){
href_p.unshift(href)
}else if(href_p.length>0){
text=GM_getValue(href_p[0])
}
let 状态,百分比,时间,文本
if(text){
状态=text[0]
百分比=text[2] || ""
时间=text[3]
文本=href_p.length>1 ? "已记录 多P" : 状态+百分比
}
let selector=$(".bpx-player-ending-related-item-img").eq(index).prev()
if(selector.hasClass("BvH-tag-big")==true){
if(selector.text()!=文本){ //判断小标签内容
selector.remove() //不同则删除,后续重新添加
}else{
return //相同则返回
}
}
if(!text){
return
}
if(href_p.length>1){ //多P
时间+=(href_p[0].indexOf("?")>-1?" "+href_p[0].split("?")[1].replace("=","").replace("p","P")+" ":" P1 ")+状态+百分比
href_p.splice(0,1)
href_p.forEach(item=>{
let item_value=GM_getValue(item)
item_value[2]=item_value[2] || ""
时间+=("
"+item_value[3]+" "+item.split("?")[1].replace("=","").replace("p","P")+" "+item_value[0]+item_value[2])
})
}
$(".bpx-player-ending-related-item-img").eq(index).before(小标签(文本,时间,"BvH-tag-big"))
})
跳转标记==true && setTimeout(视频内BV对比,3000)
}()
}
}
function 记录观看(来源,final=false){
观看时长=$(".bilibili-player-video-time-now").text() || $(".bpx-player-ctrl-time-current").text()
总时长=$(".bilibili-player-video-time-total").text() || $(".bpx-player-ctrl-time-duration").text()
观看百分比=Math.round(观看时长.split(":").reverse().map((item,index)=>(item*=Math.pow(60,index))).reduce((total,item)=>(total+=item))/总时长.split(":").reverse().map((item,index)=>(item*=Math.pow(60,index))).reduce((total,item)=>(total+=item))*100)+"%"
mark=true
BV类型="已观看"
console.log("BvH log:已观看"+来源+mark.toString())
if(final && BV){
GM_setValue(BV,[BV类型,观看时长,观看百分比,time(),标题])
}
}
function 获取当前页面(_BV){
BV=__INITIAL_STATE__.bvid+(__INITIAL_STATE__.p>1?`?p=${__INITIAL_STATE__.p}`:"")
unsafeWindow.BV=BV
BV记录=GM_getValue(BV)
if(BV记录){
BV类型=BV记录[0]
观看时长=BV记录[1]
观看百分比=BV记录[2]
BV时间=BV记录[3]
}
页面类型=getBV(当前页面,3)
//标题=document.title
if(_BV){
调用播放页面函数()
}
}
function views(BV类型_,观看时长,观看百分比,BV时间,BV号){
let 时长
if (观看时长){
时长=`
${观看时长}(${观看百分比})`
BV号=BV号+"
左键单击跳转视频播放进度
右键单击删除视频记录信息"
}else{
时长=``
BV号=BV号+"
右键单击删除视频记录信息"
}
$("body").append(`
${BV类型_}${时长}
${BV时间.split(" ")[0]}
${BV时间.split(" ")[1]}