Youtube Better Player

Scroll wheel volume, "Are you there" popup bypass, Volume percent display, Infinite autoplay, Volume save

当前为 2021-07-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Youtube Better Player
  3. // @description Scroll wheel volume, "Are you there" popup bypass, Volume percent display, Infinite autoplay, Volume save
  4. // @include /^https:\/\/www\.youtube\.com\/(?!(live_chat\?.*|ytscframe)$).*$/
  5. // @run-at document-idle
  6. // @allFrames true
  7. // @grant GM_addStyle
  8. // @version 0.0.1.20210730180549
  9. // @namespace https://greasyfork.org/users/286737
  10. // ==/UserScript==
  11.  
  12. class Player {
  13. constructor() {
  14. this.volumeSk = 'ytbp-volume'
  15. }
  16.  
  17. async init(isEmbed) {
  18. const api = this.api = isEmbed ? unsafeWindow.movie_player : await this.getApi()
  19.  
  20. this.volume = this.getSavedVolume()
  21. this.$volumeText = this.buildVolumeText()
  22.  
  23. const {$video, $eventCatcher, $volumeBar, $player} = this.getEls()
  24.  
  25. const onVolumeChange = this.onVolumeChange.bind(this)
  26. $video.addEventListener('volumechange', onVolumeChange)
  27.  
  28. new WheelVolume(this, api, $volumeBar, $player).init($eventCatcher)
  29.  
  30. if (!isEmbed) new RealAutoPlay(api).init($video)
  31. }
  32.  
  33. async getApi() {
  34. let $el, api
  35.  
  36. while (!($el = unsafeWindow['ytd-player'])) await wait(1000)
  37. while (!(api = $el.player_)) await wait(200)
  38. while (!api.isReady()) await wait(200)
  39.  
  40. return api
  41. }
  42.  
  43. getSavedVolume() {
  44. const savedVolume = localStorage.getItem(this.volumeSk)
  45.  
  46. if (savedVolume == undefined) return Math.floor(this.api.getVolume())
  47.  
  48. this.api.setVolume(savedVolume)
  49. return savedVolume
  50. }
  51.  
  52. getEls() {
  53. const $player = $('#movie_player')
  54. const $video = $('video', $player)
  55. const $eventCatcher = $player.parentElement
  56. const $volumeBar = $('.ytp-volume-slider', $player)
  57.  
  58. return {$video, $eventCatcher, $volumeBar, $player}
  59. }
  60.  
  61. buildVolumeText() {
  62. const $volumeText = document.createElement('span')
  63. $volumeText.classList.add('ytbp-volume-text')
  64. $volumeText.textContent = this.volume
  65. GM_addStyle(volumeTextStyle)
  66.  
  67. $('.ytp-volume-area').insertAdjacentElement('beforeend', $volumeText)
  68.  
  69. return $volumeText
  70. }
  71.  
  72. onVolumeChange() {
  73. this.volume = this.$volumeText.textContent = Math.floor(this.api.getVolume())
  74.  
  75. clearTimeout(this.saveTimeout)
  76.  
  77. this.saveTimeout = setTimeout(() => localStorage.setItem(this.volumeSk, this.volume), 1000)
  78. }
  79. }
  80.  
  81. class WheelVolume {
  82. constructor(player, api, $volumeBar, $player) {
  83. this.player = player
  84. this.api = api
  85. this.$volumeBar = $volumeBar
  86. this.$player = $player
  87.  
  88. this.events = {
  89. mouseover: new Event('mouseover', {bubbles: true}),
  90. mouseout: new Event('mouseout', {bubbles: true}),
  91. mousemove: new Event('mousemove')
  92. }
  93. }
  94.  
  95. init($eventCatcher) {
  96. const onWheel = this.onWheel.bind(this)
  97. const onClick = this.onClick.bind(this)
  98.  
  99. $eventCatcher.addEventListener('wheel', onWheel)
  100. $eventCatcher.addEventListener('mousedown', onClick)
  101. }
  102.  
  103. onWheel(e) {
  104. e.preventDefault()
  105. e.stopImmediatePropagation()
  106.  
  107. this.show()
  108.  
  109. const api = this.api
  110. const now = Date.now(), since = now - this.prevScrollDate
  111. const step = (e.deltaY < 0 ? 1 : -1) * (since < 50 ? 4 : 1)
  112.  
  113. if (api.isMuted()) api.unMute()
  114.  
  115. api.setVolume(this.player.volume + step)
  116.  
  117. this.prevScrollDate = now
  118. }
  119.  
  120. onClick(e) {
  121. if (e.which != 2) return
  122.  
  123. e.preventDefault()
  124.  
  125. this.show()
  126.  
  127. const api = this.api
  128.  
  129. if (api.isMuted()) {
  130. api.unMute()
  131. api.setVolume(this.player.volume)
  132. }
  133. else api.mute()
  134. }
  135.  
  136. show() {
  137. const $volumeBar = this.$volumeBar, events = this.events
  138.  
  139. this.$player.dispatchEvent(events.mousemove)
  140.  
  141. clearTimeout(this.showTimeout)
  142.  
  143. $volumeBar.dispatchEvent(events.mouseover)
  144.  
  145. this.showTimeout = setTimeout(() => $volumeBar.dispatchEvent(events.mouseout), 1000)
  146. }
  147. }
  148.  
  149. class RealAutoPlay {
  150. constructor(api) {
  151. this.api = api
  152.  
  153. this.popupName = 'yt-confirm-dialog-renderer'
  154. this.popupContainer = $('ytd-popup-container', unsafeWindow.document)
  155.  
  156. const storedAutoNav = localStorage.getItem('yt.autonav::autonav_disabled')
  157. this.autoNavEnabled = storedAutoNav ? !JSON.parse(storedAutoNav).data : true
  158. }
  159.  
  160. init($video) {
  161. const bypassPopup = this.bypassPopup.bind(this)
  162. const forceNextVideo = this.forceNextVideo.bind(this)
  163. const onToggleAutoNav = this.onToggleAutoNav.bind(this)
  164.  
  165. $video.addEventListener('pause', bypassPopup)
  166. $video.addEventListener('waiting', bypassPopup)
  167. $video.addEventListener('ended', forceNextVideo)
  168.  
  169. $('.ytp-autonav-toggle-button').addEventListener('click', onToggleAutoNav)
  170. }
  171.  
  172. bypassPopup() {
  173. const popup = this.popupContainer.popups_[this.popupName]
  174.  
  175. if (!popup) return
  176.  
  177. this.api.playVideo()
  178.  
  179. popup.popup.remove()
  180. delete this.popupContainer.popups_[this.popupName]
  181. }
  182.  
  183. forceNextVideo() {
  184. if (this.autoNavEnabled && !document.hasFocus()) this.api.nextVideo()
  185. }
  186.  
  187. onToggleAutoNav() {
  188. this.autoNavEnabled = !this.autoNavEnabled
  189. }
  190. }
  191.  
  192. const init = async () => {
  193. const isEmbed = location.pathname.startsWith('/embed/')
  194.  
  195. if (isEmbed) await new Promise(r => $('video').addEventListener('canplay', r, {once: true}))
  196.  
  197. new Player().init(isEmbed)
  198. }
  199.  
  200. const volumeTextStyle = `
  201. .ytbp-volume-text {
  202. width: 0;
  203. text-indent: 2px;
  204. overflow: hidden;
  205. color: #ddd;
  206. font-size: 109%;
  207. line-height: 39px;
  208. text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
  209. transition: width .2s;
  210. }
  211. .ytbp-volume-text:after { content: '%'; }
  212.  
  213. .ytp-volume-control-hover:not([aria-valuenow="0"], [aria-valuenow="100"]) + .ytbp-volume-text {
  214. width: 32px;
  215. }
  216. `
  217.  
  218. const $ = (sel, el = document) => el.querySelector(sel)
  219.  
  220. const wait = (ms) => new Promise(r => setTimeout(r, ms))
  221.  
  222.  
  223. init()