Advertisement
Embrosy

Local YouTube Downloader

Oct 24th, 2022
63
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 26.35 KB | None | 0 0
  1.  
  2.  
  3. // ==UserScript==
  4. // @name Local YouTube Downloader
  5. // @name:zh-TW 本地 YouTube 下載器
  6. // @name:zh-HK 本地 YouTube 下載器
  7. // @name:zh-CN 本地 YouTube 下载器
  8. // @name:ja ローカル YouTube ダウンローダー
  9. // @name:kr 로컬 YouTube 다운로더
  10. // @namespace https://blog.maple3142.net/
  11. // @version 0.9.50
  12. // @description Download YouTube videos without external service.
  13. // @description:zh-TW 不需透過第三方服務即可下載 YouTube 影片。
  14. // @description:zh-HK 不需透過第三方服務即可下載 YouTube 影片。
  15. // @description:zh-CN 不需要透过第三方的服务就能下载 YouTube 视频。
  16. // @description:ja 外部サービスなしで YouTube 動画をダウンロード
  17. // @description:kr 외부 서비스없이 YouTube 동영상을 다운로드
  18. // @description:fr Obtenez un lien brut YouTube sans service externe.
  19. // @description:cs Stahujte YouTube videa bez externích služeb.
  20. // @author maple3142
  21. // @match https://*.youtube.com/*
  22. // @require https://unpkg.com/vue@2.6.10/dist/vue.js
  23. // @require https://unpkg.com/xfetch-js@0.3.4/xfetch.min.js
  24. // @require https://unpkg.com/@ffmpeg/ffmpeg@0.6.1/dist/ffmpeg.min.js
  25. // @require https://bundle.run/p-queue@6.3.0
  26. // @grant GM_xmlhttpRequest
  27. // @grant unsafeWindow
  28. // @run-at document-end
  29. // @connect googlevideo.com
  30. // @compatible firefox >=52
  31. // @compatible chrome >=55
  32. // @license MIT
  33. // ==/UserScript==
  34.  
  35. ;(function () {
  36. 'use strict'
  37. const DEBUG = true
  38. const createLogger = (console, tag) =>
  39. Object.keys(console)
  40. .map(k => [k, (...args) => (DEBUG ? console[k](tag + ': ' + args[0], ...args.slice(1)) : void 0)])
  41. .reduce((acc, [k, fn]) => ((acc[k] = fn), acc), {})
  42. const logger = createLogger(console, 'YTDL')
  43. const sleep = ms => new Promise(res => setTimeout(res, ms))
  44.  
  45. const LANG_FALLBACK = 'en'
  46. const LOCALE = {
  47. en: {
  48. togglelinks: 'Show/Hide Links',
  49. stream: 'Stream',
  50. adaptive: 'Adaptive (No Sound)',
  51. videoid: 'Video ID: ',
  52. inbrowser_adaptive_merger: 'Online Adaptive Video & Audio Merger (FFmpeg)',
  53. dlmp4: 'Download high-resolution mp4 in one click',
  54. get_video_failed: 'Failed to get video infomation for unknown reason, refresh the page may work.',
  55. live_stream_disabled_message: 'Local YouTube Downloader is not available for live stream'
  56. },
  57. 'zh-tw': {
  58. togglelinks: '顯示 / 隱藏連結',
  59. stream: '串流 Stream',
  60. adaptive: '自適應 Adaptive (沒有聲音)',
  61. videoid: '影片 ID: ',
  62. inbrowser_adaptive_merger: '線上自適應影片及音訊合成工具 (FFmpeg)',
  63. dlmp4: '一鍵下載高畫質 mp4',
  64. get_video_failed: '無法取得影片資訊,重新整理頁面可能會有效果。',
  65. live_stream_disabled_message: '因為是直播的緣故,本地 YouTube 下載器的功能是停用的。'
  66. },
  67. 'zh-hk': {
  68. togglelinks: '顯示/隱藏連結',
  69. stream: '串流 Stream',
  70. adaptive: '自動適應 Adaptive (沒有聲音)',
  71. videoid: '影片 ID: ',
  72. inbrowser_adaptive_merger: '網上自動適應影片及音訊合成工具 (FFmpeg)',
  73. dlmp4: '一 click 下載高畫質 mp4',
  74. get_video_failed: '無法取得影片資訊,重新整理頁面可能會有效果。',
  75. live_stream_disabled_message: '本地 YouTube 下載器無法用於直播。'
  76. },
  77. zh: {
  78. togglelinks: '显示/隐藏链接',
  79. stream: '串流 Stream',
  80. adaptive: '自适应 Adaptive (没有声音)',
  81. videoid: '视频 ID: ',
  82. inbrowser_adaptive_merger: '线上自适应视频及音频合成工具 (FFmpeg)',
  83. dlmp4: '一键下载高画质 mp4',
  84. get_video_failed: '无法取得影片资讯,重新整理页面可能会有效果。',
  85. live_stream_disabled_message: '因为是直播,本地 YouTube 下载器的功能已被禁用。'
  86. },
  87. ja: {
  88. togglelinks: 'リンク表示・非表示',
  89. stream: 'ストリーミング',
  90. adaptive: 'アダプティブ(音無し)',
  91. videoid: 'ビデオ ID: ',
  92. inbrowser_adaptive_merger: 'ビデオとオーディオを合併するオンラインツール (FFmpeg)',
  93. dlmp4: 'ワンクリックで高解像度の mp4 をダウンロード',
  94. live_stream_disabled_message: 'ライブ配信のため、ローカル YouTube ダウンローダーは無効になっています。'
  95. },
  96. kr: {
  97. togglelinks: '링크 보이기 · 숨기기',
  98. stream: '스트리밍',
  99. adaptive: '적응 (어댑티브)',
  100. videoid: '비디오 ID: ',
  101. inbrowser_adaptive_merger: '비디오와 오디오를 합병하는 온라인 도구 (FFmpeg)',
  102. dlmp4: '한 번의 클릭으로 고해상도 mp4 다운로드'
  103. },
  104. es: {
  105. togglelinks: 'Mostrar/Ocultar Links',
  106. stream: 'Stream',
  107. adaptive: 'Adaptable',
  108. videoid: 'Id del Video: ',
  109. inbrowser_adaptive_merger: 'Acoplar Audio a Video (FFmpeg)'
  110. },
  111. he: {
  112. togglelinks: 'הצג/הסתר קישורים',
  113. stream: 'סטרים',
  114. adaptive: 'אדפטיבי',
  115. videoid: 'מזהה סרטון: '
  116. },
  117. fr: {
  118. togglelinks: 'Afficher/Masquer les liens',
  119. stream: 'Stream',
  120. adaptive: 'Adaptative',
  121. videoid: 'ID vidéo: ',
  122. inbrowser_adaptive_merger: 'Fusionner vidéos et audios adaptatifs dans le navigateur (FFmpeg)',
  123. dlmp4: 'Téléchargez la plus haute résolution mp4 en un clic'
  124. },
  125. pl: {
  126. togglelinks: 'Pokaż/Ukryj Linki',
  127. stream: 'Stream',
  128. adaptive: 'Adaptywne',
  129. videoid: 'ID filmu: ',
  130. inbrowser_adaptive_merger: 'Połącz audio i wideo adaptywne w przeglądarce (FFmpeg)',
  131. dlmp4: 'Pobierz .mp4 w najwyższej jakości'
  132. },
  133. hi: {
  134. togglelinks: 'लिंक टॉगल करें',
  135. stream: 'स्ट्रीमिंग (Stream)',
  136. adaptive: 'अनुकूली (Adaptive)',
  137. videoid: 'वीडियो आईडी: {{id}}'
  138. },
  139. ru: {
  140. togglelinks: 'Показать/Cкрыть ссылки',
  141. stream: 'Поток',
  142. adaptive: 'Адаптивный',
  143. videoid: 'Идентификатор видео: ',
  144. inbrowser_adaptive_merger: 'Адаптивное слияние видео и аудио онлайн (FFmpeg)',
  145. dlmp4: 'Скачать mp4 в высоком разрешении в один клик',
  146. get_video_failed:
  147. 'Не удалось получить информацию о видео по неизвестной причине, попробуйте обновить страницу.',
  148. live_stream_disabled_message: 'Локальный загрузчик YouTube недоступен для прямой трансляции'
  149. },
  150. ua: {
  151. togglelinks: 'Показати/Приховати посилання',
  152. stream: 'Потік',
  153. adaptive: 'Адаптивний',
  154. videoid: 'Ідентифікатор відео: ',
  155. inbrowser_adaptive_merger: 'Адаптивне злиття відео і аудіо онлайн (FFmpeg)',
  156. dlmp4: 'Завантажити mp4 у високій роздільній здатності в один клік',
  157. get_video_failed:
  158. 'Не вдалося отримати інформацію про відео з невідомої причини, спробуйте оновити сторінку.',
  159. live_stream_disabled_message: 'Локальний завантажувач YouTube недоступний для прямої трансляції'
  160. },
  161. cs: {
  162. togglelinks: 'Zobrazit/Skrýt odkazy',
  163. stream: 'Stream',
  164. adaptive: 'Adaptivní',
  165. videoid: 'ID videa: ',
  166. inbrowser_adaptive_merger: 'Online nástroj pro sloučení videa a audia (FFmpeg)',
  167. dlmp4: 'Stáhnout video mp4 jedním kliknutím ve vysokém rozlišení',
  168. get_video_failed: 'Nepodařilo se nahrát informace o videu. Zkuste obnovit stránku (F5).',
  169. live_stream_disabled_message: 'Local YouTube Downloader není dostupný pro živé vysílání'
  170. }
  171. }
  172. for (const [lang, data] of Object.entries(LOCALE)) {
  173. if (lang === LANG_FALLBACK) continue
  174. for (const key of Object.keys(LOCALE[LANG_FALLBACK])) {
  175. if (!(key in data)) {
  176. data[key] = LOCALE[LANG_FALLBACK][key]
  177. }
  178. }
  179. }
  180. const findLang = l => {
  181. l = l.replace('-Hant', '') // special case for zh-Hant-TW
  182. // language resolution logic: zh-tw --(if not exists)--> zh --(if not exists)--> LANG_FALLBACK(en)
  183. l = l.toLowerCase().replace('_', '-')
  184. if (l in LOCALE) return l
  185. else if (l.length > 2) return findLang(l.split('-')[0])
  186. else return LANG_FALLBACK
  187. }
  188. const getLangCode = () => {
  189. const html = document.querySelector('html')
  190. if (html) {
  191. return html.lang
  192. } else {
  193. return navigator.language
  194. }
  195. }
  196. const $ = (s, x = document) => x.querySelector(s)
  197. const $el = (tag, opts) => {
  198. const el = document.createElement(tag)
  199. Object.assign(el, opts)
  200. return el
  201. }
  202. const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  203. const parseDecsig = data => {
  204. try {
  205. if (data.startsWith('var script')) {
  206. // they inject the script via script tag
  207. const obj = {}
  208. const document = {
  209. createElement: () => obj,
  210. head: { appendChild: () => {} }
  211. }
  212. eval(data)
  213. data = obj.innerHTML
  214. }
  215. const fnnameresult = /=([a-zA-Z0-9\$]+?)\(decodeURIComponent/.exec(data)
  216. const fnname = fnnameresult[1]
  217. const _argnamefnbodyresult = new RegExp(escapeRegExp(fnname) + '=function\\((.+?)\\){((.+)=\\2.+?)}').exec(
  218. data
  219. )
  220. const [_, argname, fnbody] = _argnamefnbodyresult
  221. const helpernameresult = /;(.+?)\..+?\(/.exec(fnbody)
  222. const helpername = helpernameresult[1]
  223. const helperresult = new RegExp('var ' + escapeRegExp(helpername) + '={[\\s\\S]+?};').exec(data)
  224. const helper = helperresult[0]
  225. logger.log(`parsedecsig result: %s=>{%s\n%s}`, argname, helper, fnbody)
  226. return new Function([argname], helper + '\n' + fnbody)
  227. } catch (e) {
  228. logger.error('parsedecsig error: %o', e)
  229. logger.info('script content: %s', data)
  230. logger.info(
  231. 'If you encounter this error, please copy the full "script content" to https://pastebin.com/ for me.'
  232. )
  233. }
  234. }
  235. const parseQuery = s => [...new URLSearchParams(s).entries()].reduce((acc, [k, v]) => ((acc[k] = v), acc), {})
  236. const parseResponse = (id, playerResponse, decsig) => {
  237. logger.log(`video %s playerResponse: %o`, id, playerResponse)
  238. let stream = []
  239. if (playerResponse.streamingData.formats) {
  240. stream = playerResponse.streamingData.formats.map(x =>
  241. Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
  242. )
  243. logger.log(`video %s stream: %o`, id, stream)
  244. for (const obj of stream) {
  245. if (obj.s) {
  246. obj.s = decsig(obj.s)
  247. obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`
  248. }
  249. }
  250. }
  251.  
  252. let adaptive = []
  253. if (playerResponse.streamingData.adaptiveFormats) {
  254. adaptive = playerResponse.streamingData.adaptiveFormats.map(x =>
  255. Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
  256. )
  257. logger.log(`video %s adaptive: %o`, id, adaptive)
  258. for (const obj of adaptive) {
  259. if (obj.s) {
  260. obj.s = decsig(obj.s)
  261. obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`
  262. }
  263. }
  264. }
  265. logger.log(`video %s result: %o`, id, { stream, adaptive })
  266. return { stream, adaptive, details: playerResponse.videoDetails, playerResponse }
  267. }
  268.  
  269. // video downloader
  270. const xhrDownloadUint8Array = async ({ url, contentLength }, progressCb) => {
  271. if (typeof contentLength === 'string') contentLength = parseInt(contentLength)
  272. progressCb({
  273. loaded: 0,
  274. total: contentLength,
  275. speed: 0
  276. })
  277. const chunkSize = 65536
  278. const getBuffer = (start, end) =>
  279. fetch(url + `&range=${start}-${end ? end - 1 : ''}`).then(r=>r.arrayBuffer())
  280. const data = new Uint8Array(contentLength)
  281. let downloaded = 0
  282. const queue = new pQueue.default({ concurrency: 6 })
  283. const startTime = Date.now()
  284. const ps = []
  285. for (let start = 0; start < contentLength; start += chunkSize) {
  286. const exceeded = start + chunkSize > contentLength
  287. const curChunkSize = exceeded ? contentLength - start : chunkSize
  288. const end = exceeded ? null : start + chunkSize
  289. const p = queue.add(() => {
  290. console.log('dl start', url, start, end)
  291. return getBuffer(start, end)
  292. .then(buf => {
  293. console.log('dl done', url, start, end)
  294. downloaded += curChunkSize
  295. data.set(new Uint8Array(buf), start)
  296. const ds = (Date.now() - startTime + 1) / 1000
  297. progressCb({
  298. loaded: downloaded,
  299. total: contentLength,
  300. speed: downloaded / ds
  301. })
  302. })
  303. .catch(err => {
  304. queue.clear()
  305. alert('Download error')
  306. })
  307. })
  308. ps.push(p)
  309. }
  310. await Promise.all(ps)
  311. return data
  312. }
  313.  
  314. const ffWorker = FFmpeg.createWorker({
  315. logger: DEBUG ? m => logger.log(m.message) : () => {}
  316. })
  317. let ffWorkerLoaded = false
  318. const mergeVideo = async (video, audio) => {
  319. if (!ffWorkerLoaded) await ffWorker.load()
  320. await ffWorker.write('video.mp4', video)
  321. await ffWorker.write('audio.mp4', audio)
  322. await ffWorker.run('-i video.mp4 -i audio.mp4 -c copy output.mp4', {
  323. input: ['video.mp4', 'audio.mp4'],
  324. output: 'output.mp4'
  325. })
  326. const { data } = await ffWorker.read('output.mp4')
  327. await ffWorker.remove('output.mp4')
  328. return data
  329. }
  330. const triggerDownload = (url, filename) => {
  331. const a = document.createElement('a')
  332. a.href = url
  333. a.download = filename
  334. document.body.appendChild(a)
  335. a.click()
  336. a.remove()
  337. }
  338. const dlModalTemplate = `
  339. <div style="width: 100%; height: 100%;">
  340. <div v-if="merging" style="height: 100%; width: 100%; display: flex; justify-content: center; align-items: center; font-size: 24px;">Merging video, please wait...</div>
  341. <div v-else style="height: 100%; width: 100%; display: flex; flex-direction: column;">
  342. <div style="flex: 1; margin: 10px;">
  343. <p style="font-size: 24px;">Video</p>
  344. <progress style="width: 100%;" :value="video.progress" min="0" max="100"></progress>
  345. <div style="display: flex; justify-content: space-between;">
  346. <span>{{video.speed}} kB/s</span>
  347. <span>{{video.loaded}}/{{video.total}} MB</span>
  348. </div>
  349. </div>
  350. <div style="flex: 1; margin: 10px;">
  351. <p style="font-size: 24px;">Audio</p>
  352. <progress style="width: 100%;" :value="audio.progress" min="0" max="100"></progress>
  353. <div style="display: flex; justify-content: space-between;">
  354. <span>{{audio.speed}} kB/s</span>
  355. <span>{{audio.loaded}}/{{audio.total}} MB</span>
  356. </div>
  357. </div>
  358. </div>
  359. </div>
  360. `
  361. function openDownloadModel(adaptive, title) {
  362. const win = open(
  363. '',
  364. 'Video Download',
  365. `toolbar=no,height=${screen.height / 2},width=${screen.width / 2},left=${screenLeft},top=${screenTop}`
  366. )
  367. const div = win.document.createElement('div')
  368. win.document.body.appendChild(div)
  369. win.document.title = `Downloading "${title}"`
  370. const dlModalApp = new Vue({
  371. template: dlModalTemplate,
  372. data() {
  373. return {
  374. video: {
  375. progress: 0,
  376. total: 0,
  377. loaded: 0,
  378. speed: 0
  379. },
  380. audio: {
  381. progress: 0,
  382. total: 0,
  383. loaded: 0,
  384. speed: 0
  385. },
  386. merging: false
  387. }
  388. },
  389. methods: {
  390. async start(adaptive, title) {
  391. win.onbeforeunload = () => true
  392. // YouTube's default order is descending by video quality
  393. const videoObj = adaptive
  394. .filter(x => x.mimeType.includes('video/mp4') || x.mimeType.includes('video/webm'))
  395. .map(v => {
  396. const [_, quality, fps] = /(\d+)p(\d*)/.exec(v.qualityLabel)
  397. v.qualityNum = parseInt(quality)
  398. v.fps = fps ? parseInt(fps) : 30
  399. return v
  400. })
  401. .sort((a, b) => {
  402. if (a.qualityNum === b.qualityNum) return b.fps - a.fps // ex: 30-60=-30, then a will be put before b
  403. return b.qualityNum - a.qualityNum
  404. })[0]
  405. const audioObj = adaptive.find(x => x.mimeType.includes('audio/mp4'))
  406. const vPromise = xhrDownloadUint8Array(videoObj, e => {
  407. this.video.progress = (e.loaded / e.total) * 100
  408. this.video.loaded = (e.loaded / 1024 / 1024).toFixed(2)
  409. this.video.total = (e.total / 1024 / 1024).toFixed(2)
  410. this.video.speed = (e.speed / 1024).toFixed(2)
  411. })
  412. const aPromise = xhrDownloadUint8Array(audioObj, e => {
  413. this.audio.progress = (e.loaded / e.total) * 100
  414. this.audio.loaded = (e.loaded / 1024 / 1024).toFixed(2)
  415. this.audio.total = (e.total / 1024 / 1024).toFixed(2)
  416. this.audio.speed = (e.speed / 1024).toFixed(2)
  417. })
  418. const [varr, aarr] = await Promise.all([vPromise, aPromise])
  419. this.merging = true
  420. win.onunload = () => {
  421. // trigger download when user close it
  422. const bvurl = URL.createObjectURL(new Blob([varr]))
  423. const baurl = URL.createObjectURL(new Blob([aarr]))
  424. triggerDownload(bvurl, title + '-videoonly.mp4')
  425. triggerDownload(baurl, title + '-audioonly.mp4')
  426. }
  427. const result = await Promise.race([mergeVideo(varr, aarr), sleep(1000 * 25).then(() => null)])
  428. if (!result) {
  429. alert('An error has occurred when merging video')
  430. const bvurl = URL.createObjectURL(new Blob([varr]))
  431. const baurl = URL.createObjectURL(new Blob([aarr]))
  432. triggerDownload(bvurl, title + '-videoonly.mp4')
  433. triggerDownload(baurl, title + '-audioonly.mp4')
  434. return this.close()
  435. }
  436. this.merging = false
  437. const url = URL.createObjectURL(new Blob([result]))
  438. triggerDownload(url, title + '.mp4')
  439. win.onbeforeunload = null
  440. win.onunload = null
  441. win.close()
  442. }
  443. }
  444. }).$mount(div)
  445. dlModalApp.start(adaptive, title)
  446. }
  447.  
  448. const template = `
  449. <div class="box" :class="{'dark':dark}">
  450. <template v-if="!isLiveStream">
  451. <div v-if="adaptive.length" class="of-h t-center c-pointer lh-20">
  452. <a class="fs-14px" @click="dlmp4" v-text="strings.dlmp4"></a>
  453. </div>
  454. <div @click="hide=!hide" class="box-toggle div-a t-center fs-14px c-pointer lh-20" v-text="strings.togglelinks"></div>
  455. <div :class="{'hide':hide}">
  456. <div class="t-center fs-14px" v-text="strings.videoid+id"></div>
  457. <div class="d-flex">
  458. <div class="f-1 of-h">
  459. <div class="t-center fs-14px" v-text="strings.stream"></div>
  460. <a class="ytdl-link-btn fs-14px" target="_blank" v-for="vid in stream" :href="vid.url" :title="vid.type" v-text="formatStreamText(vid)"></a>
  461. </div>
  462. <div class="f-1 of-h">
  463. <div class="t-center fs-14px" v-text="strings.adaptive"></div>
  464. <a class="ytdl-link-btn fs-14px" target="_blank" v-for="vid in adaptive" :href="vid.url" :title="vid.type" v-text="formatAdaptiveText(vid)"></a>
  465. </div>
  466. </div>
  467. <div class="of-h t-center">
  468. <a class="fs-14px" href="https://maple3142.github.io/mergemp4/" target="_blank" v-text="strings.inbrowser_adaptive_merger"></a>
  469. </div>
  470. </div>
  471. </template>
  472. <template v-else>
  473. <div class="t-center fs-14px lh-20" v-text="strings.live_stream_disabled_message"></div>
  474. </template>
  475. </div>
  476. `.slice(1)
  477. const app = new Vue({
  478. data() {
  479. return {
  480. hide: true,
  481. id: '',
  482. isLiveStream: false,
  483. stream: [],
  484. adaptive: [],
  485. details: null,
  486. dark: false,
  487. lang: findLang(getLangCode())
  488. }
  489. },
  490. computed: {
  491. strings() {
  492. return LOCALE[this.lang.toLowerCase()]
  493. }
  494. },
  495. methods: {
  496. dlmp4() {
  497. openDownloadModel(this.adaptive, this.details.title)
  498. },
  499. formatStreamText(vid) {
  500. return [vid.qualityLabel, vid.quality].filter(x => x).join(': ')
  501. },
  502. formatAdaptiveText(vid) {
  503. let str = [vid.qualityLabel, vid.mimeType].filter(x => x).join(': ')
  504. if (vid.mimeType.includes('audio')) {
  505. str += ` ${Math.round(vid.bitrate / 1000)}kbps`
  506. }
  507. return str
  508. }
  509. },
  510. template
  511. })
  512. logger.log(`default language: %s`, app.lang)
  513.  
  514. // attach element
  515. const shadowHost = $el('div')
  516. const shadow = shadowHost.attachShadow ? shadowHost.attachShadow({ mode: 'closed' }) : shadowHost // no shadow dom
  517. logger.log('shadowHost: %o', shadowHost)
  518. const container = $el('div')
  519. shadow.appendChild(container)
  520. app.$mount(container)
  521.  
  522. if (DEBUG && typeof unsafeWindow !== 'undefined') {
  523. // expose some functions for debugging
  524. unsafeWindow.$app = app
  525. unsafeWindow.parseQuery = parseQuery
  526. unsafeWindow.parseDecsig = parseDecsig
  527. unsafeWindow.parseResponse = parseResponse
  528. }
  529. const load = async playerResponse => {
  530. try {
  531. const basejs =
  532. (typeof ytplayer !== 'undefined' && 'config' in ytplayer && ytplayer.config.assets
  533. ? 'https://' + location.host + ytplayer.config.assets.js
  534. : 'web_player_context_config' in ytplayer
  535. ? 'https://' + location.host + ytplayer.web_player_context_config.jsUrl
  536. : null) || $('script[src$="base.js"]').src
  537. const decsig = await xf.get(basejs).text(parseDecsig)
  538. const id = parseQuery(location.search).v
  539. const data = parseResponse(id, playerResponse, decsig)
  540. logger.log('video loaded: %s', id)
  541. app.isLiveStream = data.playerResponse.playabilityStatus.liveStreamability != null
  542. app.id = id
  543. app.stream = data.stream
  544. app.adaptive = data.adaptive
  545. app.details = data.details
  546.  
  547. const actLang = getLangCode()
  548. if (actLang != null) {
  549. const lang = findLang(actLang)
  550. logger.log('youtube ui lang: %s', actLang)
  551. logger.log('ytdl lang:', lang)
  552. app.lang = lang
  553. }
  554. } catch (err) {
  555. alert(app.strings.get_video_failed)
  556. logger.error('load', err)
  557. }
  558. }
  559.  
  560. // hook fetch response
  561. const ff = fetch
  562. unsafeWindow.fetch = (...args) => {
  563. if (args[0] instanceof Request) {
  564. return ff(...args).then(resp => {
  565. if (resp.url.includes('player')) {
  566. resp.clone().json().then(load)
  567. }
  568. return resp
  569. })
  570. }
  571. return ff(...args)
  572. }
  573.  
  574. // attach element
  575. setInterval(() => {
  576. const el =
  577. $('#info-contents') ||
  578. $('#watch-header') ||
  579. $('.page-container:not([hidden]) ytm-item-section-renderer>lazy-list')
  580. if (el && !el.contains(shadowHost)) {
  581. el.appendChild(shadowHost)
  582. }
  583. }, 100)
  584.  
  585. // init
  586. unsafeWindow.addEventListener('load', () => {
  587. const firstResp = unsafeWindow?.ytplayer?.config?.args?.raw_player_response
  588. if (firstResp) {
  589. load(firstResp)
  590. }
  591. })
  592.  
  593. // listen to dark mode toggle
  594. const $html = $('html')
  595. new MutationObserver(() => {
  596. app.dark = $html.getAttribute('dark') !== null
  597. }).observe($html, { attributes: true })
  598. app.dark = $html.getAttribute('dark') !== null
  599.  
  600. const css = `
  601. .hide{
  602. display: none;
  603. }
  604. .t-center{
  605. text-align: center;
  606. }
  607. .d-flex{
  608. display: flex;
  609. }
  610. .f-1{
  611. flex: 1;
  612. }
  613. .fs-14px{
  614. font-size: 14px;
  615. }
  616. .of-h{
  617. overflow: hidden;
  618. }
  619. .box{
  620. padding-top: .5em;
  621. padding-bottom: .5em;
  622. border-bottom: 1px solid var(--yt-border-color);
  623. font-family: Arial;
  624. }
  625. .box-toggle{
  626. margin: 3px;
  627. user-select: none;
  628. -moz-user-select: -moz-none;
  629. }
  630. .ytdl-link-btn{
  631. display: block;
  632. border: 1px solid !important;
  633. border-radius: 3px;
  634. text-decoration: none !important;
  635. outline: 0;
  636. text-align: center;
  637. padding: 2px;
  638. margin: 5px;
  639. color: black;
  640. }
  641. a, .div-a{
  642. text-decoration: none;
  643. color: var(--yt-button-color, inherit);
  644. }
  645. a:hover, .div-a:hover{
  646. color: var(--yt-spec-call-to-action, blue);
  647. }
  648. .box.dark{
  649. color: var(--yt-endpoint-color, var(--yt-spec-text-primary));
  650. }
  651. .box.dark .ytdl-link-btn{
  652. color: var(--yt-endpoint-color, var(--yt-spec-text-primary));
  653. }
  654. .box.dark .ytdl-link-btn:hover{
  655. color: rgba(200, 200, 255, 0.8);
  656. }
  657. .box.dark .box-toggle:hover{
  658. color: rgba(200, 200, 255, 0.8);
  659. }
  660. .c-pointer{
  661. cursor: pointer;
  662. }
  663. .lh-20{
  664. line-height: 20px;
  665. }
  666. `
  667. shadow.appendChild($el('style', { textContent: css }))
  668. const css2 = `
  669. /* https://greasyfork.org/zh-TW/scripts/369400-local-youtube-downloader/discussions/95744 */
  670. #meta-contents,
  671. #info-contents{
  672. display: contents !important;
  673. }
  674.  
  675. ytd-watch-metadata.style-scope {
  676. display: none !important;
  677. }
  678. `
  679. document.body.appendChild($el('style', { textContent: css2 }))
  680. })()
  681.  
  682.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement