NEXT-哔哩哔哩(bilibili.com)播放页调整

1.自动定位到播放器(进入播放页,可自动定位到播放器,可设置偏移量及是否在点击主播放器时定位);2.可设置是否自动选择最高画质;3.可设置播放器默认模式;

目前為 2023-04-01 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name NEXT-哔哩哔哩(bilibili.com)播放页调整
  3. // @license GPL-3.0 License
  4. // @namespace https://gf.qytechs.cn/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
  5. // @version 0.13
  6. // @description 1.自动定位到播放器(进入播放页,可自动定位到播放器,可设置偏移量及是否在点击主播放器时定位);2.可设置是否自动选择最高画质;3.可设置播放器默认模式;
  7. // @author QIAN
  8. // @match *://*.bilibili.com/video/*
  9. // @match *://*.bilibili.com/bangumi/play/*
  10. // @match *://*.bilibili.com/list/watchlater*
  11. // @run-at document-start
  12. // @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js
  13. // @require https://unpkg.com/sweetalert2@11.7.2/dist/sweetalert2.min.js
  14. // @resource swalStyle https://unpkg.com/sweetalert2@11.7.2/dist/sweetalert2.min.css
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_getResourceText
  19. // @grant GM.info
  20. // @supportURL https://github.com/QIUZAIYOU/Bilibili-VideoPage-Adjustment
  21. // @homepageURL https://github.com/QIUZAIYOU/Bilibili-VideoPage-Adjustment
  22. // @icon https://www.bilibili.com/favicon.ico?v=1
  23. // ==/UserScript==
  24. $(() => {
  25. 'use strict'
  26. let {
  27. currentUrl,
  28. theMainFunctionRunningTimes,
  29. thePrepFunctionRunningTimes,
  30. autoSelectScreenModeTimes,
  31. autoCancelMuteTimes,
  32. webfullUnlockTimes,
  33. insertGoToCommentsButtonTimes,
  34. autoSelectVideoHightestQualityTimes,
  35. } = {
  36. currentUrl: window.location.href,
  37. theMainFunctionRunningTimes: 0,
  38. thePrepFunctionRunningTimes: 0,
  39. autoSelectScreenModeTimes: 0,
  40. autoCancelMuteTimes: 0,
  41. webfullUnlockTimes: 0,
  42. insertGoToCommentsButtonTimes: 0,
  43. autoSelectVideoHightestQualityTimes: 0,
  44. }
  45. const {
  46. getValue,
  47. setValue,
  48. sleep,
  49. addStyle,
  50. historyListener,
  51. checkBrowserHistory,
  52. throttle,
  53. getClientHeight,
  54. checkElementExistence,
  55. isLogin,
  56. logger,
  57. checkPageReadyState
  58. } = {
  59. getValue(name) {
  60. return GM_getValue(name)
  61. },
  62. setValue(name, value) {
  63. GM_setValue(name, value)
  64. },
  65. sleep(time) {
  66. return new Promise(resolve => setTimeout(resolve, time))
  67. },
  68. addStyle(id, tag, css) {
  69. tag = tag || 'style'
  70. const doc = document
  71. const styleDom = doc.getElementById(id)
  72. if (styleDom) return
  73. const style = doc.createElement(tag)
  74. style.rel = 'stylesheet'
  75. style.id = id
  76. tag === 'style' ? (style.innerHTML = css) : (style.href = css)
  77. document.head.appendChild(style)
  78. },
  79. historyListener() {
  80. class Dep {
  81. constructor(name) {
  82. this.id = new Date()
  83. this.subs = []
  84. }
  85. defined() {
  86. Dep.watch.add(this)
  87. }
  88. notify() {
  89. this.subs.forEach((e, i) => {
  90. if (typeof e.update === 'function') {
  91. try {
  92. e.update.apply(e)
  93. } catch (err) {
  94. console.warr(err)
  95. }
  96. }
  97. })
  98. }
  99. }
  100. Dep.watch = null
  101. class Watch {
  102. constructor(name, fn) {
  103. this.name = name
  104. this.id = new Date()
  105. this.callBack = fn
  106. }
  107. add(dep) {
  108. dep.subs.push(this)
  109. }
  110. update() {
  111. var cb = this.callBack
  112. cb(this.name)
  113. }
  114. }
  115. var addHistoryMethod = (function() {
  116. var historyDep = new Dep()
  117. return function(name) {
  118. if (name === 'historychange') {
  119. return function(name, fn) {
  120. var event = new Watch(name, fn)
  121. Dep.watch = event
  122. historyDep.defined()
  123. Dep.watch = null
  124. }
  125. } else if (name === 'pushState' || name === 'replaceState') {
  126. var method = history[name]
  127. return function() {
  128. method.apply(history, arguments)
  129. historyDep.notify()
  130. // logger.info("访问历史|变化")
  131. }
  132. }
  133. }
  134. })()
  135. window.addHistoryListener = addHistoryMethod('historychange')
  136. history.pushState = addHistoryMethod('pushState')
  137. history.replaceState = addHistoryMethod('replaceState')
  138. window.addHistoryListener('history', function() {
  139. const throttleAutoLocation = throttle(m.autoLocation, 500)
  140. throttleAutoLocation()
  141. })
  142. },
  143. checkBrowserHistory() {
  144. window.addEventListener('popstate', () => {
  145. m.autoLocation()
  146. })
  147. },
  148. throttle(func, delay) {
  149. let wait = false
  150. return (...args) => {
  151. if (wait) {
  152. return
  153. }
  154. func(...args)
  155. wait = true
  156. setTimeout(() => {
  157. wait = false
  158. }, delay)
  159. }
  160. },
  161. getClientHeight() {
  162. const bodyHeight = document.body.clientHeight || 0
  163. const docHeight = document.documentElement.clientHeight || 0
  164. return bodyHeight < docHeight ? bodyHeight : docHeight
  165. },
  166. // 检查指定HTML元素是否存在
  167. checkElementExistence(selector, maxAttempts, interval) {
  168. return new Promise(resolve => {
  169. let attempts = 0
  170. const intervalId = setInterval(() => {
  171. attempts++
  172. const element = $(selector)
  173. if (element.length) {
  174. clearInterval(intervalId)
  175. resolve(true)
  176. } else if (attempts === maxAttempts) {
  177. clearInterval(intervalId)
  178. resolve(false)
  179. }
  180. }, interval)
  181. })
  182. },
  183. isLogin() {
  184. return Boolean(document.cookie.replace(new RegExp(String.raw`(?:(?:^|.*;\s*)bili_jct\s*=\s*([^;]*).*$)|^.*$`), '$1') || null)
  185. },
  186. logger: {
  187. info(content) {
  188. console.info('%c播放页调整', 'color:white;background:#006aff;padding:2px;border-radius:2px', content)
  189. },
  190. warn(content) {
  191. console.warn('%c播放页调整', 'color:white;background:#ff6d00;padding:2px;border-radius:2px', content)
  192. },
  193. error(content) {
  194. console.error('%c播放页调整', 'color:white;background:#f33;padding:2px;border-radius:2px', content)
  195. },
  196. },
  197. checkPageReadyState(state) {
  198. return new Promise((resolve) => {
  199. const timer = setInterval(() => {
  200. if (document.readyState === state) {
  201. clearInterval(timer);
  202. resolve(true);
  203. }
  204. }, 100);
  205. });
  206. }
  207. }
  208. const {
  209. is_vip,
  210. player_type,
  211. offset_top,
  212. auto_locate,
  213. auto_locate_video,
  214. auto_locate_bangumi,
  215. click_player_auto_locate,
  216. player_offset_top,
  217. current_screen_mode,
  218. selected_screen_mode,
  219. auto_select_video_highest_quality,
  220. contain_quality_4k,
  221. contain_quality_8k,
  222. webfull_unlock,
  223. } = {
  224. is_vip: getValue('is_vip'),
  225. player_type: getValue('player_type'),
  226. offset_top: Math.trunc(getValue('offset_top')),
  227. auto_locate: getValue('auto_locate'),
  228. auto_locate_video: getValue('auto_locate_video'),
  229. auto_locate_bangumi: getValue('auto_locate_bangumi'),
  230. click_player_auto_locate: getValue('click_player_auto_locate'),
  231. player_offset_top: Math.trunc(getValue('player_offset_top')),
  232. current_screen_mode: getValue('current_screen_mode'),
  233. selected_screen_mode: getValue('selected_screen_mode'),
  234. auto_select_video_highest_quality: getValue('auto_select_video_highest_quality'),
  235. contain_quality_4k: getValue('contain_quality_4k'),
  236. contain_quality_8k: getValue('contain_quality_8k'),
  237. webfull_unlock: getValue('webfull_unlock'),
  238. }
  239. const m = {
  240. // 初始化设置参数
  241. initValue() {
  242. const value = [{
  243. name: 'is_vip',
  244. value: false,
  245. }, {
  246. name: 'player_type',
  247. value: 'video',
  248. }, {
  249. name: 'offset_top',
  250. value: 7,
  251. }, {
  252. name: 'player_offset_top',
  253. value: 160,
  254. }, {
  255. name: 'auto_locate',
  256. value: true,
  257. }, {
  258. name: 'auto_locate_video',
  259. value: true,
  260. }, {
  261. name: 'auto_locate_bangumi',
  262. value: true,
  263. }, {
  264. name: 'click_player_auto_locate',
  265. value: true,
  266. }, {
  267. name: 'current_screen_mode',
  268. value: 'normal',
  269. }, {
  270. name: 'selected_screen_mode',
  271. value: 'wide',
  272. }, {
  273. name: 'auto_select_video_highest_quality',
  274. value: true,
  275. }, {
  276. name: 'contain_quality_4k',
  277. value: false,
  278. }, {
  279. name: 'contain_quality_8k',
  280. value: false,
  281. }, {
  282. name: 'webfull_unlock',
  283. value: false,
  284. }, ]
  285. value.forEach(v => {
  286. if (getValue(v.name) === undefined) {
  287. setValue(v.name, v.value)
  288. }
  289. })
  290. },
  291. // 检查视频资源是否加载完毕并处于可播放状态
  292. async checkVideoCanPlayThrough() {
  293. const BwpVideoPlayerExists = await checkElementExistence('bwp-video', 10, 10)
  294. if (BwpVideoPlayerExists) {
  295. return new Promise(resolve => {
  296. resolve(true)
  297. })
  298. }
  299. return new Promise(resolve => {
  300. const checkTimeout = setTimeout(() => {
  301. // logger.error('视频资源|脚本检测失败|重载页面')
  302. // location.reload(true)
  303. resolve(false)
  304. }, 7000)
  305. $('#bilibili-player video').on('canplaythrough', () => {
  306. // logger.info("视频资源加载|成功")
  307. let attempts = 10
  308. const timer = setInterval(() => {
  309. const isHidden = $('#bilibili-player .bpx-player-container').attr('data-ctrl-hidden')
  310. if (isHidden === 'false') {
  311. clearInterval(timer)
  312. clearTimeout(checkTimeout)
  313. // logger.info(`视频可播放`)
  314. // logger.info(`控制条|出现(hidden:${isHidden})`)
  315. resolve(true)
  316. } else if (attempts <= 0) {
  317. clearInterval(timer)
  318. clearTimeout(checkTimeout)
  319. // logger.error("控制条|检查失败")
  320. resolve(false)
  321. }
  322. // logger.info("控制条|检查中")
  323. attempts--
  324. }, 100)
  325. })
  326. })
  327. },
  328. // 获取当前视频类型(video/bangumi)
  329. getCurrentPlayerType() {
  330. const isVideo = currentUrl.includes('www.bilibili.com/video') || currentUrl.includes('www.bilibili.com/list/watchlater')
  331. const isBangumi = currentUrl.includes('www.bilibili.com/bangumi')
  332. setValue('player_type', isVideo ? 'video' : isBangumi && 'bangumi')
  333. },
  334. // 获取当前屏幕模式(normal/wide/web/full)
  335. async getCurrentScreenMode() {
  336. const exists = await checkElementExistence('#bilibili-player .bpx-player-container', 10, 100)
  337. if (exists) {
  338. const screenMode = $('#bilibili-player .bpx-player-container').attr('data-screen')
  339. return Promise.resolve(screenMode)
  340. } else return Promise.resolve(false)
  341. },
  342. // 监听屏幕模式变化(normal/wide/web/full)
  343. watchScreenModeChange() {
  344. const screenModObserver = new MutationObserver(mutations => {
  345. const playerDataScreen = $('#bilibili-player .bpx-player-container').attr('data-screen')
  346. setValue('current_screen_mode', playerDataScreen)
  347. })
  348. screenModObserver.observe($('#bilibili-player .bpx-player-container')[0], {
  349. attributes: true,
  350. attributeFilter: ['data-screen'],
  351. })
  352. },
  353. // 判断自动切换屏幕模式是否切换成功
  354. async checkScreenModeSuccess(expect_mode) {
  355. const current_screen_mode = await this.getCurrentScreenMode()
  356. const player_data_screen = $('#bilibili-player .bpx-player-container').attr('data-screen')
  357. const equal = new Set([
  358. expect_mode,
  359. selected_screen_mode,
  360. current_screen_mode,
  361. player_data_screen,
  362. ]).size === 1
  363. return Promise.resolve(equal)
  364. },
  365. // 自动选择屏幕模式
  366. autoSelectScreenMode() {
  367. autoSelectScreenModeTimes++
  368. if (autoSelectScreenModeTimes === 1) {
  369. const $wideEnterBtn = player_type === 'video' ? $('.bpx-player-ctrl-wide-enter') : $('.squirtle-widescreen-inactive')
  370. const $webEnterBtn = player_type === 'video' ? $('.bpx-player-ctrl-web-enter') : $('.squirtle-pagefullscreen-inactive')
  371. const selectModeBtn = selected_screen_mode === 'wide' ? $wideEnterBtn : $webEnterBtn
  372. const expect_mode = selected_screen_mode === 'wide' ? 'wide' : 'web'
  373. let attempts = 3
  374. selectModeBtn.click()
  375. const checkScreenMode = async (expect_mode) => {
  376. const success = await this.checkScreenModeSuccess(expect_mode)
  377. if (success) {
  378. clearInterval(checkScreenModeInterval)
  379. setValue('current_screen_mode', selected_screen_mode)
  380. return {
  381. done: true,
  382. mode: selected_screen_mode,
  383. }
  384. } else {
  385. selectModeBtn.click()
  386. logger.warn('自动选择屏幕模式失败正在重试')
  387. attempts--
  388. if (attempts === 0) location.reload(true)
  389. }
  390. }
  391. let checkScreenModeInterval = setInterval(checkScreenMode, 100, expect_mode)
  392. return new Promise(resolve => {
  393. checkScreenMode(expect_mode).then(result => {
  394. resolve(result)
  395. })
  396. })
  397. }
  398. },
  399. // 网页全屏解锁
  400. fixedWebfullUnlockStyle() {
  401. webfullUnlockTimes++
  402. if (webfullUnlockTimes === 1) {
  403. const clientHeight = getClientHeight()
  404. $('body.webscreen-fix').css({
  405. 'padding-top': clientHeight,
  406. position: 'unset',
  407. })
  408. $('#bilibili-player.mode-webscreen').css({
  409. height: clientHeight,
  410. position: 'absolute',
  411. })
  412. $('#app').prepend($('#bilibili-player.mode-webscreen'))
  413. $('#playerWrap').css('display', 'none')
  414. logger.info('网页全屏解锁成功')
  415. setValue('current_screen_mode', 'web')
  416. this.insertGoToCommentsButton()
  417. // 退出网页全屏
  418. $('.bpx-player-ctrl-btn-icon.bpx-player-ctrl-web-leave').click(function() {
  419. $('body').css({
  420. 'padding-top': 0,
  421. position: 'auto',
  422. })
  423. $('#playerWrap').css('display', 'block')
  424. const playerWrapHeight = $('#playerWrap').height()
  425. $('#bilibili-player').css({
  426. height: playerWrapHeight,
  427. position: 'unset',
  428. })
  429. $('#playerWrap').append($('#bilibili-player.mode-webscreen'))
  430. setValue('selected_screen_mode', 'wide')
  431. this.autoLocation()
  432. setValue('selected_screen_mode', 'web')
  433. $('.float-nav-exp .mini').css('display', '')
  434. })
  435. // 再次进入网页全屏
  436. $('.bpx-player-ctrl-btn-icon.bpx-player-ctrl-web-enter').click(function() {
  437. $('body').css({
  438. 'padding-top': clientHeight,
  439. position: 'unset',
  440. })
  441. $('#bilibili-player').css({
  442. height: clientHeight,
  443. position: 'absolute',
  444. })
  445. $('#app').prepend($('#bilibili-player'))
  446. $('#playerWrap').css('display', 'none')
  447. $('.float-nav-exp .mini').css('display', 'none')
  448. $('#danmukuBox').css('margin-top', '20px')
  449. $('html,body').scrollTop(0)
  450. })
  451. }
  452. },
  453. // 插入跳转评论按钮
  454. insertGoToCommentsButton() {
  455. insertGoToCommentsButtonTimes++
  456. if (player_type === 'video' && webfull_unlock && insertGoToCommentsButtonTimes === 1) {
  457. const goToCommentsBtnHtml = `<div class="bpx-player-ctrl-btn bpx-player-ctrl-comment" role="button" aria-label="前往评论" tabindex="0"><div id="goToComments" class="bpx-player-ctrl-btn-icon"><span class="bpx-common-svg-icon"><svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="88" height="88" preserveAspectRatio="xMidYMid meet" style="width: 100%; height: 100%; transform: translate3d(0px, 0px, 0px);"><path d="M512 85.333c235.637 0 426.667 191.03 426.667 426.667S747.637 938.667 512 938.667a424.779 424.779 0 0 1-219.125-60.502A2786.56 2786.56 0 0 0 272.82 866.4l-104.405 28.48c-23.893 6.507-45.803-15.413-39.285-39.296l28.437-104.288c-11.008-18.688-18.219-31.221-21.803-37.91A424.885 424.885 0 0 1 85.333 512c0-235.637 191.03-426.667 426.667-426.667zm-102.219 549.76a32 32 0 1 0-40.917 49.216A223.179 223.179 0 0 0 512 736c52.97 0 103.19-18.485 143.104-51.67a32 32 0 1 0-40.907-49.215A159.19 159.19 0 0 1 512 672a159.19 159.19 0 0 1-102.219-36.907z" fill="#currentColor"/></svg></span></div></div>`
  458. $('.bpx-player-control-bottom-right').append(goToCommentsBtnHtml)
  459. $('#goToComments').on('click', function(event) {
  460. event.stopPropagation()
  461. $('body,html').scrollTop($('#comment').offset().top - 10)
  462. logger.info('到达评论区')
  463. })
  464. }
  465. },
  466. // 添加返回播放器按钮
  467. async insertBackToPlayerButton() {
  468. const playerDataScreen = await this.getCurrentScreenMode()
  469. if (player_type === 'video') {
  470. const locateButtonHtml = `<div class="fixed-sidenav-storage-item locate" title="定位至播放器">\n<svg t="1643419779790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1775" width="200" height="200" style="width: 50%;height: 100%;fill: currentColor;"><path d="M512 352c-88.008 0-160.002 72-160.002 160 0 88.008 71.994 160 160.002 160 88.01 0 159.998-71.992 159.998-160 0-88-71.988-160-159.998-160z m381.876 117.334c-19.21-177.062-162.148-320-339.21-339.198V64h-85.332v66.134c-177.062 19.198-320 162.136-339.208 339.198H64v85.334h66.124c19.208 177.062 162.144 320 339.208 339.208V960h85.332v-66.124c177.062-19.208 320-162.146 339.21-339.208H960v-85.334h-66.124zM512 810.666c-164.274 0-298.668-134.396-298.668-298.666 0-164.272 134.394-298.666 298.668-298.666 164.27 0 298.664 134.396 298.664 298.666S676.27 810.666 512 810.666z" p-id="1776"></path></svg></div>`
  471. const floatNav = $('.fixed-sidenav-storage .back-to-top-wrap')
  472. const locateButton = $('.storable-items .fixed-sidenav-storage-item.locate')
  473. // $('.fixed-sidenav-storage').css('bottom', '274px')
  474. const dataV = floatNav[0].attributes[1].name
  475. const locateButtonHtmlDataV = locateButtonHtml.replace(`title="定位至播放器"`, `title="定位至播放器" ${dataV}`)
  476. floatNav.prepend(locateButtonHtmlDataV)
  477. locateButton.not(':first-child').remove()
  478. floatNav.on('click', '.locate', function() {
  479. $('html,body').scrollTop(playerDataScreen !== 'web' ? player_offset_top - offset_top : 0)
  480. })
  481. }
  482. if (player_type === 'bangumi') {
  483. const locateButtonHtml = `<div class="tool-item locate" title="定位至播放器">\n<svg t="1643419779790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1775" width="200" height="200" style="width: 50%;height: 100%;fill: currentColor;"><path d="M512 352c-88.008 0-160.002 72-160.002 160 0 88.008 71.994 160 160.002 160 88.01 0 159.998-71.992 159.998-160 0-88-71.988-160-159.998-160z m381.876 117.334c-19.21-177.062-162.148-320-339.21-339.198V64h-85.332v66.134c-177.062 19.198-320 162.136-339.208 339.198H64v85.334h66.124c19.208 177.062 162.144 320 339.208 339.208V960h85.332v-66.124c177.062-19.208 320-162.146 339.21-339.208H960v-85.334h-66.124zM512 810.666c-164.274 0-298.668-134.396-298.668-298.666 0-164.272 134.394-298.666 298.668-298.666 164.27 0 298.664 134.396 298.664 298.666S676.27 810.666 512 810.666z" p-id="1776"></path></svg></div>`
  484. const floatNav = $('.nav-tools')
  485. const locateButton = $('.nav-tools .tool-item.locate')
  486. floatNav.prepend(locateButtonHtml)
  487. locateButton.not(':first-child').remove()
  488. floatNav.on('click', '.locate', function() {
  489. $('html,body').scrollTop(playerDataScreen !== 'web' ? player_offset_top - offset_top : 0)
  490. })
  491. }
  492. },
  493. // 自动定位至播放器
  494. autoLocation() {
  495. const $player = $('#bilibili-player')
  496. const player_offset_top = Math.trunc($player.offset().top)
  497. setValue('player_offset_top', player_offset_top)
  498. return new Promise(resolve => {
  499. const isAutoLocate = auto_locate && ((!auto_locate_video && !auto_locate_bangumi) || (auto_locate_video && player_type === 'video') || (auto_locate_bangumi && player_type === 'bangumi'))
  500. if (!isAutoLocate || selected_screen_mode === 'web') {
  501. resolve(false)
  502. return
  503. }
  504. const scrollToPlayer = () => {
  505. $('html,body').scrollTop(player_offset_top - offset_top)
  506. }
  507. scrollToPlayer()
  508. const applyAutoLocationInterval = setInterval(() => {
  509. scrollToPlayer()
  510. logger.warn(`自动定位失败,继续尝试
  511. -----------------
  512. 当前文档顶部偏移量:${$(document).scrollTop()}
  513. 期望文档顶部偏移量:${player_offset_top - offset_top}
  514. 播放器顶部偏移量:${player_offset_top}
  515. 设置偏移量:${offset_top}`)
  516. }, 200)
  517. const checkAutoLocationStatus = setInterval(() => {
  518. const document_scroll_top = $(document).scrollTop()
  519. const success = document_scroll_top === player_offset_top - offset_top
  520. if (success) {
  521. clearInterval(checkAutoLocationStatus)
  522. clearInterval(applyAutoLocationInterval)
  523. // logger.info("自动定位成功");
  524. resolve(true)
  525. }
  526. }, 100)
  527. })
  528. },
  529. // 点击播放器自动定位至播放器
  530. async clickPlayerAutoLocation() {
  531. const playerDataScreen = await this.getCurrentScreenMode();
  532. if (click_player_auto_locate) {
  533. $('#bilibili-player').on('click', handleClick);
  534.  
  535. function handleClick(event) {
  536. event.stopPropagation();
  537. if ($(this).attr('status') === 'adjustment-mini') {
  538. logger.info('点击迷你播放器');
  539. } else {
  540. const scrollTop = playerDataScreen !== 'web' ? player_offset_top - offset_top : 0;
  541. $('html,body').scrollTop(scrollTop);
  542. }
  543. }
  544. }
  545. },
  546. // 点击时间锚点自动返回播放器
  547. jumpVideoTime() {
  548. const video = $('#bilibili-player video')[0]
  549. const clickTarget = player_type === 'video' ? '#comment' : '#comment_module'
  550. const $clickTarget = $(clickTarget)
  551. $clickTarget.unbind('click').on('click', '.video-time,.video-seek', function(event) {
  552. event.stopPropagation()
  553. const targetTime = $(this).attr(player_type === 'video' ? 'data-video-time' : 'data-time')
  554. video.currentTime = targetTime
  555. video.play()
  556. $('html,body').scrollTop(selected_screen_mode === 'web' ? 0 : player_offset_top - offset_top)
  557. })
  558. },
  559. // 自动取消静音
  560. autoCancelMute() {
  561. autoCancelMuteTimes++
  562. const cancelMuteButtn = player_type === 'video' ? $('.bpx-player-ctrl-muted-icon') : $('.squirtle-volume-wrap .squirtle-volume .squirtle-volume-icon')
  563. const cancelMuteButtnDisplay = cancelMuteButtn.css('display')
  564. const cancelMuteButtnClass = cancelMuteButtn.attr('class')
  565. if (autoCancelMuteTimes === 1) {
  566. if (player_type === 'video' && cancelMuteButtnDisplay === 'block') {
  567. cancelMuteButtn.click()
  568. logger.info('已自动取消静音')
  569. }
  570. if (player_type === 'bangumi' && cancelMuteButtnClass.includes('squirtle-volume-mute-state')) {
  571. cancelMuteButtn.click()
  572. logger.info('已自动取消静音')
  573. }
  574. }
  575. },
  576. // 自动选择最高画质
  577. autoSelectVideoHightestQuality() {
  578. autoSelectVideoHightestQualityTimes++;
  579. if (!auto_select_video_highest_quality) return;
  580. if (autoSelectVideoHightestQualityTimes === 1) {
  581. let qualityValue, message;
  582. switch (player_type) {
  583. case 'video':
  584. if (is_vip) {
  585. if (!contain_quality_4k && !contain_quality_8k) {
  586. qualityValue = $('.bpx-player-ctrl-quality > ul > li').filter(function() {
  587. const qualityText = $(this).children('span.bpx-player-ctrl-quality-text').text();
  588. return (!qualityText.includes('4K') && !qualityText.includes('8K'))
  589. });
  590. message = '最高画质|VIP|不包含4K及8K|切换成功';
  591. } else if (contain_quality_4k && contain_quality_8k) {
  592. qualityValue = $('.bpx-player-ctrl-quality > ul > li').filter(function() {
  593. return $(this).children('span.bpx-player-ctrl-quality-text').text().includes('8K')
  594. });
  595. message = '最高画质|VIP|8K|切换成功';
  596. } else if (contain_quality_4k && !contain_quality_8k) {
  597. qualityValue = $('.bpx-player-ctrl-quality > ul > li').filter(function() {
  598. return $(this).children('span.bpx-player-ctrl-quality-text').text().includes('4K')
  599. });
  600. message = '最高画质|VIP|4K|切换成功';
  601. } else if (!contain_quality_4k && contain_quality_8k) {
  602. qualityValue = $('.bpx-player-ctrl-quality > ul > li').filter(function() {
  603. return $(this).children('span.bpx-player-ctrl-quality-text').text().includes('8K')
  604. });
  605. message = '最高画质|VIP|8K|切换成功';
  606. }
  607. qualityValue.eq(0).click();
  608. } else {
  609. const selectVipItemLength = $('.bpx-player-ctrl-quality > ul > li').children('.bpx-player-ctrl-quality-badge-bigvip').length;
  610. $('.bpx-player-ctrl-quality > ul > li').eq(selectVipItemLength).click();
  611. message = '最高画质|非VIP|切换成功';
  612. }
  613. break;
  614. case 'bangumi':
  615. if (is_vip) {
  616. if (contain_quality_4k) {
  617. $('.squirtle-quality-wrap >.squirtle-video-quality > ul > li').eq(0).click();
  618. message = '最高画质|VIP|包含4K|切换成功';
  619. } else {
  620. qualityValue = $('.squirtle-quality-wrap > .squirtle-video-quality > ul > li').filter(function() {
  621. const qualityText = $(this).children('.squirtle-quality-text-c').children('.squirtle-quality-text').text();
  622. return (!qualityText.includes('4K') && !qualityText.includes('8K'))
  623. });
  624. qualityValue.eq(0).click()
  625. message = '最高画质|VIP|不包含4K|切换成功';
  626. }
  627. } else {
  628. const selectVipItemLength = $('.squirtle-quality-wrap >.squirtle-video-quality > ul > li').children('.squirtle-bigvip').length;
  629. $('.squirtle-quality-wrap >.squirtle-video-quality > ul > li').eq(selectVipItemLength).click();
  630. message = '最高画质|非VIP|切换成功';
  631. }
  632. break;
  633. default:
  634. break;
  635. }
  636. logger.info(message);
  637. }
  638. },
  639. // 添加样式文件
  640. addPluginStyle() {
  641. const style = `
  642. #playerAdjustment {
  643. height: 500px;
  644. overflow: auto;
  645. overscroll-behavior: contain;
  646. padding-right: 10px;
  647. }
  648. .swal2-popup {
  649. width: 34em !important;
  650. padding: 1.25em !important;
  651. }
  652. .swal2-html-container {
  653. margin: 0 !important;
  654. padding: 16px 5px 0 !important;
  655. width: 100% !important;
  656. box-sizing: border-box !important;
  657. }
  658. .swal2-footer {
  659. flex-direction: column !important;
  660. }
  661. .swal2-close {
  662. top: 5px !important;
  663. right: 3px !important;
  664. }
  665. .swal2-actions {
  666. margin: 7px auto 0 !important;
  667. }
  668. .swal2-styled.swal2-confirm {
  669. background-color: #23ade5 !important;
  670. }
  671. .swal2-icon.swal2-info.swal2-icon-show {
  672. display: none !important;
  673. }
  674. .player-adjustment-container, .swal2-container {
  675. z-index: 999999999 !important;
  676. }
  677. .player-adjustment-popup {
  678. font-size: 14px !important;
  679. }
  680. .player-adjustment-setting-label {
  681. display: flex !important;
  682. align-items: center !important;
  683. justify-content: space-between !important;
  684. padding-top: 10px !important;
  685. }
  686. .player-adjustment-setting-checkbox {
  687. width: 16px !important;
  688. height: 16px !important;
  689. }
  690. .player-adjustment-setting-tips {
  691. width: 100% !important;
  692. display: flex !important;
  693. align-items: center !important;
  694. padding: 5px !important;
  695. margin-top: 10px !important;
  696. background: #f5f5f5 !important;
  697. box-sizing: border-box !important;
  698. font-size: 14px !important;
  699. color: #666 !important;
  700. border-radius: 2px !important;
  701. text-align: left !important;
  702. }
  703. .player-adjustment-setting-tips svg {
  704. margin-right: 5px !important;
  705. }
  706. label.player-adjustment-setting-label input {
  707. border: 1px solid #cecece !important;
  708. background: #fff !important;
  709. }
  710. label.player-adjustment-setting-label input[type=checkbox],
  711. label.player-adjustment-setting-label input[type=radio] {
  712. width: 16px !important;
  713. height: 16px !important;
  714. }
  715. label.player-adjustment-setting-label input:checked {
  716. border-color: #1986b3 !important;
  717. background: #23ade5 !important;
  718. }
  719. .auto-quality-sub-options,
  720. .auto-locate-sub-options {
  721. display: flex;
  722. align-items: center;
  723. padding-left: 15px;
  724. }
  725. .auto-quality-sub-options label.player-adjustment-setting-label.fourK,
  726. .auto-locate-sub-options label.player-adjustment-setting-label.video {
  727. margin-right: 10px;
  728. }
  729. .auto-quality-sub-options .player-adjustment-setting-label input[type="checkbox"] {
  730. margin-left: 5px !important;
  731. }
  732. .player-adjustment-setting-label.screen-mod input {
  733. margin-right: 5px !important;
  734. }
  735. #biliMainHeader {
  736. height:64px!important;
  737. }
  738. #viewbox_report {
  739. height:106px!important;
  740. padding-top:24px!important;
  741. }
  742. #v_upinfo {
  743. height:80px!important;
  744. }
  745. .members-info-v1 {
  746. padding-top:0!important;
  747. }
  748. .members-info-v1 .wide-members-header {
  749. height:0!important;
  750. }
  751. .members-info-v1 .wide-members-container .up-card .info-tag {
  752. display:none!important;
  753. }
  754. .fixed-sidenav-storage-item.locate {
  755. width:40px!important;
  756. height:40px!important;
  757. }
  758. `
  759. const addStyleToHead = () => {
  760. addStyle('swal-pub-style', 'style', GM_getResourceText('swalStyle'))
  761. addStyle('player-adjustment-style', 'style', style)
  762. }
  763. if (document.head) {
  764. addStyleToHead()
  765. } else {
  766. const headObserver = new MutationObserver(() => {
  767. if (document.head) {
  768. headObserver.disconnect()
  769. addStyleToHead()
  770. }
  771. })
  772. headObserver.observe(document.documentElement, {
  773. childList: true,
  774. subtree: true,
  775. })
  776. }
  777. },
  778. // 注册(不可用)脚本设置控件
  779. registerMenuCommand() {
  780. GM_registerMenuCommand('设置', () => {
  781. const html = `
  782. <div id="playerAdjustment" style="font-size: 1em;">
  783. <label class="player-adjustment-setting-label" style="padding-top:0!important;"> 是否为大会员
  784. <input type="checkbox" id="Is-Vip" ${
  785. getValue('is_vip') ? 'checked' : ''
  786. } class="player-adjustment-setting-checkbox">
  787. </label>
  788. <span class="player-adjustment-setting-tips"> -> 请如实勾选,否则影响自动选择清晰度</span>
  789. <label class="player-adjustment-setting-label"> 自动定位至播放器
  790. <input type="checkbox" id="Auto-Locate" ${
  791. getValue('auto_locate') ? 'checked' : ''
  792. } class="player-adjustment-setting-checkbox">
  793. </label>
  794. <div class="auto-locate-sub-options">
  795. <label class="player-adjustment-setting-label video"> 普通视频(video)
  796. <input type="checkbox" id="Auto-Locate-Video" ${
  797. getValue('auto_locate_video') ? 'checked' : ''
  798. } class="player-adjustment-setting-checkbox">
  799. </label>
  800. <label class="player-adjustment-setting-label bangumi"> 其他视频(bangumi)
  801. <input type="checkbox" id="Auto-Locate-Bangumi" ${
  802. getValue('auto_locate_bangumi') ? 'checked' : ''
  803. } class="player-adjustment-setting-checkbox">
  804. </label>
  805. </div>
  806. <span class="player-adjustment-setting-tips"> -> 只有勾选自动定位至播放器,才会执行自动定位的功能;勾选自动定位至播放器后,video bangumi 两者全选或全不选,默认在这两种类型视频播放页都执行;否则勾选哪种类型,就只在这种类型的播放页才执行。</span>
  807. <label class="player-adjustment-setting-label" id="player-adjustment-Range-Wrapper">
  808. <span>播放器顶部偏移(px)</span>
  809. <input id="Top-Offset" value="${getValue(
  810. 'offset_top'
  811. )}" style="padding:5px;width: 200px;border: 1px solid #cecece;">
  812. </label>
  813. <span class="player-adjustment-setting-tips"> -> 播放器距离浏览器窗口默认距离为 ${Math.trunc(
  814. $('#bilibili-player').offset().top
  815. )};请填写小于 ${Math.trunc(
  816. $('#bilibili-player').offset().top
  817. )} 的正整数或 0;当值为 0 时,播放器上沿将紧贴浏览器窗口上沿、值为 ${Math.trunc(
  818. $('#bilibili-player').offset().top
  819. )} 时,将保持B站默认。 </span>
  820. <label class="player-adjustment-setting-label"> 点击播放器时定位
  821. <input type="checkbox" id="Click-Player-Auto-Location" ${
  822. getValue('click_player_auto_locate') ? 'checked' : ''
  823. } class="player-adjustment-setting-checkbox">
  824. </label>
  825. <div class="player-adjustment-setting-label screen-mod" style="display: flex;align-items: center;justify-content: space-between;"> 播放器默认模式 <div style="width: 215px;display: flex;align-items: center;justify-content: space-between;">
  826. <label class="player-adjustment-setting-label" style="padding-top:0!important;">
  827. <input type="radio" name="Screen-Mod" value="wide" ${
  828. getValue('selected_screen_mode') === 'wide'
  829. ? 'checked'
  830. : ''
  831. }>宽屏 </label>
  832. <label class="player-adjustment-setting-label" style="padding-top:0!important;">
  833. <input type="radio" name="Screen-Mod" value="web" ${
  834. getValue('selected_screen_mode') === 'web'
  835. ? 'checked'
  836. : ''
  837. }> 网页全屏 </label>
  838. </div>
  839. </div>
  840. <span class="player-adjustment-setting-tips"> -> 若遇到不能自动选择播放器模式可尝试点击重置</span>
  841. <label class="player-adjustment-setting-label"> 网页全屏模式解锁
  842. <input type="checkbox" id="Webfull-Unlock" ${
  843. getValue('webfull_unlock') ? 'checked' : ''
  844. } class="player-adjustment-setting-checkbox">
  845. </label>
  846. <span class="player-adjustment-setting-tips"> ->*实验性功能(不稳,可能会有这样或那样的问题):勾选后网页全屏模式下可以滑动滚动条查看下方评论等内容,2秒延迟后解锁(番剧播放页不支持)<br>->新增迷你播放器显示,不过比较简陋,只支持暂停/播放操作,有条件的建议还是直接使用浏览器自带的小窗播放功能。</span>
  847. <label class="player-adjustment-setting-label"> 自动选择最高画质
  848. <input type="checkbox" id="Auto-Quality" ${
  849. getValue('auto_select_video_highest_quality')
  850. ? 'checked'
  851. : ''
  852. } class="player-adjustment-setting-checkbox">
  853. </label>
  854. <div class="auto-quality-sub-options">
  855. <label class="player-adjustment-setting-label fourK"> 是否包含4K画质
  856. <input type="checkbox" id="Quality-4K" ${
  857. getValue('contain_quality_4k') ? 'checked' : ''
  858. } class="player-adjustment-setting-checkbox">
  859. </label>
  860. <label class="player-adjustment-setting-label eightK"> 是否包含8K画质
  861. <input type="checkbox" id="Quality-8K" ${
  862. getValue('contain_quality_8k') ? 'checked' : ''
  863. } class="player-adjustment-setting-checkbox">
  864. </label>
  865. </div>
  866. <span class="player-adjustment-setting-tips"> -> 网络条件好时可以启用此项,勾哪项选哪项,都勾选8k,否则选择4k8k外最高画质。</span>
  867. </div>
  868. `
  869. Swal.fire({
  870. title: '播放页调整设置',
  871. html: html,
  872. icon: 'info',
  873. showCloseButton: true,
  874. showDenyButton: true,
  875. confirmButtonText: '保存',
  876. denyButtonText: '重置',
  877. footer: '<div style="text-align: center;">如果发现脚本不能用,可能是播放页更新了,请耐心等待适配。</div><hr style="border: none;height: 1px;margin: 12px 0;background: #eaeaea;"><div style="text-align: center;font-size: 1.25em;"><a href="//userstyles.world/style/241/nightmode-for-bilibili-com" target="_blank">夜间哔哩 - </a><a href="//gf.qytechs.cn/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" target="_blank">检查更新</a></div>',
  878. }).then(res => {
  879. res.isConfirmed && location.reload(true)
  880. if (res.isConfirmed) {
  881. location.reload(true)
  882. } else if (res.isDenied) {
  883. setValue('current_screen_mode', 'normal')
  884. location.reload(true)
  885. }
  886. })
  887. $('#Is-Vip').change(e => {
  888. setValue('is_vip', e.target.checked)
  889. $('.fourK,.eightK').css('display', e.target.checked ? 'flex!important' : 'none!important')
  890. })
  891. $('#Auto-Locate').change(e => {
  892. setValue('auto_locate', e.target.checked)
  893. })
  894. $('#Auto-Locate-Video').change(e => {
  895. setValue('auto_locate_video', e.target.checked)
  896. })
  897. $('#Auto-Locate-Bangumi').change(e => {
  898. setValue('auto_locate_bangumi', e.target.checked)
  899. })
  900. $('#Top-Offset').change(e => {
  901. setValue('offset_top', e.target.value * 1)
  902. })
  903. $('#Click-Player-Auto-Location').change(e => {
  904. setValue('click_player_auto_locate', e.target.checked)
  905. })
  906. $('#Auto-Quality').change(e => {
  907. setValue('auto_select_video_highest_quality', e.target.checked)
  908. })
  909. $('#Quality-4K').change(e => {
  910. setValue('contain_quality_4k', e.target.checked)
  911. })
  912. $('#Quality-8K').change(e => {
  913. setValue('contain_quality_8k', e.target.checked)
  914. })
  915. $('input[name="Screen-Mod"]').click(function() {
  916. setValue('selected_screen_mode', $(this).val())
  917. })
  918. $('#Webfull-Unlock').change(e => {
  919. setValue('webfull_unlock', e.target.checked)
  920. })
  921. })
  922. },
  923. // 冻结视频标题及UP主信息样式
  924. freezeHeaderAndVideoTitleStyles() {
  925. $('#biliMainHeader').attr('style', 'height:64px!important')
  926. $('#viewbox_report').attr('style', 'height:106px!important;padding-top:24px!important')
  927. $('#v_upinfo').attr('style', 'height:80px!important')
  928. $('.members-info-v1').attr('style', 'padding-top:0!important')
  929. $('.members-info-v1 .wide-members-header').attr('style', 'height:0!important')
  930. $('.members-info-v1 .wide-members-container .up-card .info-tag').attr('style', 'display:none!important')
  931. },
  932. // 判断当前窗口是否在最上方
  933. isTopWindow() {
  934. return window.self === window.top
  935. },
  936. // 前期准备函数
  937. thePrepFunction() {
  938. thePrepFunctionRunningTimes++
  939. if (thePrepFunctionRunningTimes === 1) {
  940. isLogin()
  941. checkBrowserHistory()
  942. historyListener()
  943. this.initValue()
  944. this.addPluginStyle()
  945. this.isTopWindow() && this.registerMenuCommand()
  946. this.getCurrentPlayerType()
  947. this.getCurrentScreenMode()
  948. }
  949. },
  950. // 主函数
  951. async theMainFunction() {
  952. try {
  953. theMainFunctionRunningTimes++
  954. if (theMainFunctionRunningTimes === 1) {
  955. const videoPlayerExists = await checkElementExistence('#bilibili-player video', 5, 100) || await checkElementExistence('bwp-video', 5, 100)
  956. if (videoPlayerExists) {
  957. logger.info(`播放器|存在`)
  958. $('body').css('overflow', 'hidden')
  959. const isPlayable = await this.checkVideoCanPlayThrough()
  960. const pageComplete = await checkPageReadyState('complete')
  961. if (isPlayable || (!isPlayable && pageComplete)) {
  962. logger.info(`视频资源|可以播放`)
  963. // console.time('播放页调整:切换模式耗时')
  964. this.watchScreenModeChange()
  965. await sleep(100)
  966. const selectedScreenMode = await this.autoSelectScreenMode()
  967. // console.timeEnd('播放页调整:切换模式耗时')
  968. if (selectedScreenMode.done) {
  969. logger.info(`屏幕模式|${selectedScreenMode['mode'].toUpperCase()}|切换成功`)
  970. this.autoCancelMute()
  971. // console.time('播放页调整:选择画质耗时')
  972. this.autoSelectVideoHightestQuality()
  973. // console.timeEnd('播放页调整:选择画质耗时')
  974. this.clickPlayerAutoLocation()
  975. if (webfull_unlock && selectedScreenMode.mode === 'web') {
  976. this.fixedWebfullUnlockStyle()
  977. }
  978. // console.time('播放页调整:自动定位耗时')
  979. this.freezeHeaderAndVideoTitleStyles()
  980. const autoLocationDone = await this.autoLocation()
  981. // console.timeEnd('播放页调整:自动定位耗时')
  982. if (auto_locate && autoLocationDone) {
  983. $('body').css('overflow', '')
  984. logger.info(`自动定位|成功`)
  985. this.insertBackToPlayerButton()
  986. this.jumpVideoTime()
  987. }
  988. if (!auto_locate || (auto_locate && auto_locate_bangumi && !auto_locate_video && player_type === 'video') || (auto_locate && auto_locate_video && !auto_locate_bangumi && player_type === 'bangumi')) {
  989. $('body').css('overflow', '')
  990. logger.info(`自动定位|未开启`)
  991. }
  992. if (player_type === 'video') {
  993. const loaded = await checkElementExistence('#comment > .comment > .bili-comment', 10, 100)
  994. await sleep(100)
  995. if (loaded) {
  996. logger.info(`页面加载|完毕`)
  997. } else {
  998. location.reload(true)
  999. }
  1000. }
  1001. } else {
  1002. logger.error(`屏幕模式|${selectedScreenMode.mode}|切换失败`)
  1003. location.reload(true)
  1004. }
  1005. } else {
  1006. logger.error(`视频资源|加载失败`)
  1007. location.reload(true)
  1008. }
  1009. } else {
  1010. logger.error(`播放器|不存在`)
  1011. location.reload(true)
  1012. }
  1013. }
  1014. } catch (error) {
  1015. logger.error(error)
  1016. location.reload(true)
  1017. }
  1018. },
  1019. }
  1020. if (isLogin()) {
  1021. m.thePrepFunction()
  1022. m.theMainFunction()
  1023. } else logger.warn('请登录(不可用)|本脚本只能在登录(不可用)状态下使用')
  1024. })

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址