Precise video playback (Bilibili)

A toolbar to set precise video play time and generate clip script

Zainstaluj skrypt?
Skrypt zaproponowany przez autora

Może Ci się również spodobać. Precise video playback (YouTube)

Zainstaluj skrypt
  1. // ==UserScript==
  2. // @name Precise video playback (Bilibili)
  3. // @name:zh-CN 精确控制视频播放进度 (Bilibili)
  4. // @description A toolbar to set precise video play time and generate clip script
  5. // @description:zh-CN 精确控制视频播放进度/生成剪辑脚本的工具栏
  6. // @namespace moe.suisei.pvp.bilibili
  7. // @match https://bilibili.com/video/*
  8. // @match https://www.bilibili.com/video/*
  9. // @grant none
  10. // @version 0.7.6
  11. // @author Outvi V
  12. // ==/UserScript==
  13.  
  14. 'use strict'
  15.  
  16. function collectCutTiming(cutBar) {
  17. return [...cutBar.querySelectorAll('div > button:nth-child(1)')].map((x) =>
  18. Number(x.innerText)
  19. )
  20. }
  21.  
  22. function createCutButton(time, videoElement) {
  23. const btnJump = document.createElement('button')
  24. const btnRemove = document.createElement('button')
  25. const btnContainer = document.createElement('div')
  26. btnJump.innerText = time
  27. btnRemove.innerText = 'x'
  28. btnJump.addEventListener('click', () => {
  29. videoElement.currentTime = time
  30. })
  31. btnRemove.addEventListener('click', () => {
  32. btnContainer.style.display = 'none'
  33. })
  34. applyStyle(btnContainer, {
  35. marginRight: '0.5vw',
  36. flexShrink: '0',
  37. marginTop: '3px',
  38. })
  39. btnContainer.append(btnJump, btnRemove)
  40. return btnContainer
  41. }
  42.  
  43. console.log('Precise Video Playback is up')
  44.  
  45. function getVideoId(url) {
  46. return String(url).match(/(a|b)v([^?&#]+)/i)[0]
  47. }
  48.  
  49. function applyStyle(elem, styles) {
  50. for (const [key, value] of Object.entries(styles)) {
  51. elem.style[key] = value
  52. }
  53. }
  54.  
  55. function parseTime(str) {
  56. const hms = str.split(':')
  57. let time = 0
  58. for (const i of hms) {
  59. time *= 60
  60. time += Number(i)
  61. if (isNaN(time)) return -1
  62. }
  63. return time
  64. }
  65.  
  66. function generateControl() {
  67. const app = document.createElement('div')
  68. const cutBar = document.createElement('div')
  69. const inputFrom = document.createElement('input')
  70. inputFrom.placeholder = 'from 0'
  71. const inputTo = document.createElement('input')
  72. inputTo.placeholder = 'to ...'
  73. const currentTime = document.createElement('span')
  74. const btn = document.createElement('button')
  75. const btnStop = document.createElement('button')
  76. const btnExport = document.createElement('button')
  77. const btnCut = document.createElement('button')
  78. applyStyle(app, {
  79. display: 'flex',
  80. alignItems: 'center',
  81. justifyContent: 'space-between',
  82. maxWidth: '600px',
  83. marginTop: '15px',
  84. marginLeft: 'auto',
  85. marginRight: 'auto',
  86. })
  87. applyStyle(cutBar, {
  88. display: 'flex',
  89. flexWrap: 'wrap',
  90. marginTop: '1vh',
  91. })
  92. applyStyle(currentTime, {
  93. fontSize: '1.2rem',
  94. minWidth: '7.5rem',
  95. textAlign: 'center',
  96. })
  97. const inputCommonStyle = {
  98. width: '80px',
  99. }
  100. applyStyle(inputFrom, inputCommonStyle)
  101. applyStyle(inputTo, inputCommonStyle)
  102. btn.innerText = 'Jump'
  103. btnStop.innerText = 'Stop'
  104. btnExport.innerText = 'Export'
  105. btnCut.innerText = 'Cut'
  106. app.appendChild(inputFrom)
  107. app.appendChild(inputTo)
  108. app.appendChild(currentTime)
  109. app.appendChild(btn)
  110. app.appendChild(btnStop)
  111. app.appendChild(btnExport)
  112. app.appendChild(btnCut)
  113. return {
  114. app,
  115. cutBar,
  116. inputFrom,
  117. inputTo,
  118. currentTime,
  119. btn,
  120. btnStop,
  121. btnExport,
  122. btnCut,
  123. }
  124. }
  125.  
  126. async function sleep(time) {
  127. await new Promise((resolve) => {
  128. setTimeout(() => {
  129. resolve()
  130. }, time)
  131. })
  132. }
  133.  
  134. async function waitfor(cb) {
  135. for (;;) {
  136. if (cb()) return
  137. await sleep(500)
  138. }
  139. }
  140.  
  141. async function main() {
  142. console.log('Waiting for the page...')
  143. // Wait for Bilibili to fully render the page
  144. // Or error could occur after inserting our widget
  145.  
  146. await waitfor(() => {
  147. return (
  148. document.querySelector('#v_upinfo .up-face') ||
  149. document.querySelector('#member-container .avatar')
  150. )
  151. })
  152. console.log('Pre-install hook OK')
  153.  
  154. // Player fetching
  155. console.log('Waiting for the player...')
  156. let anchor
  157. while (true) {
  158. anchor = document.querySelector('#v_desc')
  159. if (anchor && !anchor.hidden) break
  160. await sleep(500)
  161. }
  162. const videoElement = document.querySelector('video')
  163. if (!videoElement || !anchor) {
  164. console.warn('Player not found. Exiting.')
  165. return
  166. }
  167. console.log('Player detected.')
  168.  
  169. // Layout
  170. const control = generateControl()
  171. const shadowParent = document.createElement('div')
  172. const shadow = shadowParent.attachShadow({ mode: 'open' })
  173. anchor.parentElement.insertBefore(shadowParent, anchor)
  174. anchor.parentElement.insertBefore(control.cutBar, anchor)
  175. shadow.appendChild(control.app)
  176.  
  177. // States
  178. let fromValue = 0
  179. let toValue = 0
  180.  
  181. // Initial state update attempt
  182. const urlTime = window.location.hash.match(
  183. /#pvp([0-9]+\.?[0-9]?)-([0-9]+\.?[0-9]?)/
  184. )
  185. if (urlTime !== null) {
  186. console.log('Attempting to recover time from URL...')
  187. control.inputFrom.value = fromValue = Number(urlTime[1]) || 0
  188. control.inputTo.value = toValue = Number(urlTime[2]) || 0
  189. }
  190.  
  191. // Current playback time
  192. function updateCurrentTime() {
  193. control.currentTime.innerText = Number(videoElement.currentTime).toFixed(2)
  194. requestAnimationFrame(updateCurrentTime)
  195. }
  196. requestAnimationFrame(updateCurrentTime)
  197.  
  198. // Repeat playback
  199. function onTimeUpdate() {
  200. if (videoElement.currentTime >= Number(toValue)) {
  201. videoElement.currentTime = Number(fromValue)
  202. }
  203. }
  204.  
  205. control.btn.addEventListener('click', (evt) => {
  206. evt.preventDefault()
  207. videoElement.pause()
  208. videoElement.currentTime = fromValue
  209. if (fromValue < toValue) {
  210. videoElement.play()
  211. videoElement.addEventListener('timeupdate', onTimeUpdate)
  212. } else {
  213. videoElement.removeEventListener('timeupdate', onTimeUpdate)
  214. }
  215. })
  216.  
  217. control.btnStop.addEventListener('click', (evt) => {
  218. evt.preventDefault()
  219. videoElement.removeEventListener('timeupdate', onTimeUpdate)
  220. videoElement.pause()
  221. })
  222.  
  223. control.btnCut.addEventListener('click', () => {
  224. const nowTime = Number(videoElement.currentTime).toFixed(2)
  225. const btn = createCutButton(nowTime, videoElement)
  226. control.cutBar.append(btn)
  227. })
  228.  
  229. control.btnCut.addEventListener('contextmenu', (evt) => {
  230. evt.preventDefault()
  231. if (!control.cutBar) return
  232. const timings = collectCutTiming(control.cutBar)
  233. const newTimings = prompt(
  234. 'This is your current cut list. Change it to import cut from others.',
  235. JSON.stringify(timings)
  236. )
  237. if (newTimings === null) return
  238. const parsedNewTimings = (() => {
  239. try {
  240. return JSON.parse(newTimings)
  241. } catch {
  242. console.warn('Failed to parse the new cut list.')
  243. return []
  244. }
  245. })()
  246. if (JSON.stringify(timings) === JSON.stringify(parsedNewTimings)) {
  247. console.log('No changes on the cut list.')
  248. return
  249. }
  250. control.cutBar.innerHTML = ''
  251. for (const i of parsedNewTimings) {
  252. const btn = createCutButton(i, videoElement)
  253. control.cutBar.append(btn)
  254. }
  255. })
  256.  
  257. // Start/end time setting
  258. function updateURL() {
  259. history.pushState(null, null, `#pvp${fromValue}-${toValue}`)
  260. }
  261. control.inputFrom.addEventListener('change', () => {
  262. const input = control.inputFrom.value
  263. if (input === '') {
  264. fromValue = 0
  265. control.inputFrom.placeholder = 'from 0'
  266. return
  267. }
  268. const time = parseTime(input)
  269. if (time === -1) {
  270. control.btn.disabled = true
  271. return
  272. }
  273. control.btn.disabled = false
  274. fromValue = time
  275. updateURL()
  276. })
  277. control.inputTo.addEventListener('change', () => {
  278. const input = control.inputTo.value
  279. if (input === '') {
  280. toValue = videoElement.duration || 0
  281. control.btn.innerText = 'Jump'
  282. return
  283. }
  284. control.btn.innerText = 'Repeat'
  285. const time = parseTime(input)
  286. if (time === -1) {
  287. control.btn.disabled = true
  288. return
  289. }
  290. control.btn.disabled = false
  291. toValue = time
  292. updateURL()
  293. })
  294.  
  295. // Button export
  296. control.btnExport.addEventListener('click', (evt) => {
  297. evt.preventDefault()
  298. const videoId = getVideoId(window.location)
  299. alert(`youtube-dl -f 0 "https://www.bilibili.com/video/${videoId}" \\
  300. -x --audio-format mp3 --audio-quality 192k \\
  301. --postprocessor-args "-ss ${fromValue} -to ${toValue} -af loudnorm=I=-16:TP=-2:LRA=11" \\
  302. -o "output-%(id)s-${fromValue}-${toValue}.%(ext)s"`)
  303. })
  304.  
  305. function setInitialDuration(dur) {
  306. control.inputTo.placeholder = `to ${dur.toFixed(2)}`
  307. const input = control.inputTo.value
  308. if (input !== '') return
  309. toValue = dur
  310. }
  311.  
  312. if (videoElement.duration) {
  313. setInitialDuration(videoElement.duration)
  314. } else {
  315. videoElement.addEventListener('loadedmetadata', () => {
  316. setInitialDuration(videoElement.duration)
  317. })
  318. }
  319. }
  320.  
  321. main()