// ==UserScript== // @name bilibili - b站 - 未登录账号可以使用最高画质 // @description 未登录账号可以使用最高画质,原作者DD1969,https://greasyfork.org/zh-CN/users/675901-dd1969 // @namespace https://bilibili.com/ // @version 0.6.1 // @license GPL-3.0 // @author DD1969 // @match https://www.bilibili.com/video/* // @icon https://www.bilibili.com/favicon.ico // @require https://lib.baomitu.com/viewerjs/1.11.4/viewer.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js // @run-at document-start // @downloadURL none // ==/UserScript== ;(async () => { 'use strict' if (document.cookie.includes('DedeUserID')) { return } const oldHref = unsafeWindow.location.href const urlChangeTimer = setInterval(() => { const newHref = unsafeWindow.location.href if (newHref === oldHref) { return } const re = /https:\/\/www\.bilibili\.com\/video\/.*/ if (re.test(newHref) || re.test(oldHref)) { clearInterval(urlChangeTimer) unsafeWindow.location.reload() } }, 200) const originSetTimeout = unsafeWindow.setTimeout unsafeWindow.setTimeout = (func, delay) => { if (delay === 3e4) { delay = 3e8 } return originSetTimeout(func, delay) } const profileString = unsafeWindow.localStorage.getItem('bpx_player_profile') if (profileString) { const profile = JSON.parse(profileString) profile.lastView = Date.now() - 864e5 unsafeWindow.localStorage.setItem('bpx_player_profile', JSON.stringify(profile)) } else { let playerProfile = { lastView: Date.now() - 864e5, lastUid: 0, media: { quality: 0, volume: 1, nonzeroVol: 1, hideBlackGap: true, dolbyAudio: true, audioQuality: null, autoplay: false, handoff: 0, seniorTip: true, }, dmSend: { upDm: false, dmChecked: false, }, blockList: [], dmSetting: { status: true, dmSwitch: true, aiSwitch: false, aiLevel: 3, preventshade: false, dmask: true, typeTop: true, typeScroll: true, typeBottom: true, typeColor: true, typeSpecial: true, opacity: 0.8, dmarea: 50, speedplus: 1, fontsize: 0.8, fullscreensync: true, speedSync: false, fontfamily: "SimHei, 'Microsoft JhengHei'", bold: true, fontborder: 0, seniorModeSwitch: 0, }, basEditorData: {}, audioEffect: null, boceTimes: [], interaction: { rookieGuide: null, showedDialog: false, }, iswide: null, widesave: null, subtitle: { fade: false, scale: true, fontsize: 1, opacity: 0.4, bilingual: false, color: '16777215', shadow: '0', position: 'bottom-center', }, progress: { precisionGuide: null, pbpstate: true, pinstate: false, }, panorama: true, ksInfo: { ts: 0, kss: null, }, } unsafeWindow.localStorage.setItem('bpx_player_profile', JSON.stringify(playerProfile, null, 4)) } const trialButtonTimer = setInterval(() => { const trialBtn = document.querySelector('.bpx-player-toast-confirm-login') if (trialBtn) { trialBtn.click() clearInterval(trialButtonTimer) } }, 200) const originQuerySelector = document.querySelector document.querySelector = function (selector) { return selector === '.bili-mini-mask' ? true : originQuerySelector.call(this, selector) } const originAppendChild = HTMLElement.prototype.appendChild HTMLElement.prototype.appendChild = function (childElement) { return childElement && childElement.classList && childElement.classList.contains('bili-mini-mask') && childElement.innerHTML.includes('bili-mini-login') ? null : originAppendChild.call(this, childElement) } const enableReplyPagination = false const enableLoadAllSubRepliesAtOnce = false const { oid, createrID, rootReplyHash, subReplyHash, replyList } = await new Promise(resolve => { const timer = setInterval(() => { const oid = unsafeWindow?.__INITIAL_STATE__?.aid const createrID = unsafeWindow?.__INITIAL_STATE__?.upData?.mid const rootReplyHashMatchResult = document.head.innerHTML.match( /\.reply-item\[(?data-v-[a-z0-9]{8})\]/, ) const subReplyHashMatchResult = document.head.innerHTML.match( /\.sub-reply-item\[(?data-v-[a-z0-9]{8})\]/, ) const replyList = document.querySelector('.reply-list') if (oid && createrID && rootReplyHashMatchResult && subReplyHashMatchResult && replyList) { clearInterval(timer) resolve({ oid, createrID: parseInt(createrID), rootReplyHash: rootReplyHashMatchResult.groups.rootReplyHash, subReplyHash: subReplyHashMatchResult.groups.subReplyHash, replyList, }) } }, 200) }) const sortTypeConstant = { LATEST: 0, HOT: 2 } let currentSortType = 1 await enableSwitchingSortType() let replyPool = {} await addStyle() await loadFirstPagination() async function loadFirstPagination() { const { data: firstPaginationData, code: resultCode } = await getPaginationData(1) replyList.innerHTML = '' // clear replyPool replyPool = {} // script ends here if not able to fetch pagination data if (resultCode !== 0) { replyList.innerHTML = '

无法从API获取评论数据

' return } // load the top reply if it exists if (firstPaginationData.top_replies && firstPaginationData.top_replies.length !== 0) { const topReplyData = firstPaginationData.top_replies[0] appendReplyItem(topReplyData, true) } // script ends here if there is no reply of this video if (firstPaginationData.replies.length === 0) { return } // load normal replies for (const replyData of firstPaginationData.replies) { appendReplyItem(replyData) } // add page loader enableReplyPagination ? addReplyPageSwitcher() : addAnchor() } // get reply data according to pagination number async function getPaginationData(paginationNumber) { return await fetch( `https://api.bilibili.com/x/v2/reply?oid=${oid}&type=1&sort=${currentSortType}&pn=${paginationNumber}`, ) .then(res => res.json()) .then(json => { console.log(json.data) return { data: json.data, code: json.code, } }) } function appendReplyItem(replyData, isTopReply) { if (!enableReplyPagination && replyPool[replyData.rpid_str]) { return } const replyItemElement = document.createElement('div') replyItemElement.classList.add('reply-item') replyItemElement.innerHTML = `
${ replyData.member.pendant.image ? `
` : '' }
${ replyData.member.user_sailing?.cardbg ? `
NO.
${replyData.member.user_sailing.cardbg.fan.number .toString() .padStart(6, '0')}
` : '' }
${isTopReply ? '置顶' : ''}${ replyData.content.pictures ? `
笔记
` : '' }${getConvertedMessage(replyData.content)}
${ replyData.content.pictures ? `
${getImageItems(replyData.content.pictures)}
` : '' }
${getFormattedTime( replyData.ctime, )} ${replyData.like} 回复
${ replyData.card_label ? replyData.card_label.reduce( (acc, cur) => acc + `${cur.text_content}`, '', ) : '' }
${getSubReplyItems(replyData.replies) || ''} ${ replyData.rcount > 3 ? `
共${replyData.rcount}条回复, 点击查看
` : '' }
` replyList.appendChild(replyItemElement) if (!enableReplyPagination) { replyPool[replyData.rpid_str] = true } const previewImageContainer = replyItemElement.querySelector('.preview-image-container') if (previewImageContainer) { new Viewer(previewImageContainer, { title: false, toolbar: false, tooltip: false, keyboard: false }) } const subReplyList = replyItemElement.querySelector('.sub-reply-list') const viewMoreBtn = replyItemElement.querySelector('.view-more-btn') viewMoreBtn && viewMoreBtn.addEventListener('click', () => { enableLoadAllSubRepliesAtOnce ? loadAllSubReplies(replyData.rpid, subReplyList) : loadPaginatedSubReplies(replyData.rpid, subReplyList, replyData.rcount, 1) }) } function getFormattedTime(ms) { const time = new Date(ms * 1000) const year = time.getFullYear() const month = (time.getMonth() + 1).toString().padStart(2, '0') const day = time.getDate().toString().padStart(2, '0') const hour = time.getHours().toString().padStart(2, '0') const minute = time.getMinutes().toString().padStart(2, '0') return `${year}-${month}-${day} ${hour}:${minute}` } function getMemberLevelColor(level) { return { 1: '#BBBBBB', 2: '#8BD29B', 3: '#7BCDEF', 4: '#FEBB8B', 5: '#EE672A', 6: '#F04C49', }[level] } function getConvertedMessage(content) { let result = content.message if (content.emote) { for (const [key, value] of Object.entries(content.emote)) { const imageElementHTML = `${key}` result = result.replaceAll(key, imageElementHTML) } } result = result.replaceAll(/\d{1,2}(:|:)\d{1,2}/g, timestamp => { const [minute, second] = timestamp.replace(':', ':').split(':') const totalSecond = parseInt(minute) * 60 + parseInt(second) if (Number.isNaN(totalSecond)) { return timestamp } return `${timestamp.replace(':', ':')}` }) if (Object.keys(content.jump_url).length) { for (const [key, value] of Object.entries(content.jump_url)) { const href = key.startsWith('BV') ? `https://www.bilibili.com/video/${key}` : value.pc_url || key const linkElementHTML = `${value.title}` result = result.replaceAll(key, linkElementHTML) } } if (content.at_name_to_mid) { for (const [key, value] of Object.entries(content.at_name_to_mid)) { const linkElementHTML = `@${key}` result = result.replaceAll(`@${key}`, linkElementHTML) } } return result } function getImageItems(images) { images = images.slice(0, 3) const imageSizeConfig = { 1: 'max-width: 280px; max-height: 180px;', 2: 'width: 128px; height: 128px;', 3: 'width: 96px; height: 96px;', }[images.length] let result = '' for (const image of images) { result += `
` } return result } function getSubReplyItems(subReplies) { if (!subReplies || subReplies.length === 0) { return } let result = '' for (const replyData of subReplies) { result += `
${getConvertedMessage(replyData.content)}
${getFormattedTime( replyData.ctime, )} ${replyData.like} 回复
` } return result } async function loadAllSubReplies(rootReplyID, subReplyList) { let subPaginationCounter = 1 while (true) { const subReplyData = await fetch( `https://api.bilibili.com/x/v2/reply/reply?oid=${oid}&pn=${subPaginationCounter++}&ps=20&root=${rootReplyID}&type=1`, ) .then(res => res.json()) .then(json => json.data) if (subPaginationCounter - 1 === 1) { subReplyList.innerHTML = '' } if (subReplyData.replies) { subReplyList.innerHTML += getSubReplyItems(subReplyData.replies) } else { break } } } async function loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, paginationNumber) { // replace reply list with new replies const subReplyData = await fetch( `https://api.bilibili.com/x/v2/reply/reply?oid=${oid}&pn=${paginationNumber}&ps=10&root=${rootReplyID}&type=1`, ) .then(res => res.json()) .then(json => json.data) if (subReplyData.replies) { subReplyList.innerHTML = getSubReplyItems(subReplyData.replies) } // add page switcher addSubReplyPageSwitcher(rootReplyID, subReplyList, subReplyAmount, paginationNumber) // scroll to the top of replyItem let elemTop = subReplyList.parentElement.parentElement.offsetTop let parentElem = subReplyList.parentElement.parentElement.offsetParent while (parentElem) { elemTop += parentElem.offsetTop parentElem = parentElem.offsetParent } unsafeWindow.scrollTo(0, elemTop - 60) } function addSubReplyPageSwitcher(rootReplyID, subReplyList, subReplyAmount, currentPageNumber) { if (subReplyAmount <= 10) { return } const pageAmount = Math.ceil(subReplyAmount / 10) const pageSwitcher = document.createElement('div') pageSwitcher.classList.add('view-more') pageSwitcher.innerHTML = `
共${pageAmount}页 ${currentPageNumber !== 1 ? '上一页' : ''} ${(() => { // 4 on the left, 4 on the right, then merge const left = [ currentPageNumber - 4, currentPageNumber - 3, currentPageNumber - 2, currentPageNumber - 1, ].filter(num => num >= 1) const right = [ currentPageNumber + 1, currentPageNumber + 2, currentPageNumber + 3, currentPageNumber + 4, ].filter(num => num <= pageAmount) const merge = [].concat(left, currentPageNumber, right) // chosen 5(if able) let chosen if (currentPageNumber <= 3) { chosen = merge.slice(0, 5) } else if (currentPageNumber >= pageAmount - 3) { chosen = merge.reverse().slice(0, 5).reverse() } else { chosen = merge.slice(merge.indexOf(currentPageNumber) - 2, merge.indexOf(currentPageNumber) + 3) } // add first and dots let final = JSON.parse(JSON.stringify(chosen)) if (!final.includes(1)) { let front = [1] if (final.at(0) !== 2) { front = [1, '...'] } final = [].concat(front, final) } // add last and dots if (!final.includes(pageAmount)) { let back = [pageAmount] if (final.at(-1) !== pageAmount - 1) { back = ['...', pageAmount] } final = [].concat(final, back) } // assemble to html return final.reduce((acc, cur) => { if (cur === '...') { return acc + '...' } if (cur === currentPageNumber) { return acc + `${cur}` } return acc + `${cur}` }, '') })()} ${currentPageNumber !== pageAmount ? '下一页' : ''}
` pageSwitcher .querySelector('.pagination-to-prev-btn') ?.addEventListener('click', () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, currentPageNumber - 1), ) pageSwitcher .querySelector('.pagination-to-next-btn') ?.addEventListener('click', () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, currentPageNumber + 1), ) pageSwitcher.querySelectorAll('.pagination-page-number:not(.current-page)')?.forEach(pageNumberElement => { const number = parseInt(pageNumberElement.textContent) pageNumberElement.addEventListener('click', () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, number), ) }) subReplyList.appendChild(pageSwitcher) } async function addReplyPageSwitcher() { const oldPageSwitcher = document.querySelector('#comment .reply-warp .page-switcher') oldPageSwitcher && oldPageSwitcher.remove() let isPageAmountFound = false let currentMaxPageNumber = 1 let currentPageNumber = 1 const { data: nextPaginationData } = await getPaginationData(currentPageNumber + 1) if (!nextPaginationData.replies || nextPaginationData.replies.length === 0) { return } const pageSwitcher = document.createElement('div') pageSwitcher.classList.add('page-switcher') pageSwitcher.style = ` width: 100%; display: flex; justify-content: center; transform: translateY(-60px); ` pageSwitcher.appendChild(generatePageSwitcher()) document.querySelector('#comment .reply-warp').appendChild(pageSwitcher) function generatePageSwitcher() { const wrapper = document.createElement('div') wrapper.classList.add('page-switcher-wrapper') wrapper.innerHTML = ` ${ currentPageNumber === 1 ? '上一页' : '上一页' } ${(() => { // 4 on the left, 4 on the right, then merge const left = [ currentPageNumber - 4, currentPageNumber - 3, currentPageNumber - 2, currentPageNumber - 1, ].filter(num => num >= 1) const right = [ currentPageNumber + 1, currentPageNumber + 2, currentPageNumber + 3, currentPageNumber + 4, ].filter(num => num <= currentMaxPageNumber) const merge = [].concat(left, currentPageNumber, right) // chosen 5(if able) let chosen if (currentPageNumber <= 3) { chosen = merge.slice(0, 5) } else if (currentPageNumber >= currentMaxPageNumber - 3) { chosen = merge.reverse().slice(0, 5).reverse() } else { chosen = merge.slice(merge.indexOf(currentPageNumber) - 2, merge.indexOf(currentPageNumber) + 3) } // add first and dots let final = JSON.parse(JSON.stringify(chosen)) if (!final.includes(1)) { let front = [1] if (final.at(0) !== 2) { front = [1, 'dot'] } final = [].concat(front, final) } // add last and dots if (!final.includes(currentMaxPageNumber)) { let back = [currentMaxPageNumber] if (final.at(-1) !== currentMaxPageNumber - 1) { back = ['dot', currentMaxPageNumber] } final = [].concat(final, back) } // assemble to html return final.reduce((acc, cur) => { if (cur === 'dot') { return acc + '•••' } if (cur === currentPageNumber) { return acc + `${cur}` } return acc + `${cur}` }, '') })()} ${ isPageAmountFound && currentPageNumber === currentMaxPageNumber ? '下一页' : '下一页' } ` wrapper.querySelector('.page-switcher-prev-btn')?.addEventListener('click', async () => { currentPageNumber -= 1 const { data: prevPaginationData } = await getPaginationData(currentPageNumber) replyList.innerHTML = '' // if loading page 1, load top reply if it exists if ( currentPageNumber === 1 && prevPaginationData.top_replies && prevPaginationData.top_replies.length !== 0 ) { const topReplyData = prevPaginationData.top_replies[0] appendReplyItem(topReplyData, true) } for (const replyData of prevPaginationData.replies) { appendReplyItem(replyData) } pageSwitcher.innerHTML = '' pageSwitcher.appendChild(generatePageSwitcher()) scrollToTopOfReplyList() }) // next button click event wrapper .querySelector('.page-switcher-next-btn') ?.addEventListener('click', async function nextButtonOnClickHandler(e) { if (currentPageNumber === currentMaxPageNumber && isPageAmountFound) { return } const { data: nextPaginationData } = await getPaginationData(currentPageNumber + 1) if (!nextPaginationData.replies || nextPaginationData.replies.length === 0) { isPageAmountFound = true e.target.classList.add('page-switcher-next-btn__disabled') e.target.classList.remove('page-switcher-next-btn') return } if (currentPageNumber === currentMaxPageNumber) { currentMaxPageNumber += 1 } currentPageNumber += 1 replyList.innerHTML = '' for (const replyData of nextPaginationData.replies) { appendReplyItem(replyData) } pageSwitcher.innerHTML = '' pageSwitcher.appendChild(generatePageSwitcher()) scrollToTopOfReplyList() }) // number button click event wrapper .querySelectorAll('.page-switcher-number:not(.page-switcher-current-page)') ?.forEach(numberElement => { numberElement.addEventListener('click', async () => { const targetPageNumber = parseInt(numberElement.textContent) currentPageNumber = targetPageNumber const { data: paginationData } = await getPaginationData(targetPageNumber) replyList.innerHTML = '' // if loading page 1, load top reply if it exists if ( targetPageNumber === 1 && paginationData.top_replies && paginationData.top_replies.length !== 0 ) { const topReplyData = paginationData.top_replies[0] appendReplyItem(topReplyData, true) } for (const replyData of paginationData.replies) { appendReplyItem(replyData) } pageSwitcher.innerHTML = '' pageSwitcher.appendChild(generatePageSwitcher()) scrollToTopOfReplyList() }) }) return wrapper } // scroll to the top of reply list function scrollToTopOfReplyList() { let elemTop = replyList.offsetTop let parentElem = replyList.offsetParent while (parentElem) { elemTop += parentElem.offsetTop parentElem = parentElem.offsetParent } unsafeWindow.scrollTo(0, elemTop - 196) } } function addAnchor() { // clear old anchor const oldAnchor = document.querySelector('#comment .reply-warp .anchor-for-loading') oldAnchor && oldAnchor.remove() const anchorElement = document.createElement('div') anchorElement.classList.add('anchor-for-loading') anchorElement.textContent = '正在加载...' anchorElement.style = ` width: calc(100% - 22px); height: 40px; margin-left: 22px; display: flex; justify-content: center; align-items: center; transform: translateY(-60px); color: #61666d; ` document.querySelector('#comment .reply-warp').appendChild(anchorElement) let paginationCounter = 1 const ob = new IntersectionObserver(async entries => { if (!entries[0].isIntersecting) { return } const { data: newPaginationData } = await getPaginationData(++paginationCounter) if (!newPaginationData.replies || newPaginationData.replies.length === 0) { anchorElement.textContent = '所有评论已加载完毕' ob.disconnect() return } for (const replyData of newPaginationData.replies) { appendReplyItem(replyData) } }) ob.observe(anchorElement) } async function enableSwitchingSortType() { const { hotSortElement, timeSortElement } = await new Promise(resolve => { const timer = setInterval(() => { const hotSortElement = document.querySelector('#comment .reply-header .hot-sort') const timeSortElement = document.querySelector('#comment .reply-header .time-sort') if (hotSortElement && timeSortElement) { clearInterval(timer) resolve({ hotSortElement, timeSortElement }) } }, 200) }) hotSortElement.addEventListener('click', async () => { if (currentSortType === sortTypeConstant.HOT) { return } currentSortType = sortTypeConstant.HOT hotSortElement.style.color = '#18191C' timeSortElement.style.color = '#9499A0' await loadFirstPagination() }) timeSortElement.addEventListener('click', async () => { if (currentSortType === sortTypeConstant.LATEST) { return } currentSortType = sortTypeConstant.LATEST hotSortElement.style.color = '#9499A0' timeSortElement.style.color = '#18191C' await loadFirstPagination() }) } async function addStyle() { const avatarCSS = document.createElement('style') avatarCSS.textContent = ` .reply-item .root-reply-avatar .avatar .bili-avatar { width: 48px; height: 48px; } .sub-reply-item .sub-reply-avatar .avatar .bili-avatar { width: 30px; height: 30px; } @media screen and (max-width: 1620px) { .reply-item .root-reply-avatar .avatar .bili-avatar { width: 40px; height: 40px; } .sub-reply-item .sub-reply-avatar .avatar .bili-avatar { width: 24px; height: 24px; } } ` document.head.appendChild(avatarCSS) const viewMoreCSS = document.createElement('style') viewMoreCSS.textContent = ` .sub-reply-container .view-more-btn:hover { color: #00AEEC; } .view-more { padding-left: 8px; color: #222; font-size: 13px; user-select: none; } .pagination-page-count { margin-right: 10px; } .pagination-page-dot, .pagination-page-number { margin: 0 4px; } .pagination-btn, .pagination-page-number { cursor: pointer; } .current-page, .pagination-btn:hover, .pagination-page-number:hover { color: #00AEEC; } ` document.head.appendChild(viewMoreCSS) const pageSwitcherCSS = document.createElement('style') pageSwitcherCSS.textContent = ` .page-switcher-wrapper { display: flex; font-size: 14px; color: #666; user-select: none; } .page-switcher-wrapper span { margin-right: 6px; } .page-switcher-wrapper span:not(.page-switcher-dot){ display: flex; padding: 0 14px; height: 38px; align-items: center; border: 1px solid #D7DDE4; border-radius: 4px; cursor: pointer; transition: border-color 0.2s; } .page-switcher-prev-btn:hover, .page-switcher-next-btn:hover, .page-switcher-number:hover { border-color: #00A1D6 !important; } .page-switcher-current-page { color: white; background-color: #00A1D6; border-color: #00A1D6 !important; } .page-switcher-dot { padding: 0 5px; display: flex; align-items: center; color: #CCC; } .page-switcher-prev-btn__disabled, .page-switcher-next-btn__disabled { color: #D7DDE4 !important; cursor: not-allowed !important; } ` document.head.appendChild(pageSwitcherCSS) const viewerjsCSS = document.createElement('style') viewerjsCSS.textContent = await fetch('https://lib.baomitu.com/viewerjs/1.11.4/viewer.min.css').then(res => res.text(), ) document.head.appendChild(viewerjsCSS) } await new Promise(resolve => { const timer = setInterval(() => { if (unsafeWindow.player && unsafeWindow.player.getMediaInfo) { clearInterval(timer) resolve() } }, 1000) }) const originFunc = unsafeWindow.player.getMediaInfo unsafeWindow.player.getMediaInfo = () => { const { absolutePlayTime, relativePlayTime, playUrl } = originFunc() return { absolutePlayTime: 0, relativePlayTime, playUrl } } })()