// ==UserScript== // @name 哔哩哔哩(bilibili.com)播放页调整 // @license GPL-3.0 License // @namespace https://greasyfork.org/zh-CN/scripts/415804-bilibili%E6%92%AD%E6%94%BE%E9%A1%B5%E8%B0%83%E6%95%B4-%E8%87%AA%E7%94%A8 // @version 0.39 // @description 1.自动定位到播放器(进入播放页,可自动定位到播放器,可设置偏移量及是否在点击主播放器时定位);2.可设置是否自动选择最高画质;3.可设置播放器默认模式; // @author QIAN // @match *://*.bilibili.com/video/* // @match *://*.bilibili.com/bangumi/play/* // @match *://*.bilibili.com/list/* // @run-at document-start // @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js // @require https://unpkg.com/sweetalert2@11.7.2/dist/sweetalert2.min.js // @resource swalStyle https://unpkg.com/sweetalert2@11.7.2/dist/sweetalert2.min.css // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_getResourceText // @grant GM.info // @supportURL https://github.com/QIUZAIYOU/Bilibili-VideoPage-Adjustment // @homepageURL https://github.com/QIUZAIYOU/Bilibili-VideoPage-Adjustment // @icon https://www.bilibili.com/favicon.ico?v=1 // @downloadURL https://update.greasyfork.cloud/scripts/415804/%E5%93%94%E5%93%A9%E5%93%94%E5%93%A9%EF%BC%88bilibilicom%EF%BC%89%E6%92%AD%E6%94%BE%E9%A1%B5%E8%B0%83%E6%95%B4.user.js // @updateURL https://update.greasyfork.cloud/scripts/415804/%E5%93%94%E5%93%A9%E5%93%94%E5%93%A9%EF%BC%88bilibilicom%EF%BC%89%E6%92%AD%E6%94%BE%E9%A1%B5%E8%B0%83%E6%95%B4.meta.js // ==/UserScript== $(() => { 'use strict' let { currentUrl, theMainFunctionRunningTimes, thePrepFunctionRunningTimes, autoSelectScreenModeTimes, autoCancelMuteTimes, webfullUnlockTimes, insertGoToCommentsButtonTimes, autoSelectVideoHighestQualityTimes, functionExecutionsTimes = 0, } = { currentUrl: window.location.href, theMainFunctionRunningTimes: 0, thePrepFunctionRunningTimes: 0, autoSelectScreenModeTimes: 0, autoCancelMuteTimes: 0, webfullUnlockTimes: 0, insertGoToCommentsButtonTimes: 0, autoSelectVideoHighestQualityTimes: 0, } const { getValue, setValue, sleep, addStyle, historyListener, checkBrowserHistory, throttle, getClientHeight, checkElementExistence, isDocumentHidden, isLogin, logger, checkPageReadyState, pageReload, scrollToPlayer } = { getValue (name) { return GM_getValue(name) }, setValue (name, value) { GM_setValue(name, value) }, sleep (time) { return new Promise(resolve => setTimeout(resolve, time)) }, addStyle (id, tag, css) { tag = tag || 'style' const doc = document const styleDom = doc.getElementById(id) if (styleDom) return const style = doc.createElement(tag) style.rel = 'stylesheet' style.id = id tag === 'style' ? (style.innerHTML = css) : (style.href = css) document.head.appendChild(style) }, historyListener () { class Dep { constructor(name) { this.id = new Date() this.subs = [] } defined () { Dep.watch.add(this) } notify () { this.subs.forEach((e, i) => { if (typeof e.update === 'function') { try { e.update.apply(e) } catch (err) { console.warr(err) } } }) } } Dep.watch = null class Watch { constructor(name, fn) { this.name = name this.id = new Date() this.callBack = fn } add (dep) { dep.subs.push(this) } update () { var cb = this.callBack cb(this.name) } } var addHistoryMethod = (function () { var historyDep = new Dep() return function (name) { if (name === 'historychange') { return function (name, fn) { var event = new Watch(name, fn) Dep.watch = event historyDep.defined() Dep.watch = null } } else if (name === 'pushState' || name === 'replaceState') { var method = history[name] return function () { method.apply(history, arguments) historyDep.notify() // logger.info("访问历史|变化") } } } })() window.addHistoryListener = addHistoryMethod('historychange') history.pushState = addHistoryMethod('pushState') history.replaceState = addHistoryMethod('replaceState') window.addHistoryListener('history', function () { const throttleAutoLocation = throttle(m.autoLocation, 500) throttleAutoLocation() }) }, checkBrowserHistory () { window.addEventListener('popstate', () => { m.autoLocation() }) }, throttle (func, delay) { let wait = false return (...args) => { if (wait) { return } func(...args) wait = true setTimeout(() => { wait = false }, delay) } }, getClientHeight () { const bodyHeight = document.body.clientHeight || 0 const docHeight = document.documentElement.clientHeight || 0 return bodyHeight < docHeight ? bodyHeight : docHeight }, // 检查指定HTML元素是否存在 checkElementExistence (selector, maxAttempts, interval) { // functionExecutionsTimes += 1 // const funName = (new Error()).stack.split("\n")[2].trim().split(" ")[1].replace('Object.', '') // logger.debug(`(调用:${functionExecutionsTimes}) ${funName} -> ${selector}`) return new Promise(resolve => { let attempts = 0 const intervalId = setInterval(() => { attempts++ // logger.debug(`(尝试:${attempts}) -> ${selector}`) const element = $(selector) if (element.length) { clearInterval(intervalId) resolve(true) } else if (attempts === maxAttempts) { clearInterval(intervalId) resolve(false) } }, interval) }) }, isDocumentHidden () { const visibilityChangeEventNames = ['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange'] const documentHiddenPropertyName = visibilityChangeEventNames.find(name => name in document) || 'onfocusin' in document || 'onpageshow' in window ? 'hidden' : null if (documentHiddenPropertyName !== null) { const isHidden = () => document[documentHiddenPropertyName] const onChange = () => isHidden() // 添加各种事件监听器 visibilityChangeEventNames.forEach(eventName => document.addEventListener(eventName, onChange)) window.addEventListener('focus', onChange) window.addEventListener('blur', onChange) window.addEventListener('pageshow', onChange) window.addEventListener('pagehide', onChange) document.onfocusin = document.onfocusout = onChange return isHidden() } // 如果无法判断是否隐藏,则返回undefined return undefined }, isLogin () { return Boolean(document.cookie.replace(new RegExp(String.raw`(?:(?:^|.*;\s*)bili_jct\s*=\s*([^;]*).*$)|^.*$`), '$1') || null) }, logger: { info (content) { console.info('%c播放页调整', 'color:white;background:#006aff;padding:2px;border-radius:2px', content) }, warn (content) { console.warn('%c播放页调整', 'color:white;background:#ff6d00;padding:2px;border-radius:2px', content) }, error (content) { console.error('%c播放页调整', 'color:white;background:#f33;padding:2px;border-radius:2px', content) }, debug (content) { console.info('%c播放页调整(调试)', 'color:white;background:#cc00ff;padding:2px;border-radius:2px', content) }, }, checkPageReadyState (state) { return new Promise((resolve) => { const timer = setInterval(() => { if (document.readyState === state) { clearInterval(timer) resolve(true) } }, 100) }) }, pageReload () { if (auto_reload) location.reload(true) }, scrollToPlayer (offset) { $('html,body').scrollTop(offset) } } const { is_vip, player_type, offset_top, auto_locate, auto_locate_video, auto_locate_bangumi, click_player_auto_locate, player_offset_top, current_screen_mode, selected_screen_mode, auto_select_video_highest_quality, contain_quality_4k, contain_quality_8k, webfull_unlock, auto_reload } = { is_vip: getValue('is_vip'), player_type: getValue('player_type'), offset_top: Math.trunc(getValue('offset_top')), auto_locate: getValue('auto_locate'), auto_locate_video: getValue('auto_locate_video'), auto_locate_bangumi: getValue('auto_locate_bangumi'), click_player_auto_locate: getValue('click_player_auto_locate'), player_offset_top: Math.trunc(getValue('player_offset_top')), current_screen_mode: getValue('current_screen_mode'), selected_screen_mode: getValue('selected_screen_mode'), auto_select_video_highest_quality: getValue('auto_select_video_highest_quality'), contain_quality_4k: getValue('contain_quality_4k'), contain_quality_8k: getValue('contain_quality_8k'), webfull_unlock: getValue('webfull_unlock'), auto_reload: getValue('auto_reload') } const m = { // 初始化设置参数 initValue () { const value = [{ name: 'is_vip', value: false, }, { name: 'player_type', value: 'video', }, { name: 'offset_top', value: 7, }, { name: 'player_offset_top', value: 160, }, { name: 'auto_locate', value: true, }, { name: 'auto_locate_video', value: true, }, { name: 'auto_locate_bangumi', value: true, }, { name: 'click_player_auto_locate', value: true, }, { name: 'current_screen_mode', value: 'normal', }, { name: 'selected_screen_mode', value: 'wide', }, { name: 'auto_select_video_highest_quality', value: true, }, { name: 'contain_quality_4k', value: false, }, { name: 'contain_quality_8k', value: false, }, { name: 'webfull_unlock', value: false, }, { name: 'auto_reload', value: false, }] value.forEach(v => { if (getValue(v.name) === undefined) { setValue(v.name, v.value) } }) }, // 检查视频资源是否加载完毕并处于可播放状态 async checkVideoCanPlayThrough () { const BwpVideoPlayerExists = await checkElementExistence('bwp-video', 10, 10) // logger.debug(`bwp-video|${BwpVideoPlayerExists?'存在':'不存在'}`) if (BwpVideoPlayerExists) { return new Promise(resolve => { resolve(true) }) } const $video = $('#bilibili-player video') const videoReadyState = $video[0].readyState // logger.debug(`视频资源|${videoReadyState>=4?'可播放':'不可播放'}` if (videoReadyState >= 4) { return new Promise(resolve => { resolve(true) }) } else { return new Promise(resolve => { const checkTimeout = setTimeout(() => { // logger.error('视频资源|脚本检测失败|重载页面') pageReload() resolve(false) }, 7000) $video.on('canplaythrough', () => { // logger.info("视频资源加载|成功") let attempts = 100 const timer = setInterval(() => { const isHidden = $('#bilibili-player .bpx-player-container').attr('data-ctrl-hidden') if (isHidden === 'false') { clearInterval(timer) clearTimeout(checkTimeout) // logger.info(`视频可播放`) // logger.info(`控制条|出现(hidden:${isHidden})`) resolve(true) } else if (attempts <= 0) { clearInterval(timer) clearTimeout(checkTimeout) // logger.error("控制条|检查失败") resolve(false) } // logger.info("控制条|检查中") attempts-- }, 100) }) }) } }, // 获取当前视频类型(video/bangumi) getCurrentPlayerType () { const isVideo = currentUrl.includes('www.bilibili.com/video') || currentUrl.includes('www.bilibili.com/list/') const isBangumi = currentUrl.includes('www.bilibili.com/bangumi') setValue('player_type', isVideo ? 'video' : isBangumi && 'bangumi') }, // 获取当前屏幕模式(normal/wide/web/full) async getCurrentScreenMode () { const exists = await checkElementExistence('#bilibili-player .bpx-player-container', 10, 100) if (exists) { const screenMode = $('#bilibili-player .bpx-player-container').attr('data-screen') return Promise.resolve(screenMode) } else return Promise.resolve(false) }, // 监听屏幕模式变化(normal/wide/web/full) watchScreenModeChange () { const screenModObserver = new MutationObserver(mutations => { const playerDataScreen = $('#bilibili-player .bpx-player-container').attr('data-screen') setValue('current_screen_mode', playerDataScreen) }) screenModObserver.observe($('#bilibili-player .bpx-player-container')[0], { attributes: true, attributeFilter: ['data-screen'], }) }, // 判断自动切换屏幕模式是否切换成功 async checkScreenModeSuccess (expect_mode) { const current_screen_mode = await this.getCurrentScreenMode() const player_data_screen = $('#bilibili-player .bpx-player-container').attr('data-screen') const equal = new Set([ expect_mode, selected_screen_mode, current_screen_mode, player_data_screen, ]).size === 1 return Promise.resolve(equal) }, // 自动选择屏幕模式 async autoSelectScreenMode () { const current_screen_mode = await this.getCurrentScreenMode() if (current_screen_mode === 'wide') return { done: true, mode: selected_screen_mode } if (current_screen_mode === 'web') return { done: true, mode: selected_screen_mode } autoSelectScreenModeTimes++ if (autoSelectScreenModeTimes === 1) { const wideEnterBtn = document.querySelector('.bpx-player-ctrl-wide-enter') const webEnterBtn = document.querySelector('.bpx-player-ctrl-web-enter') const selectModeBtn = selected_screen_mode === 'wide' ? wideEnterBtn : webEnterBtn const expect_mode = selected_screen_mode === 'wide' ? 'wide' : 'web' let attempts = 50 selectModeBtn.click() const checkScreenMode = async (expect_mode) => { const success = await this.checkScreenModeSuccess(expect_mode) if (success) { clearInterval(checkScreenModeInterval) setValue('current_screen_mode', selected_screen_mode) return { done: true, mode: selected_screen_mode } } else { await sleep(1000) selectModeBtn.click() logger.warn('自动选择屏幕模式失败正在重试') attempts-- if (attempts === 0) { clearInterval(checkScreenModeInterval) pageReload() } } } let checkScreenModeInterval = setInterval(checkScreenMode, 100, expect_mode) return new Promise(resolve => { checkScreenMode(expect_mode).then(result => { resolve(result) }) }) } }, // 网页全屏解锁 fixedWebfullUnlockStyle () { webfullUnlockTimes++ async function resetPlayerLayout () { $('body').css({ 'padding-top': 0, position: 'auto', }) $('#playerWrap').css('display', 'block') $('#bilibili-player').css({ height: 'auto', position: 'unset', }) $('#playerWrap').append($('#bilibili-player')) $('.float-nav-exp .mini').css('display', '') // 临时设置默认屏幕模式为宽屏用以触发执行自动定位至播放器,定位完后再重新改为网页全屏 setValue('selected_screen_mode', 'wide') const playerDataScreen = await m.getCurrentScreenMode() if (playerDataScreen !== 'full') { m.autoLocation() } setValue('selected_screen_mode', 'web') } if (webfullUnlockTimes === 1) { const clientHeight = getClientHeight() $('body.webscreen-fix').css({ 'padding-top': clientHeight, position: 'unset', }) $('#bilibili-player.mode-webscreen').css({ height: clientHeight, position: 'absolute', }) $('#app').prepend($('#bilibili-player.mode-webscreen')) $('#playerWrap').css('display', 'none') logger.info('网页全屏解锁|成功') setValue('current_screen_mode', 'web') this.insertGoToCommentsButton() // 退出网页全屏 $('.bpx-player-ctrl-btn-icon.bpx-player-ctrl-web-leave').click(function () { resetPlayerLayout() }) // 再次进入网页全屏 $('.bpx-player-ctrl-btn-icon.bpx-player-ctrl-web-enter').click(function () { $('body').css({ 'padding-top': clientHeight, position: 'unset', }) $('#bilibili-player').css({ height: clientHeight, position: 'absolute', }) $('#app').prepend($('#bilibili-player')) $('#playerWrap').css('display', 'none') $('.float-nav-exp .mini').css('display', 'none') $('html,body').scrollTop(0) }) // 进入退出全屏 $('.bpx-player-ctrl-btn.bpx-player-ctrl-full').click(function () { resetPlayerLayout() }) // 进入宽屏 $('.bpx-player-ctrl-btn-icon.bpx-player-ctrl-wide-enter').click(function () { resetPlayerLayout() }) } }, // 插入跳转评论按钮 insertGoToCommentsButton () { insertGoToCommentsButtonTimes++ if (player_type === 'video' && webfull_unlock && insertGoToCommentsButtonTimes === 1) { const goToCommentsBtnHtml = '