// ==UserScript==
// @name B站观看内容统计-我的时间不见了
// @version 0.0.10
// @description 在b站网页首页获取历史观看记录近三个月,每次刷新首页会自动获取并存储数据,可以到浏览器控制台 F12 查看
// @author strangeZombies
// @namespace https://www.github.com/strangeZombies
// @match https://www.bilibili.com
// @match https://www.bilibili.com/?*
// @match https://www.bilibili.com/index.html
// @require https://static.hdslb.com/js/jquery.min.js
// @icon https://static.hdslb.com/images/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// ==/UserScript==
/* globals $ */
/* jshint esversion: 8 */
//GM_deleteValue("lishijilulist");
//GM_addStyle
// 版本 latest - script - bilibili - history - ajax - 202302201449.js
// 原作 判官喵的B站观看内容统计-我的时间都去哪了
// 目前 将其页面跳转获取历史记录更改为Ajax异步的方式
// 未完成 统计页面模块 下载历史记录模块
// 使用方法 f12控制台复制对象 |在本脚本使用函数GM_getvalue('lishijilulist')
// 建议加装 ClearURLs 扩展插件
// 由于能力有限不保证脚本出厂质量 | 我自己用好像没问题 | 也许会更新
(function () {
'use strict';
//下载 ajax get请求携带cookie Json
async function getOneAjax(addr) {
try {
let res = await $.ajax({
type: "get",
url: addr,
dataType: "json",
async: true,
// 允许请求携带cookie
xhrFields: {
withCredentials: true
}
}).then(json => { return json; })
return res;
} catch (err) {
return err;
}
}
//读取缓存模块 返回string->json格式化缓存内容
// "lishijilulist"
function GMgetStrToJson(cacheName) {
// 初始字符
let cacheJson, cacheStr;
// 获取缓存
cacheStr = GM_getValue(cacheName);
// 如果存在缓存则格式化为Json并返回
if (cacheStr == undefined) {
cacheJson = cacheStr; // 如果不存在则为 undifined
} else {
cacheJson = JSON.parse(cacheStr);
}
return cacheJson;
}
//存储缓存列表模块 Json
function GMsetJson(cacheName, cacheValue) {
let cacheValueTemp = JSON.stringify(cacheValue);
GM_setValue(cacheName, cacheValueTemp);
}
//初始化标头存储模块
function headerCache(firstCache, preJson, cacheBucket) {
let getId, getViewAt, PerJsonBucketLen;
PerJsonBucketLen = preJson.data.list.length;
if (PerJsonBucketLen != 0) {
//提取最后观看数据项oid和观看时间
getId = preJson.data.list[0].history.oid;
getViewAt = preJson.data.list[0].view_at;
}
//判断是否为首次缓存
if (firstCache == 1) {
if (cacheBucket.length == 0) {
console.log("首次获取数据时间较长请耐心等待!")
//是首次缓存则 添加标头项首次缓存标记,最后观看oid和时间数据,返回存储列表
cacheBucket[0] = ({ first_cache: firstCache, last_oid: getId, last_view_at: getViewAt, thisCache_len: 0 })
}
return cacheBucket;
} else {
//非首次缓存存储最后观看数据项
//是则 把此页最后观看数据添加到标头项备用栏,返回存储列表
cacheBucket[0].beiyong_last_oid = getId;
cacheBucket[0].beiyong_last_view_at = getViewAt;
cacheBucket[0].bencicunchu_len = 0;
return cacheBucket;
}
}
//openai 计数统计
const samer = {
same: 0,
page: 0,
inSame() {
this.same++;
},
getSame() {
return this.same;
},
reSame() {
this.same = 0;
},
inPage() {
this.page++;
},
getPage() {
return this.page;
}
};
//获取进度 (未完成)
const preEC = {
// 进度条
preE: 0,
setPreE(preE) {
this.preE = preE;
return this.preE;
},
getPreE() {
return this.preE;
}
}
//需要被循环执行的模块
//prejson数据添加到存储列表模块
function preJsonToCacheBucket(preJson, i, cacheBucket) {
//每个视频bvid 每个视频时长 每个视频观看时长 每个视频观看时间
let every_oid, //视频是av号,专栏是cv号,直播是直播间号
every_author_mid, //up主uid
every_author_name, //up主名字
every_badge, //此条记录的类型
every_title, //标题
every_duration, //视频时长
every_progress, //观看时长
every_view_at; //观看时间
//可能增加的数据
//every_cover,
//every_author_face;
//开始赋值
every_oid = preJson.data.list[i].history.oid;
every_author_mid = preJson.data.list[i].author_mid;
every_author_name = preJson.data.list[i].author_name;
every_badge = preJson.data.list[i].badge;
every_title = preJson.data.list[i].title;
every_duration = preJson.data.list[i].duration;
every_progress = preJson.data.list[i].progress;
every_view_at = preJson.data.list[i].view_at;
//可能增加的数据
//every_cover = preJson.data.list[i].cover;
//every_author_face = preJson.data.list[i].author_face;
//判断是否有重复内容
//寻找相同观看时间内容
let sure = cacheBucket.find(i => i.view_at === every_view_at);
//获取本次存储计数
let bencicunchu_len = cacheBucket[0].bencicunchu_len;
//如果没有找到则 添加数据后返回存储列表
if (sure == undefined) {
// console.log('没找到');
cacheBucket.push({
oid: every_oid, author_mid: every_author_mid,
author_name: every_author_name, badge: every_badge, title: every_title,
duration: every_duration, progress: every_progress, view_at: every_view_at
});
cacheBucket[0].bencicunchu_len = bencicunchu_len + 1;
return cacheBucket;
} else {
samer.inSame();
//console.log('有找到',samer.getSame());
//有找到则 不做处理直接返回存储列表
return cacheBucket;
}
}
//处理json数据加入存储列表模块 调用preJsonToCacheBucket
function jsonToCacheBucket(preJson, cacheBucket) {
console.log(cacheBucket);
//提取首次存储标记
let firstCache = cacheBucket[0].first_cache;
//提取json数据列表长度
let preJsonListLen = preJson.data.list.length;
//提取记录的最后一个观看时间
let lastViewAt = cacheBucket[0].last_view_at;
//提取记录的备用最后一个观看时间
let backupLastViewAt = cacheBucket[0].beiyong_last_view_at
let backupLastOid = cacheBucket[0].beiyong_last_oid;
//列表执行计数
let i;
//获取时间戳
let tistime = Date.now();
//判断数据长度是否为0
if (preJsonListLen == 0) {
//如果没有数据则是提取到最后一页,将首次存储状态改为0无效,并存储 返回下页状态为0
cacheBucket[0].first_cache = 0;
cacheBucket[0].last_jiancha_time = tistime;
GMsetJson('lishijilulist', cacheBucket);
console.log("最后一条");
return 0;
//返回状态0不再进行下个页面获取
} else {
//有数据则判断是否为首次缓存
if (firstCache == 1) {
//首次缓存直接循环执行 api页面json数据添加到存储列表模块
for (i = 0; i < preJsonListLen; i++) {
cacheBucket = preJsonToCacheBucket(preJson, i, cacheBucket);
}
GMsetJson('lishijilulist', cacheBucket);
//返回状态1继续进行下个页面获取
return 1;
} else {
//非首次缓存则,判断缓存最后一个观看时间记录能否比页面数据的观看时间记录更小
//对比缓存的最后一个观看时间与记录列表时间大小
// BUG: IF IT IS NOT EXIST , THERE IS NO RESULT
for (i = 0; i < preJsonListLen; i++) {
// 之前的时间小于现在获取的时间
if (lastViewAt < preJson.data.list[i].view_at) {
//如果时间小于列表时间则加入存储列表
cacheBucket = preJsonToCacheBucket(preJson, i, cacheBucket);
} else if (lastViewAt == preJson.data.list[i].view_at) {
//如果时间等于列表时间则停止获取,将备用最后时间添加到存储列表的最后时间,直接存储已有列表
cacheBucket[0].last_view_at = backupLastViewAt;
cacheBucket[0].last_oid = backupLastOid;
cacheBucket[0].last_jiancha_time = tistime;
GMsetJson('lishijilulist', cacheBucket);
return 0;
//返回状态0不再进行下个页面获取
} else {
//如果时间大于列表时间则停止获取,将备用最后时间添加到存储列表的最后时间,直接存储已有列表
cacheBucket[0].last_jiancha_time = tistime;
GMsetJson('lishijilulist', cacheBucket);
return 0;
//返回状态0不再进行下个页面获取
}
}
//整页获取完后存储,返回状态1继续进行下个页面获取
GMsetJson('lishijilulist', cacheBucket);
return 1;
}
}
}
//获取一组数据
async function ajaxOneHistory(maxId, viewAt, businessId) {
let url = `https://api.bilibili.com/x/web-interface/history/cursor?max=${maxId}&view_at=${viewAt}&business=${businessId}`;
//console.log('正在获取', url);
await new Promise(resolve => setTimeout(resolve, 100));
let data = await getOneAjax(url);
return data;
}
// MAIN FETCH
async function ajaxHistory() {
//首次缓存标记,1有效,0无效 | 读取列表 | 存入列表 | 获取列表
let originCacheBucket, cacheBucket = [], preJson, firstCache;
//读取缓存
originCacheBucket = GMgetStrToJson('lishijilulist');
cacheBucket = originCacheBucket; // 如果没有则此处为undifined;
//判断缓存是否存在
if (originCacheBucket == undefined) {
//不存在则执行以下
//是首次缓存
firstCache = 1;
cacheBucket = [];
} else {
firstCache = 0; //007 006
}
preEC.setPreE(0);
let nextCache = 1;
let maxId = 0, viewAtId = 0, businessId = '';
while (nextCache == 1) {
preJson = await ajaxOneHistory(maxId, viewAtId, businessId).then(preJson => { return preJson });
preEC.setPreE(10);
cacheBucket = headerCache(firstCache, preJson, cacheBucket);
nextCache = jsonToCacheBucket(preJson, cacheBucket);
maxId = preJson.data.cursor.max;
viewAtId = preJson.data.cursor.view_at;
businessId = preJson.data.cursor.business;
//
if (samer.getSame() === 20) {
samer.inPage();
}
samer.reSame();
if (samer.getPage() === 2) {
// console.log('Enough');
nextCache = 0;
}
}
preEC.setPreE(20);
}
//备份数据
function BackupData(opts) {
let oldCacheBucket, backupCacheBucket;
if (opts == 1) {
//备份旧数据 1
oldCacheBucket = GMgetStrToJson("lishijilulist");
backupCacheBucket = oldCacheBucket;
GMsetJson('backupCacheBucket', oldCacheBucket);
backupCacheBucket = GMsetJson("backupCacheBucket", backupCacheBucket)
console.log(GMgetStrToJson('backupCacheBucket'),GM_listValues());
} else if (opts == 2) {
// 用备份还原数据 2
backupCacheBucket = GMgetStrToJson('backupCacheBucket');
if (backupCacheBucket == undefined | backupCacheBucket == '') {
console.log('没有备份数据');
} else {
GMsetJson('lishijilulist', backupCacheBucket);
}
} else if (opts == 3) {
// 删除旧有数据仅保留备份 3
GM_deleteValue("lishijilulist");
}
}
//数据展示
function historyResult() {
let his = pharseResult(GMgetStrToJson('lishijilulist'));
const info = ['今日', '昨日', '本周', '本月', '上月', '统计以来'];
$.each(his, (index, obj) => {
$("#bili-history-nums").append(
`<div style="margin: 0 .5rem;display:flex;flex-direction:column;">
<span style="position:relative;top:-1.4rem;border:1px solid #888;box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);">| ${info[index]} </span><span style="position:relative;top:-1.3rem;border:1px solid #888;box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);">| ${obj.av}次 </span><span style="position:relative;top:-1.2rem;border:1px solid #888;box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);">| ${pharseTime(obj.avtime)} </span></div>`)
});
}
//展示界面
function historyGUI() {
// border-radius: 8px 8px 8px 8px;border:1px solid #888;
$("<div id='bili-history' style='color: #888;min-height: 3rem;max-width:1000px;margin: 5px auto; display:flex;flex-direction:flex-wrap; justify-content:center;' ></div>").appendTo('.bili-header'); //⚙
$("<span id='bili-history-tips' style='display:block;padding-left: 5px;cursor: pointer;padding-right: 5px;text-align:top;height:1rem;'><div style='height:1.4rem;border:1px solid #888;box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);margin: .2rem 0'>| 你生命在哪里展开,</div><div style='height:1.4rem;border:1px solid #888;box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);'>| 哪里就是历史的界面</div></span>").appendTo("#bili-history");
$("<span id='bili-history-nums' style='display:flex;flex-direction:flex-wrap;justify-content: flex-start;padding-left: 5px;'> </span>").appendTo('#bili-history');
//$("<span style='padding-left: 5px; top: -5px;'>显示</span>").appendTo('#bili-history-nums');
$("<span id='bili-history-reduce' style='height:1.5rem;padding: 0 1rem;border: 1px solid #777;box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);top:-4.6rem;float:right;margin-right:10px;position:relative;'></span>").appendTo('#bili-history-tips');
//步骤控制以及显示界面
const timer2 = setInterval(() => {
const result = preEC.getPreE(); // 调用你的函数,获取返回值
if (result === 0) {
$('#bili-history-reduce').text('未开始');
} else if (result === 20) {
$('#bili-history-reduce').text('已获取');
historyResult();
//补充数据
preEC.setPreE(30);
clearInterval(timer2);
} else if (result === 10) {
$('#bili-history-reduce').text('获取中');
}
}, 1000); // 每秒钟执行一次函数
//是否完全完成了
const timer3 = setInterval(()=>{
const result = preEC.getPreE();
if(result ===30){
videoTag(GMgetStrToJson('lishijilulist'));
$('#bili-history-reduce').text('补全中');
} else if(result === 40){
$('#bili-history-reduce').text('已补全');
preEC.setPreE(50);
}else if(result === 50){
$('#bili-history-reduce').text('已完成');
console.log('观看数据',GMgetStrToJson('lishijilulist'));
clearInterval(timer3);
}
},1000)
$('#bili-history-reduce').click(function () {
let result = preEC.getPreE();
alert(result);
if(result >= 20){
// 获取历史记录数据并将其保存到一个Blob对象中
let blob = new Blob([JSON.stringify(GMgetStrToJson('lishijilulist'))], { type: "application/json" });
// 使用URL.createObjectURL()方法创建一个可以下载Blob数据的URL
let url = URL.createObjectURL(blob);
// 创建一个链接元素,并设置它的href和download属性
let link = document.createElement('a');
link.href = url;
link.download = 'history.json';
// 将链接元素添加到DOM中,并模拟单击该元素以触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
$('#bili-history-reduce').text('已下载');
}else {
$('#bili-history-reduce').text('未完成');
}
});
}
//数量时长计算模块
function pharseResult(cacheBucket) {
//获取时间戳
const nowtime = new Date(),
//获取年份
thisyear = nowtime.getFullYear(),
//获取当月的月份
thismonth = nowtime.getMonth() + 1,
lastmonth = nowtime.getMonth(),
//获取当前日期
today = nowtime.getDate(),
//当天0点时间戳
todaytime = (new Date(thisyear + "-" + thismonth + "-" + today) / 1000),
//前一天0点时间戳
yesterdaytime = (new Date(thisyear + "-" + thismonth + "-" + today) / 1000) - 24 * 60 * 60,
//6天前的时间戳
weektime = (new Date(thisyear + "-" + thismonth + "-" + today) / 1000) - 24 * 60 * 60 * 6,
//本月1号的时间戳
thismonthtime = (new Date(thisyear + "-" + thismonth + "-" + 1) / 1000),
//上月1号的时间戳
lastmonthtime = (new Date(thisyear + "-" + lastmonth + "-" + 1) / 1000);
//计算结果
let pharseResult = [],
judgeTime, //判断时间状态,5统计所有,4统计上月,3统计本月,2统计7天,1统计昨天,0统计今天
every_badge, //此条记录的类型
every_duration, //视频时长
every_progress, //观看时长
every_view_at, //观看时间
//获取数据长度
cacheBucketLen = cacheBucket.length,
//计数用
ntjs;
//逐个统计计算
for (ntjs = 1; ntjs < cacheBucketLen; ntjs++) {
//逐个获取数据
every_badge = cacheBucket[ntjs].badge;
every_duration = cacheBucket[ntjs].duration;
every_progress = cacheBucket[ntjs].progress;
every_view_at = cacheBucket[ntjs].view_at;
//计入统计所有
judgeTime = 5;
//执行集中统计模块 返回统计结果
pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
if (lastmonthtime < every_view_at && every_view_at < thismonthtime) {
//判断时间是否在上月范围内
judgeTime = 4;
//是则执行集中统计模块 返回统计结果
pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
}
if (thismonthtime < every_view_at) {
//判断时间是否在本月范围内
judgeTime = 3;
//是则执行集中统计模块 返回统计结果
pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
}
if (weektime < every_view_at) {
//判断时间是否在7天范围内
judgeTime = 2;
//是则执行集中统计模块 返回统计结果
pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
}
if (yesterdaytime < every_view_at && every_view_at < todaytime) {
//判断时间是否在昨天范围内
judgeTime = 1;
//是则执行集中统计模块 返回统计结果
pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
}
if (todaytime < every_view_at) {
//判断时间是否在今天范围内
judgeTime = 0;
//是则执行集中统计模块 返回统计结果
pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
}
}
return pharseResult;
}
//集中统计模块
function pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult) {
//判断是否属于视频
if (every_badge == "" || every_badge == "综艺" || every_badge == "电影" || every_badge == "番剧" || every_badge == "纪录片" || every_badge == "电视剧" || every_badge == "国创") {
if (pharseResult.length == 0) {
//赋值初始内容
for (let j = 0; j < 6; j++) {
pharseResult[j] = ({ av: 0, cv: 0, live: 0, avtime: 0, gktime: 0 });
}
}
//属于视频则添加计数,并加总视频时长和观看时长
pharseResult[judgeTime].av = pharseResult[judgeTime].av + 1;
pharseResult[judgeTime].avtime = pharseResult[judgeTime].avtime + every_duration;
//如果every_duration是-1值,则说明看完了此视频
if (every_progress == -1) {
every_progress = every_duration;
}
pharseResult[judgeTime].gktime = pharseResult[judgeTime].gktime + every_progress;
} else if (every_badge == "专栏" || every_badge == "笔记") {
//判断是否属于专栏
pharseResult[judgeTime].cv = pharseResult[judgeTime].cv + 1;
} else if (every_badge == "未开播" || every_badge == "直播中") {
//判断是否属于直播
pharseResult[judgeTime].live = pharseResult[judgeTime].live + 1;
}
//返回统计结果
return pharseResult;
}
//时分秒格式化模块
function pharseTime(changetime) {
let hh = parseInt(changetime / 3600);
if (hh < 10) hh = "0" + hh;
let mm = parseInt((changetime - hh * 3600) / 60);
if (mm < 10) mm = "0" + mm;
//let ss = parseInt((changetime - hh * 3600) % 60);
// if (ss < 10) ss = "0" + ss;
let geshihuatime = hh + ":" + mm;// + ":" + ss;
if (changetime > 0) {
return geshihuatime;
} else {
return "NaN";
}
}
//给数据添加视频描述
async function videoDisc(iii, cacheBucket) {
const cacheLen = cacheBucket.length;
//从什么位置开始
let tag, repair;
for (let i = iii; i < cacheLen; i++) {
//console.log(i);
tag = await getOneAjax(`https://api.bilibili.com/x/web-interface/view/detail/tag?aid=${cacheBucket[i].oid}`);
if (tag.code === 0) {
let tag_data = tag.data;
//视频标签循环获取及拼接
//设置无描述计数
let tag_name = '';
for (let k = 0; k < tag_data.length; k++) {
//设置 tag_name
if (tag_name == '') {
tag_name = '' + tag_data[k].tag_name;
} else {
tag_name = tag_name + ';' + tag_data[k].tag_name;
}
cacheBucket[i].tag_name = tag_name;
}
repair = cacheBucket[i].view_at;
}
//是否到最后的位置
if (cacheBucket[cacheLen - 1].view_at === repair) {
cacheBucket[0].first_cache = 8;
}
cacheBucket[0].repair = repair;
GMsetJson('lishijilulist', cacheBucket);
}
GMsetJson('lishijilulist', cacheBucket);
}
//根据数据特性判断是否要补全视频描述信息
function videoTag(cacheBucket) {
let cacheLen = cacheBucket.length;
let disc = cacheBucket[cacheLen - 1].hasOwnProperty('tag_name');
//从最早的数据开始处理
//最早数据有视频描述
if (cacheBucket[1].hasOwnProperty('tag_name')) {
//最新数据有 最早也有
if (disc) {
cacheBucket[0].repair = cacheBucket[cacheLen - 1].view_at;
cacheBucket[0].first_cache = 8 //完全不需要处理
} else { //最新没有 最早有
cacheBucket[0].first_cache = 7; //完全处理过但需更新
//console.log("需更新");
if (cacheBucket[0].hasOwnProperty('repair')) {
// 遍历数组,找到第一个属性prop等于value的元素
const foundElement = cacheBucket.find(e => e.view_at === cacheBucket[0].repair);
// 如果找到了元素,返回它在数组中的下标;否则返回-1
const index = foundElement ? cacheBucket.indexOf(foundElement) : -1;
videoDisc(index, cacheBucket);
}
}
} else {
//第一次直接完全处理
//最早没有 最新也没有
if (!disc) {
//由于是第一次则直接获取全部数组
cacheBucket[0].first_cache = 7;
videoDisc(1, cacheBucket);
} else { //最早没有 最新有
//不会出现的情况
}
}
preEC.setPreE(40);
}
//完成基本命令
function baseCommand() {
ajaxHistory();
historyGUI();
}
if (unsafeWindow.location.href.indexOf('bilibili.com') != -1) {
//BackupData(2);
baseCommand();
}
})();