您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
显示合集整体观看进度,方便掌控学习进度,合理安排学习时间。
// ==UserScript== // @name Bilibili合集观看进度 // @namespace https://github.com/zkytech/Tampermonkey_scripts // @version 0.7.2 // @description 显示合集整体观看进度,方便掌控学习进度,合理安排学习时间。 // @author zkytech // @include *://www.bilibili.com/video/BV* // @include *://www.bilibili.com/video/av*?p=* // @grant none // ==/UserScript== (function () { 'use strict'; var dict = [ 'f', 'Z', 'o', 'd', 'R', '9', 'X', 'Q', 'D', 'S', 'U', 'm', '2', '1', 'y', 'C', 'k', 'r', '6', 'z', 'B', 'q', 'i', 'v', 'e', 'Y', 'a', 'h', '8', 'b', 't', '4', 'x', 's', 'W', 'p', 'H', 'n', 'J', 'E', '7', 'j', 'L', '5', 'V', 'G', '3', 'g', 'u', 'M', 'T', 'K', 'N', 'P', 'A', 'w', 'c', 'F' ] var cn_n = 177451812; var cn_a = '100618342136696320'; function BigAdd(a, b) { var min_str = a.length < b.length ? a.split("").reverse() : b.split("").reverse(); var max_str = a.length >= b.length ? a.split("").reverse() : b.split("").reverse(); var quotient = 0, remainder = 0; var resutl = []; var temp = 0; for (var i = 0; i < min_str.length; i++) { temp = parseInt(min_str[i]) + parseInt(max_str[i]) + quotient; quotient = parseInt(temp / 10); //进位 remainder = temp % 10; //余数 resutl.push(remainder); } for (i; i < max_str.length; i++) { temp = parseInt(max_str[i]) + quotient; quotient = temp / 10; //进位 remainder = temp % 10; //余数 resutl.push(remainder); if (temp < 10 && i < max_str.length - 1) { return max_str.slice(i + 1).reverse().join("") + resutl.slice().reverse().join(""); } } } function BigDiv(a, b) { var alen = a.length, blen = b.length; var quotient = 0, remainder = 0; var result = [], temp = 0; for (var i = 0; i < alen; i++) { temp = remainder * 10 + parseInt(a[i]); if (temp < b) { remainder = temp; result.push(0); } else { quotient = parseInt(temp / b); remainder = temp % b; result.push(quotient); } } return [result.join("").replace(/\b(0+)/gi, ""), remainder]; } /** * av 转 bv * @param {*} av */ function toBv(av) { var p = BigAdd((av ^ cn_n).toString(), cn_a); var res = []; for (var i = 0; i < 10; i++) { res.push( BigDiv( BigDiv( p, Math.pow(58, i).toString() )[0], '58' )[1] ); } var mix = []; var d = [6, 2, 4, 8, 5, 9, 3, 7, 1, 0]; var temp = res.map(function (v) { return dict[v]; }); for (var m in d) { mix.push(temp[d[m]]); } return mix.join(''); } let total_duration = 0 // 首先必须是BV链接才执行后面的程序 if (window.location.pathname.includes("/video/BV") || (window.location.pathname.includes("/video/av") && window.location.search.includes("p="))) { let bvid = "" if (window.location.pathname.includes("/video/BV")) { bvid = window.location.pathname.split("/")[2].slice(2) } else { // av转bv const avid = window.location.pathname.split("/")[2].slice(2) console.log(avid) bvid = toBv(avid + "") } console.log("bvid=", bvid) // 请求API获取合集信息 fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`).then(res => res.json()).then(res => { const data = res.data // 合集整体进度显示 const container = document.createElement("div") container.setAttribute("class", "bpx-player-ctrl-btn bpx-player-ctrl-time") container.innerHTML = "<div class=\"bpx-player-ctrl-time-label\" name=\"time_textarea_\" id=\"zky_total_time_wrapper\">" + " 合集:" + "<span class=\"bpx-player-ctrl-time-current\" name=\"time_textarea_\" id=\"zky_finished_time\">00:00</span>" + "<span class=\"bpx-player-ctrl-time-divide\" name=\"time_textarea_\"> / </span>" + "<span class=\"bpx-player-ctrl-time-duration\" name=\"time_textarea_\" id=\"zky_total_time\">00:00</span>" + "  <span id=\"zky_finished_percent\">00.00%</span>" + " <span id=\"zky_target_distance\" style=\"display:none\">目标倒计时:<span>00:00</span></span>" + "</div>" // 目标进度设定框 const time_plan_tools = document.createElement("div") time_plan_tools.setAttribute("id", "zky_time_plan_tools") time_plan_tools.setAttribute("style", "margin-bottom:20px;background-color:rgb(244, 244, 244);height:46px;position:relative") let target_str = "00:00:00" if (localStorage[`zky_target_${bvid}`] !== undefined) { target_str = format_seconds(localStorage[`zky_target_${bvid}`]) } let current_p = "" // 当前分P的序列号 time_plan_tools.innerHTML = "<label for=\"zky_target_time_input\" id=\"zky_target_time_input_label\" style=\"font-size:16px;color:#222;padding-left:16px;line-height:46px\" >设定观看目标:</label>" + "<input value=\"" + target_str + "\" id=\"zky_target_time_input\" name=\"zky_target_time_input\" style=\"width:50px\"/> / <span id=\"zky_target_time_input_total\">00:00:00</span>  <span style=\"position:absolute;right:14px;margin-top:16px;margin-bottom:16px;cursor:pointer\" id=\"zky_clear_target_btn\">清除</span>" exec_when_element_exist(function () { const targets = document.querySelectorAll('.list-box > li') const options = { attributes: true, //观察node对象的属性 childList: false, subtree: false, attributeFilter: ['class'] //只观察class属性 } function apply_element_change(){ const temp_p = get_current_p() if (temp_p === current_p) { //分P未发生改变 return } else { current_p = temp_p } exec_when_element_exist( function () { // 将自定义组件添加到html document.querySelector(".right-container-inner").insertBefore(time_plan_tools, document.querySelector("#danmukuBox")) document.querySelector(".bpx-player-control-bottom-left").appendChild(container) // 绑定事件:设定目标时间 document.querySelector("#zky_target_time_input").addEventListener("keydown", function (e) { if (e.keyCode === 13) { const t_str = e.target.value localStorage[`zky_target_${bvid}`] = str_to_seconds(t_str) update_() update_mark() } }) // 绑定事件:清除目标设定 document.querySelector("#zky_clear_target_btn").addEventListener("click", function () { localStorage.removeItem(`zky_target_${bvid}`) document.querySelector("#zky_target_time_input").value = "00:00:00" update_mark() update_() }) update_mark() /** * 更新列表中的小圆点标记 */ function update_mark() { const target_p = get_target_p(bvid, data) // 观看目标的终点分P的序列号 const page_list = [...document.querySelectorAll(".list-box > li > a")] const target_page = page_list[target_p - 1] const progress_p = page_list.slice(current_p, target_p - 1) // 首先清除所有标记 page_list.forEach((v, i) => { if (i < current_p || i >= target_p) { v.classList.remove("zky_target_p") v.classList.remove("zky_progress_p") v.classList.remove("zky_p_mark") } }) // 标记目标分P localStorage[`zky_target_${bvid}`] && (target_page.classList.add("zky_target_p") || target_page.classList.add("zky_p_mark")) // 标记待观看的分P progress_p.forEach(v => { v.classList.add("zky_progress_p"); v.classList.add("zky_p_mark") }) // 进度条标记 if (target_p === current_p && !document.querySelector("#zky_controller_progress_mark")) { const cp_mark = document.createElement("div") cp_mark.setAttribute("id", "zky_controller_progress_mark") cp_mark.setAttribute("style", "background-color:#52C41A;width:2px;height:5px") document.querySelector(".bui-bar-wrap").appendChild(cp_mark) } } const mb1 = new MutationObserver(function (mr, obs) { update_() }) // 监听播放器全屏状态改变 mb1.observe(document.querySelector("#bilibili-player"), options) total_duration = data.duration // 合集总时间(秒) const total_str = format_seconds(total_duration) // 格式化(00:00:00)后的合集总时间 // 将合集总时间更新到页面 document.querySelector("#zky_total_time").textContent = total_str document.querySelector("#zky_target_time_input_total").textContent = total_str update_() // 监听播放器时间变化,同步更新合集完成时间 document.querySelector(".bpx-player-ctrl-time-current").addEventListener("DOMSubtreeModified", update_) }, ".bpx-player-ctrl-time" ) } apply_element_change() const mb = new MutationObserver(function (mutationRecord, observer){apply_element_change()} ) targets.forEach(v => mb.observe(v, options)) }, ".list-box") // 当窗口大小变化时进行更新 window.addEventListener("resize", update_) /** * 更新页面上显示的时间进度 */ function update_() { try { const target_p = get_target_p(bvid,data) // 观看目标的终点分P的序列号 // 当前是否处于全屏/网页全屏/宽屏模式 // let show_ext_info = document.querySelector("#bilibiliPlayer").className.includes("screen") const video_width = document.querySelector(".bpx-player-control-wrap").clientWidth document.querySelector("#bilibili-player").className.includes("screen") && video_width < 2000 && document.querySelector(".bpx-player-control-bottom-center").setAttribute("style","display:none"); let show_ext_info = false video_width > 1200 ? show_ext_info = true : show_ext_info = false // 只要播放器宽度超过900 也显示倒计时信息 const current_p = get_current_p() let finished_duration = 0 // 完成观看的总秒数 const target_time = localStorage[`zky_target_${bvid}`] let target_distance = null // 距离观看目标的秒数 data.pages.slice(0, current_p - 1).forEach(v => finished_duration += v.duration) const [min_, sec_] = document.querySelector(".bpx-player-ctrl-time-current").textContent.split(":") finished_duration += Number(min_) * 60 + Number(sec_) // 已完成所有视频的秒数 // 如果设定了目标,且当前未完成目标就显示目标进度信息 if (target_time && target_time > finished_duration) { target_distance = target_time - finished_duration show_ext_info && document.querySelector("#zky_target_distance").removeAttribute("style") document.querySelector("#zky_target_distance > span").textContent = format_seconds(target_distance) } // 如果没有设定目标,就隐藏进度信息 else if (!target_time) { document.querySelector("#zky_target_distance").setAttribute("style", "display:none") } // 设定了目标且目标已经完成,显示“已完成目标” else { show_ext_info && document.querySelector("#zky_target_distance").removeAttribute("style") document.querySelector("#zky_target_distance > span").textContent = "已完成目标" } show_ext_info ? document.querySelector("#zky_finished_percent").removeAttribute("style") : (document.querySelector("#zky_target_distance").setAttribute("style", "display:none") || document.querySelector("#zky_finished_percent").setAttribute("style", "display:none")) // 设定目标分P的进度条提示 if (target_p === current_p) { const ctrl_width = document.querySelector(".bui-bar-wrap").offsetWidth document.querySelector("#zky_controller_progress_mark").style.transform = `translateX(${ctrl_width * ((target_time - (finished_duration - Number(min_) * 60 - Number(sec_)))/data.pages[current_p-1].duration )}px)` } const finished_percent = (finished_duration * 100 / total_duration).toFixed(2) // 完成百分比 const finished_str = format_seconds(finished_duration) const percent_str = finished_percent < 10 ? "0" + finished_percent + "%" : finished_percent + "%" document.querySelector("#zky_finished_time").textContent = finished_str document.querySelector("#zky_finished_percent").textContent = percent_str } catch (e) { console.log(e) } } }) } // 提示小圆点 add_new_style(".zky_p_mark > span:before{content:\"\";position:relative;top:50%;transform:translateY(-50%);right:0px;width: 5px;height: 5px;box-sizing: border-box;color: white;text-align: center;border-radius: 5px;display: inline-block;}") add_new_style(".zky_target_p > span:before {" + "background: #52C41A " + "}") add_new_style(".zky_progress_p > span:before {" + "background: #FADB14" + "}") /** * 获取观看目标的分P序列号 * @param {string} bvid * @param {any[]} data */ function get_target_p(bvid, data) { let target_p = 0 let __temp_duration = 0 localStorage[`zky_target_${bvid}`] && data.pages.some((v, i) => { __temp_duration += v.duration target_p = i return __temp_duration >= localStorage[`zky_target_${bvid}`] ? true : false }) return target_p + 1 } /** * 获取当前分P序列号 */ function get_current_p() { let current_p = getQueryVariable("p") // 当前分P序列号 // 如果路径中没有分p的id,当前分p就是1 if (current_p === false) { current_p = 1 } return Number(current_p) } /** * 当元素出现时,执行函数 * @param {function} fn 需要执行的函数 * @param {string} elem_selector 元素选择器 */ function exec_when_element_exist(fn, elem_selector) { const __interval_a = setInterval(function () { if (document.querySelector(elem_selector) !== null) { clearInterval(__interval_a) fn() } }, 500) } /** * 将"01:00:00"格式的时间转换为秒数 * @param {string} time_str 格式为"01:00:00"时间字符串 */ function str_to_seconds(time_str) { const time_nums = time_str.split(":").map(val => Number(val)).reverse() return time_nums[0] + time_nums[1] * 60 + (time_nums[2] ? time_nums[2] * 60 * 60 : 0) } /** * 将秒数转换为"01:00:00"的格式 * @param {number} seconds_num 秒数 */ function format_seconds(seconds_num) { const seconds = seconds_num % 60 const minutes = (seconds_num % 3600 - seconds) / 60 const hours = Math.floor(seconds_num / 3600) const seconds_str = seconds > 9 ? seconds + "" : `0${seconds}` const minutes_str = minutes > 9 ? minutes + ":" : `0${minutes}:` const hours_str = hours === 0 ? '' : hours > 9 ? hours + ":" : `0${hours}:` return `${hours_str}${minutes_str}${seconds_str}` } /** * 获取url路径参数值 * @param {string} variable 需要查询的参数名 */ function getQueryVariable(variable) { const query = window.location.search.substring(1); const vars = query.split("&"); for (let i = 0; i < vars.length; i++) { const pair = vars[i].split("="); if (pair[0] == variable) { return pair[1]; } } return false; } /** * 全局引入自定义style文本 * @param {string} newStyle css文本 */ function add_new_style(newStyle) { let styleElement = document.getElementById('zk_styles_js'); if (!styleElement) { styleElement = document.createElement('style'); styleElement.type = 'text/css'; styleElement.id = 'zk_styles_js'; document.getElementsByTagName('head')[0].appendChild(styleElement); } styleElement.appendChild(document.createTextNode(newStyle)); } })();