// ==UserScript== // @name Biliplus Evolved // @version 1.0.0 // @description 简单的B+增强脚本 // @author DeltaFlyer // @copyright 2025, DeltaFlyer(https://github.com/DeltaFlyerW) // @license MIT // @match https://*.biliplus.com/* // @run-at document-end // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect api.bilibili.com // @connect comment.bilibili.com // @connect bangumi.bilibili.com // @connect www.bilibili.com // @connect delflare505.win // @icon https://www.biliplus.com/favicon.ico // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js // @namespace https://greasyfork.org/users/927887 // @downloadURL none // ==/UserScript=='''use strict'; let toastText = (function () { let html = `
` document.body.insertAdjacentHTML("beforeend", html) let bubbleContainer = document.querySelector('.df-bubble-container') function createToast(text) { console.log('toast', text) const bubble = document.createElement('div'); bubble.classList.add('df-bubble'); bubble.textContent = text; bubbleContainer.appendChild(bubble); setTimeout(() => { bubble.classList.add('df-show-bubble'); setTimeout(() => { bubble.classList.remove('df-show-bubble'); setTimeout(() => { bubbleContainer.removeChild(bubble); }, 500); // Remove the bubble after fade out }, 3000); // Show bubble for 3 seconds }, 100); // Delay before showing the bubble } return createToast })(); async function sleep(time) { await new Promise((resolve) => setTimeout(resolve, time)); } async function xhrGet(url) { function isCors(url) { if (url[0] === '/') return false // Extract the domain from the URL const urlDomain = new URL(url).hostname; // Extract the domain from the current page's URL const currentDomain = window.location.hostname; // Check if the domains are different (CORS request) return urlDomain !== currentDomain; } console.log('Get', url); if (isCors(url)) { // Use GM_xmlhttpRequest for cross-origin requests return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, withCredentials: true, onload: (response) => { if (response.status === 200) { resolve(response.responseText); } else { resolve(null); } }, onerror: (error) => { console.error('GM_xmlhttpRequest error:', error); resolve(null); }, }); }); } else { // Use XMLHttpRequest for same-origin requests return new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.withCredentials = true; xhr.send(); xhr.onreadystatechange = async () => { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseText); } else { resolve(null); } } }; }); } } function downloadFile(fileName, content, type = 'text/plain;charset=utf-8') { let aLink = document.createElement('a'); let blob = content if (typeof (content) == 'string') blob = new Blob([content], {'type': type}) aLink.download = fileName; let url = URL.createObjectURL(blob) aLink.href = url aLink.click() URL.revokeObjectURL(url) } let bv2av = (function bv2av() { //https://github.com/TGSAN/bv2av.js/tree/master let s = (` const table = 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf'; const max_avid = 1n << 51n; const base = 58n; const bvid_len = 12n; const xor = 23442827791579n; const mask = 2251799813685247n; let tr = []; for (let i = 0; i < base; i++) { tr[table[i]] = i; } /** * avid to bvid * @param {bigint} avid * @returns {string} bvid */ function enc(avid) { let r = ['B', 'V']; let idx = bvid_len - 1n; let tmp = (max_avid | avid) ^ xor; while (tmp !== 0n) { r[idx] = table[tmp % base]; tmp /= base; idx -= 1n; } [r[3], r[9]] = [r[9], r[3]]; [r[4], r[7]] = [r[7], r[4]]; return r.join(''); } /** * bvid to avid * @param {string} bvid * @returns {bigint} avid */ function dec(bvid) { let r = bvid.split(''); [r[3], r[9]] = [r[9], r[3]]; [r[4], r[7]] = [r[7], r[4]]; let tmp = 0n; for (let char of r.slice(3)) { console.log(char) let idx = BigInt(tr[char]); tmp = tmp * base + idx; } let avid = (tmp & mask) ^ xor; return avid; }`) eval(s) return dec })(); function domInserted(target, handle) { const observer = new MutationObserver(mutationList => { console.log("mutationList", mutationList) mutationList.filter(m => m.type === 'childList').forEach(m => { for (let e of m.addedNodes) { handle({target: e}) } }) } ); observer.observe(target, {childList: true, subtree: true}); } class CustomEventEmitter { constructor() { this.eventListeners = {}; } addEventListener(event, callback) { if (!this.eventListeners[event]) { this.eventListeners[event] = []; } this.eventListeners[event].push(callback); } removeEventListener(event, callback) { if (this.eventListeners[event]) { const index = this.eventListeners[event].indexOf(callback); if (index !== -1) { this.eventListeners[event].splice(index, 1); } } } postMessage(data) { const event = 'message' console.log(data) if (this.eventListeners[event]) { this.eventListeners[event].forEach(callback => { callback({data: data}); }); } } } class ObjectRegistry { constructor() { this.registeredObjects = new Set(); } register(obj) { if (this.registeredObjects.has(obj)) { throw new Error('Object is already registered.'); } this.registeredObjects.add(obj); } } function xmlunEscape(content) { return content.replace(/, /g, ';') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/'/g, "'") .replace(/"/g, '"') } async function aidQuery(event, init, jump) { function transform(src) { let dst = {} dst.id = src.id dst.ver = 1 dst.aid = src.id dst.lastupdatets = Math.floor(new Date().getTime() / 1000) dst.lastupdate = new Date().toLocaleString(); if (src.title === "已失效视频") { let pic = document.querySelector('.detail_cover') if (pic) { dst.title = document.title.slice(0, document.title.indexOf(" - ")) dst.pic = pic.src } } else { dst.pic = src.cover dst.title = src.title } dst.description = src.intro dst.tid = src.tid dst.typename = "tid_" + src.tid dst.created = src.pubtime dst.created_at = new Date(src.pubtime * 1000).toLocaleString() dst.author = src.upper.name dst.mid = src.upper.mid dst.play = src.cnt_info.play.toString() dst.coins = src.cnt_info.coin dst.review = src.cnt_info.reply dst.video_review = src.cnt_info.danmaku dst.favorites = src.cnt_info.collect dst.tag = "tag_undefined" let list = [] for (let page of src.pages) { list.push({ "page": page.page, "type": page.from, "cid": page.id, "vid": undefined, "part": page.title + "_时长" + page.duration + "秒", "duration": page.duration }) } dst.list = list return dst } let aid let href = new URL(window.location.href) if (href.searchParams.has("get_info") || init) { aid = unsafeWindow.av } else { aid = window.prompt("请输入要查询的aid或bvid", unsafeWindow.av) console.log('input', aid) if (!/^\d+$/.exec(aid)) { if (/^av\d+$/.exec(aid) || /^AV\d+$/.exec(aid)) { aid = aid.substring(2) } else if (/^BV/.exec(aid)) { aid = bv2av(aid) } else { alert("请输入正确的视频号,bv号请以BV开头") return } } jump = true } if (jump) { if (href.toString().indexOf(`/all/video/av${aid}/`) === -1) { href = new URL(href.origin + `/all/video/av${aid}/`) } href.searchParams.set("get_info", '1') window.location.href = href.toString() } let url = `https://delflare505.win/getVideoInfo?aid=` + aid let response = await xhrGet(url) let body = JSON.parse(await response) console.log(body) let videoInfo = transform(body.data) unsafeWindow.videoInfo = videoInfo if (unsafeWindow.cloudmoe) { let cacheInfo = JSON.parse(JSON.stringify(videoInfo)) cacheInfo.isDetailed = true cacheInfo.keywords = "" cacheInfo = { code: 0, data: { id: cacheInfo.id, info: cacheInfo, parts: cacheInfo.list, }, } unsafeWindow.cloudmoe(cacheInfo) } else { unsafeWindow.view(videoInfo) } } function client() { let registeredTimestamp = new Date().getTime() console.log('biliplus script running') function getPageAid() { let aid if (/\/BV/.exec(window.location.href)) { aid = bv2av(/BV[a-zA-Z0-9]+/.exec(window.location.href)[0]) } else { aid = /av(\d+)/.exec(window.location.href)[1] } return Number(aid) } let createElement = function (sHtml) { // 创建一个可复用的包装元素 let recycled = document.createElement('div'), // 创建标签简易匹配 reg = /^<([a-zA-Z]+)(?=\s|\/>|>)[\s\S]*>$/, // 某些元素HTML标签必须插入特定的父标签内,才能产生合法元素 // 另规避:ie7-某些元素innerHTML只读 // 创建这些需要包装的父标签hash hash = { 'colgroup': 'table', 'col': 'colgroup', 'thead': 'table', 'tfoot': 'table', 'tbody': 'table', 'tr': 'tbody', 'th': 'tr', 'td': 'tr', 'optgroup': 'select', 'option': 'optgroup', 'legend': 'fieldset' }; // 闭包重载方法(预定义变量避免重复创建,调用执行更快,成员私有化) createElement = function (sHtml) { // 若不包含标签,调用内置方法创建并返回元素 if (!reg.test(sHtml)) { return document.createElement(sHtml); } // hash中是否包含匹配的标签名 let tagName = hash[RegExp.$1.toLowerCase()]; // 若无,向包装元素innerHTML,创建/截取并返回元素 if (!tagName) { recycled.innerHTML = sHtml; return recycled.removeChild(recycled.firstChild); } // 若匹配hash标签,迭代包装父标签,并保存迭代层次 let deep = 0, element = recycled; do { sHtml = '<' + tagName + '>' + sHtml + '' + tagName + '>'; deep++; } while (tagName = hash[tagName]); element.innerHTML = sHtml; // 根据迭代层次截取被包装的子元素 do { element = element.removeChild(element.firstChild); } while (--deep > -1); // 最终返回需要创建的元素 return element; } // 执行方法并返回结果 return createElement(sHtml); } async function parseVideoInfo(aid) { let videoInfo if (unsafeWindow.videoInfo && window.location.href.includes('/video/')) { return unsafeWindow.videoInfo } if (aid === undefined) { aid = getPageAid() } try { let videoPage = await xhrGet('https://www.biliplus.com/video/av' + aid + '/') videoInfo = JSON.parse(xmlunEscape(/({"id":.*?})\);/.exec(videoPage)[1])) videoInfo['aid'] = videoInfo['id'] if (!videoInfo.list || videoInfo.list.length === 0) { throw ["No part found in videoInfo Normal, try cidHistory", JSON.stringify(videoInfo, undefined, "\t")] } } catch (e) { console.log(e) let videoPage = await xhrGet('https://www.biliplus.com/all/video/av' + aid + '/') let url = /(\/api\/view_all\?.*?)'/.exec(videoPage)[1] url = 'https://www.biliplus.com' + url let data = JSON.parse(xmlunEscape(await xhrGet(url)))['data'] videoInfo = data['info'] videoInfo['list'] = data['parts'] } if (videoInfo.created) { videoInfo.videoPublishDate = videoInfo.created } console.log(videoInfo) if (window.location.href.includes('/video/')) { unsafeWindow.videoInfo = videoInfo } return videoInfo } async function parseEpisodesInfo(aid) { if (aid === undefined) { aid = getPageAid() } let response = await xhrGet('https://api.bilibili.com/x/web-interface/wbi/view?aid=' + aid) let videoInfo = JSON.parse(response).data let i = 0 videoInfo.list = [] let episodes try { episodes = videoInfo.ugc_season.sections[0].episodes } catch (e) { alert("未找到合集信息") return } for (let episode of episodes) { i += 1 let partInfo = { page: i, title: episode.arc.title, part: episode.arc.title + "_时长" + episode.arc.duration + "秒", duration: episode.arc.duration, cid: episode.cid } videoInfo.list.push(partInfo) } console.log(videoInfo) return videoInfo } (function searchFix() { if (window.location.href.indexOf('api/do.php') === -1) return function searchOption() { let searchField = document.querySelector("#searchField > fieldset") let searchDiv = document.querySelector("#searchField > fieldset > div:nth-child(1)") searchDiv.insertAdjacentHTML('afterend', ` `) let aliveSection = searchField.querySelector("select[id='alive-section']") let deadSection = searchField.querySelector("select[id='dead-section']") let poster = searchField.querySelector("select[id='poster']") let searchInput = document.querySelector("#searchField > fieldset > div:nth-child(1) > input[type=search]") function setSection(section) { let content = searchInput.value if (section !== "") { section = ' @' + section } if (/ @\S+/.exec(content)) { content = content.replace(/ @\S+/, section) } else content += section searchInput.value = content } function setPoster(uid) { if (uid !== "") { uid = ' @m=' + uid } let content = searchInput.value if (/ @m=\d+/.exec(content)) { content = content.replace(/ @m=\d+/, uid) } else content += uid searchInput.value = content } aliveSection.addEventListener('change', (event) => { if (deadSection.value !== "") { deadSection.value = "" } setSection(event.target.value) }) deadSection.addEventListener('change', (event) => { if (aliveSection.value !== "") { aliveSection.value = "" } setSection(event.target.value) }) poster.addEventListener('change', (event) => { setPoster(event.target.value) }) } let getjson = unsafeWindow.parent.getjsonReal = unsafeWindow.parent.getjson let aidList = [] let irrelevantArchive = [] let allArchive = [] async function joinCallback(url, callback, n) { console.log("joinCallback") if (url[0] === '/') url = 'https:' + url let word = /word=(.*?)(&|$)/.exec(url)[1] let wordList = [] for (let keyword of decodeURIComponent(word).replace(/([^ ])@/, '$1 @').split(' ')) { if (keyword[0] !== '@') { wordList.push(keyword) } } let pn = /p=(\d+)/.exec(url) pn = pn ? pn[1] : '1' if (pn === '1') { aidList = [] irrelevantArchive = [] allArchive = [] } let request = xhrGet(url) let aidSearchUrl = '/api/search?word=' + word + '&page=' + pn let aid_request = xhrGet(aidSearchUrl) let searchResult = JSON.parse(await request) let archive = [] searchResult['data']['items']['archive'].forEach(function (item) { if (item.goto === 'av') { if (aidList.indexOf(item.param) === -1) { aidList.push(item.param) let isRelevant = false for (let keyword of wordList) { for (let key of ['title', 'desc']) { if (item[key].indexOf(keyword) !== -1) { isRelevant = true } } } if (isRelevant) { archive.push(item) } else { irrelevantArchive.push(item) } allArchive.push(item) } } else { archive.push(item) } }) try { let aidSearchResult = JSON.parse((await aid_request))['result'] aidSearchResult.forEach(function (video) { if (aidList.indexOf(video.aid) === -1) { let item = { author: video.author, cover: video.pic, created: new Date(video.created.replace(/-/g, '/')).getTime() / 1000, review: video.review, desc: video.description, goto: "av", param: video.aid, play: video.play, title: video.title, } let isRelevant = false for (let keyword of wordList) { for (let key of ['title', 'desc']) { if (item[key].toLowerCase().indexOf(keyword.toLowerCase()) !== -1) { isRelevant = true } } } if (isRelevant) { archive.push(item) } else { irrelevantArchive.push(item) } allArchive.push(item) } }) } catch (e) { console.log(e) } if (archive.length === 0) { archive = irrelevantArchive irrelevantArchive = [] } searchResult['data']['items']['archive'] = archive callback(searchResult, n) return } unsafeWindow.getjson = unsafeWindow.parent.getjson = function (url, callback, n) { console.log("getjson", arguments) if (url.indexOf("search_api") !== -1 && url.indexOf("source=biliplus") !== -1) { try { return joinCallback(url, callback, n) } catch (e) { console.log(e) return getjson(url, callback, n) } } else return getjson(url, callback, n) }; broadcastChannel.addEventListener('message', function (event) { console.log(event.data) if (event.data.type === 'aidComplete') { let elem = document.querySelector('[id="av' + event.data.aid + '"]') if (elem) elem.textContent = '下载完成' } if (event.data.type === 'aidDownloaded') { let elem = document.querySelector('[id="av' + event.data.aid + '"]') if (elem) elem.textContent = '已下载' } if (event.data.type === 'aidStart') { let elem = document.querySelector('[id="av' + event.data.aid + '"]') if (elem) elem.textContent = '开始下载' } if (event.data.type === 'cidComplete') { let elem = document.querySelector('[id="av' + event.data.aid + '"]') if (elem) elem.textContent = event.data.progress + "%" } }) function updatePointer(pointerElem) { if (pointerElem.querySelector("#videoDetail")) { return } let aidElem = pointerElem .querySelector('div[class="video-card-desc"]') if (!aidElem) { return } let link = pointerElem.getAttribute("data-link") if (link) { console.log(link) pointerElem.removeAttribute("data-link") pointerElem.addEventListener('click', async function (event) { console.log(event) event.preventDefault() if (event.target.className !== 'download') { unsafeWindow.openOrigin(link) } }) } let aid = parseInt(aidElem.textContent.slice(2)) if (!aidElem.querySelector('[class="download"]')) { let downloadButton = createElement('