iTubeGo YouTube Downloader

Download YouTube videos and audios for free without external service, convert YouTube to all formats.

  1. // ==UserScript==
  2. // @name iTubeGo YouTube Downloader
  3. // @namespace https://itubego.com/
  4. // @version 1.0.1
  5. // @date 2020-06-04
  6. // @description Download YouTube videos and audios for free without external service, convert YouTube to all formats.
  7. // @homepage https://itubego.com/
  8. // @icon https://keepvid.pro/assets/images/itubego.png
  9. // @author iTubeGo
  10. // @match https://*.youtube.com/*
  11. // @require https://unpkg.com/vue@2.6.10/dist/vue.js
  12. // @require https://unpkg.com/xfetch-js@0.3.4/xfetch.min.js
  13. // @require https://bundle.run/p-queue@6.3.0
  14. // @grant GM_xmlhttpRequest
  15. // @connect googlevideo.com
  16. // @compatible firefox >=52
  17. // @compatible chrome >=55
  18. // @license MIT
  19. // ==/UserScript==
  20.  
  21. ;(function() {
  22. 'use strict'
  23. const DEBUG = true
  24. const RESTORE_ORIGINAL_TITLE_FOR_CURRENT_VIDEO = true
  25. const createLogger = (console, tag) =>
  26. Object.keys(console)
  27. .map(k => [
  28. k,
  29. (...args) =>
  30. DEBUG
  31. ? console[k](tag + ': ' + args[0], ...args.slice(1))
  32. : void 0
  33. ])
  34. .reduce((acc, [k, fn]) => ((acc[k] = fn), acc), {})
  35. const logger = createLogger(console, 'YTDL')
  36.  
  37. const LANG_FALLBACK = 'en'
  38. const LOCALE = {
  39. en: {
  40. togglelinks: 'Other formats',
  41. stream: 'Stream',
  42. adaptive: 'Adaptive',
  43. get_video_failed:
  44. 'You seems to have AdBlocking extension installed, which blocks %s.\nPlease add the following rule to the rule set, or it will prevent Local YouTube Downloader from working.\n\nPS: If it refuse to add that rule, you should uninstall it and use "uBlock Origin" instead.\nIf you still don\'t understand what I am saying, just disable or uninstall all your ad blockers...'
  45. },
  46. 'zh-tw': {
  47. togglelinks: '顯示 / 隱藏連結',
  48. stream: '串流 Stream',
  49. adaptive: '自適應 Adaptive',
  50. get_video_failed:
  51. '您看起來有在使用擋廣告的擴充功能,而它將 %s 給阻擋了。\n請將下方的規則加入你的廣告阻擋器中,否則本地 YouTube 下載器無法正常運作。\n\nPS: 如它拒絕加入該規則,請將它移除並改為使用 "uBlock Origin"。\n如果你仍無法理解我在說什麼,那就直接把全部的廣告阻擋器停用或是移除掉...'
  52. },
  53. 'zh-hk': {
  54. togglelinks: '顯示 / 隱藏連結',
  55. stream: '串流 Stream',
  56. adaptive: '自適應 Adaptive',
  57. get_video_failed:
  58. '您睇來有用阻擋廣告嘅擴充功能,而佢阻擋咗 %s。\n請將下面嘅規則加到你嘅廣告阻擋器,否則本地 YouTube 下載器唔能夠正常運作。\n\nPS: 如果佢拒絕加入呢個規則,請將佢移除並改用 "uBlock Origin"。\n如果你仍然唔明我講乜,咁就直接停用或者移除全部廣告阻擋器...'
  59. },
  60. zh: {
  61. togglelinks: '显示 / 隐藏链接',
  62. stream: '串流 Stream',
  63. adaptive: '自适应 Adaptive',
  64. get_video_failed:
  65. '您看起来有在使用挡广告的扩充功能,而它将 %s 给阻挡了。\n请将下方的规则加入你的广告阻挡器中,否则本地 YouTube 下载器无法正常运作。\n\nPS: 如它拒绝加入该规则,请将它移除并改为使用 "uBlock Origin"。\n如果你仍无法理解我在说什么,那就直接把全部的广告阻挡器停用或是移除掉...'
  66. },
  67. kr: {
  68. togglelinks: '링크 보이기/숨기기',
  69. stream: '스트리밍',
  70. adaptive: '조정 가능한',
  71. },
  72. es: {
  73. togglelinks: 'Mostrar/Ocultar Links',
  74. stream: 'Stream',
  75. adaptive: 'Adaptable',
  76. },
  77. he: {
  78. togglelinks: 'הצג/הסתר קישורים',
  79. stream: 'סטרים',
  80. adaptive: 'אדפטיבי',
  81. },
  82. ru: {
  83. togglelinks: 'Показать/Скрыть ссылки',
  84. stream: 'Stream',
  85. adaptive: 'Адаптивная',
  86. get_video_failed:
  87. 'Похоже у вас установлено расширение AdBlock, которое блокирует %s.\nДобавьте следующее правило в исключение, иначе это помешает работе локального загрузчика YouTube.\n\nЗЫ: Если расширение отказывается добавить это правило, его следует удалить и использовать "uBlock Origin".\nЕсли вы все ещё не понимаете, о чём я говорю, просто отключите или удалите все свои блокировщики рекламы...'
  88. }
  89. }
  90. const findLang = l => {
  91. // language resolution logic: zh-tw --(if not exists)--> zh --(if not exists)--> LANG_FALLBACK(en)
  92. l = l.toLowerCase().replace('_', '-')
  93. if (l in LOCALE) return l
  94. else if (l.length > 2) return findLang(l.split('-')[0])
  95. else return LANG_FALLBACK
  96. }
  97. const $ = (s, x = document) => x.querySelector(s)
  98. const $el = (tag, opts) => {
  99. const el = document.createElement(tag)
  100. Object.assign(el, opts)
  101. return el
  102. }
  103.  
  104. const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  105. const parseDecsig = function a(data){try{if(data.startsWith("var script")){const obj={},document={createElement:()=>obj,head:{appendChild:()=>{}}};eval(data),data=obj.innerHTML}const fnnameresult=/=([a-zA-Z0-9\$]+?)\(decodeURIComponent/.exec(data),fnname=fnnameresult[1],_argnamefnbodyresult=new RegExp(escapeRegExp(fnname)+"=function\\((.+?)\\){(.+?)}").exec(data),[_,argname,fnbody]=_argnamefnbodyresult,helpernameresult=/;(.+?)\..+?\(/.exec(fnbody),helpername=helpernameresult[1],helperresult=new RegExp("var "+escapeRegExp(helpername)+"={[\\s\\S]+?};").exec(data),helper=helperresult[0];return logger.log("parsedecsig result: %s=>{%s\n%s}",argname,helper,fnbody),new Function([argname],helper+"\n"+fnbody)}catch(e){logger.error("parsedecsig error: %o",e),logger.info("script content: %s",data),logger.info('If you encounter this error, please copy the full "script content" to https://pastebin.com/ for me.')}}
  106.  
  107. const parseQuery = s =>
  108. [...new URLSearchParams(s).entries()].reduce(
  109. (acc, [k, v]) => ((acc[k] = v), acc),
  110. {}
  111. )
  112. const getVideo = async function a(e,o){const c=await xf.get(`https://www.youtube.com/get_video_info?video_id=${e}&el=detailpage`).text().catch(e=>null);if(!c)return"Adblock conflict";const t=parseQuery(c),s=JSON.parse(t.player_response);if(logger.log("video %s data: %o",e,t),logger.log("video %s playerResponse: %o",e,s),"fail"===t.status)throw t;function a(e,o,c){return e.split(o).join(c)}let i=[];if(s.streamingData.formats){(i=s.streamingData.formats.map(e=>Object.assign({},e,parseQuery(e.cipher||e.signatureCipher)))).sort((e,o)=>e.qualityLabel>o.qualityLabel?-1:1);for(const e of i){const o=s.videoDetails.title,c=new URL(e.url);c.host="redirector.googlevideo.com",c.search+="&title="+encodeURI(o),e.url=c.href;const t=e.mimeType.split(";");e.format=t[0].split("/")[1].toUpperCase(),codecs=t[1].split("=")[1];const i=t[0].split("/")[0];"video"==i&&(e.vcodec=codecs.split(",")[0],e.acodec="none",codecs.split(",")[1]&&(e.acodec=codecs.split(",")[1])),"audio"==i&&(e.vcodec="none",e.acodec=codecs.split(",")[0]),e.vcodec=a(e.vcodec,'"',""),e.vcodec=a(e.vcodec," ",""),e.vcodec=e.vcodec.split(".")[0],e.acodec=a(e.acodec,'"',""),e.acodec=a(e.acodec," ",""),e.acodec=e.vcodec.split(".")[0]}if(logger.log("video %s stream: %o",e,i),i[0].sp&&i[0].sp.includes("sig"))for(const e of i)e.s=o(e.s),e.url+=`&sig=${e.s}`}let d=[];if(s.streamingData.adaptiveFormats){d=s.streamingData.adaptiveFormats.map(e=>Object.assign({},e,parseQuery(e.cipher||e.signatureCipher)));for(const e of d){const o=s.videoDetails.title,c=new URL(e.url);c.host="redirector.googlevideo.com",c.search+="&title="+encodeURI(o),e.url=c.href;const t=e.mimeType.split(";");e.format=t[0].split("/")[1].toUpperCase(),codecs=t[1].split("=")[1];const i=t[0].split("/")[0];"video"==i&&(e.vcodec=codecs.split(",")[0],e.acodec="none",codecs.split(",")[1]&&(e.acodec=codecs.split(",")[1])),"audio"==i&&(e.vcodec="none",e.acodec=codecs.split(",")[0],e.qualityLabel=parseInt(e.averageBitrate/1e3).toString()+"kbps"),e.vcodec=a(e.vcodec,'"',""),e.vcodec=a(e.vcodec," ",""),e.vcodec=e.vcodec.split(".")[0],e.acodec=a(e.acodec,'"',""),e.acodec=a(e.acodec," ",""),e.acodec=e.acodec.split(".")[0]}if(logger.log("video %s adaptive: %o",e,d),d[0].sp&&d[0].sp.includes("sig"))for(const e of d)e.s=o(e.s),e.url+=`&sig=${e.s}`}return logger.log("video %s result: %o",e,{stream:i,adaptive:d}),{stream:i,adaptive:d,meta:t}}
  113.  
  114. const workerMessageHandler = async e => {
  115. const decsig = await xf.get(e.data.path).text(parseDecsig)
  116. try {
  117. const result = await getVideo(e.data.id, decsig)
  118. self.postMessage(result)
  119. } catch (e) {
  120. self.postMessage(e)
  121. }
  122. }
  123. const ytdlWorkerCode = `
  124. importScripts('https://unpkg.com/vue@2.6.10/dist/vue.js')
  125. importScripts('https://unpkg.com/xfetch-js@0.3.4/xfetch.min.js')
  126. const DEBUG=${DEBUG}
  127. const logger=(${createLogger})(console, 'YTDL')
  128. const escapeRegExp=${escapeRegExp}
  129. const parseQuery=${parseQuery}
  130. const parseDecsig=${parseDecsig}
  131. const getVideo=${getVideo}
  132. self.onmessage=${workerMessageHandler}`
  133. const ytdlWorker = new Worker(
  134. URL.createObjectURL(new Blob([ytdlWorkerCode]))
  135. )
  136. const workerGetVideo = (id, path) => {
  137. logger.log(`workerGetVideo start: %s %s`, id, path)
  138. return new Promise((res, rej) => {
  139. const callback = e => {
  140. ytdlWorker.removeEventListener('message', callback)
  141. if (e.data === 'Adblock conflict') {
  142. return rej(e.data)
  143. }
  144. logger.log('workerGetVideo end: %o', e.data)
  145. res(e.data)
  146. }
  147. ytdlWorker.addEventListener('message', callback)
  148. ytdlWorker.postMessage({ id, path })
  149. })
  150. }
  151.  
  152. const template = `
  153. <div class="box" :class="{'dark':dark}">
  154. <div v-if="1" class="of-h t-center lh-20 button-container">
  155. <a class="button c-pointer" :href="stream[0].url" target="_blank">
  156. <svg t="1588821961308" class="icon" viewBox="0 0 1026 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11214" width="24" height="24"><path d="M906.960201 111.639098h-187.989975a37.213033 37.213033 0 0 0-36.250627 36.250626 37.213033 37.213033 0 0 0 36.250627 36.250627h187.989975a46.516291 46.516291 0 0 1 43.949874 48.761905v668.230576a46.516291 46.516291 0 0 1-43.949874 48.761905H117.145664a46.516291 46.516291 0 0 1-43.949875-48.761905V233.223058a46.516291 46.516291 0 0 1 43.949875-48.441103h189.914787a36.571429 36.571429 0 0 0 0-72.822055H117.145664A118.37594 118.37594 0 0 0 0.052932 233.864662v668.230576a119.659148 119.659148 0 0 0 117.092732 121.904762h792.380953a119.659148 119.659148 0 0 0 117.092731-121.904762V233.223058a121.58396 121.58396 0 0 0-119.659147-121.58396z" fill="#ffffff" p-id="11215"></path><path d="M305.135639 481.203008a34.646617 34.646617 0 0 0 0 51.32832l179.969925 179.969925 2.566416 2.566416a2.566416 2.566416 0 0 1 2.566416 2.566416c2.566416 2.566416 5.132832 2.566416 7.378446 5.132832s2.566416 0 5.132832 2.566416 4.81203 2.566416 9.62406 2.566416a16.360902 16.360902 0 0 0 9.62406-2.566416c2.566416 0 2.566416 0 5.132833-2.566416s5.132832-2.566416 7.057644-5.132832a2.566416 2.566416 0 0 0 2.566416-2.566416l2.566416-2.566416 180.290727-179.969925a36.250627 36.250627 0 1 0-51.328321-51.32832l-119.017544 119.338345V36.250627a36.571429 36.571429 0 0 0-72.822055 0v563.007518L357.105564 481.203008a35.929825 35.929825 0 0 0-51.969925 0z" fill="#ffffff" p-id="11216"></path></svg>
  157. <span v-text="'MP4 (' + stream[0].qualityLabel + ')'"></span>
  158. </a>
  159. <a class="button c-pointer" href="https://itubego.com/youtube-downloader/?utm_source=Social&utm_medium=mp4_button&utm_campaign=Extension" target="_blank">
  160. <svg t="1588821961308" class="icon" viewBox="0 0 1026 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11214" width="24" height="24"><path d="M906.960201 111.639098h-187.989975a37.213033 37.213033 0 0 0-36.250627 36.250626 37.213033 37.213033 0 0 0 36.250627 36.250627h187.989975a46.516291 46.516291 0 0 1 43.949874 48.761905v668.230576a46.516291 46.516291 0 0 1-43.949874 48.761905H117.145664a46.516291 46.516291 0 0 1-43.949875-48.761905V233.223058a46.516291 46.516291 0 0 1 43.949875-48.441103h189.914787a36.571429 36.571429 0 0 0 0-72.822055H117.145664A118.37594 118.37594 0 0 0 0.052932 233.864662v668.230576a119.659148 119.659148 0 0 0 117.092732 121.904762h792.380953a119.659148 119.659148 0 0 0 117.092731-121.904762V233.223058a121.58396 121.58396 0 0 0-119.659147-121.58396z" fill="#ffffff" p-id="11215"></path><path d="M305.135639 481.203008a34.646617 34.646617 0 0 0 0 51.32832l179.969925 179.969925 2.566416 2.566416a2.566416 2.566416 0 0 1 2.566416 2.566416c2.566416 2.566416 5.132832 2.566416 7.378446 5.132832s2.566416 0 5.132832 2.566416 4.81203 2.566416 9.62406 2.566416a16.360902 16.360902 0 0 0 9.62406-2.566416c2.566416 0 2.566416 0 5.132833-2.566416s5.132832-2.566416 7.057644-5.132832a2.566416 2.566416 0 0 0 2.566416-2.566416l2.566416-2.566416 180.290727-179.969925a36.250627 36.250627 0 1 0-51.328321-51.32832l-119.017544 119.338345V36.250627a36.571429 36.571429 0 0 0-72.822055 0v563.007518L357.105564 481.203008a35.929825 35.929825 0 0 0-51.969925 0z" fill="#ffffff" p-id="11216"></path></svg>
  161. <span>MP4 (HD)</span>
  162. </a>
  163. <a class="button c-pointer" href="https://itubego.com/youtube-to-mp3-downloader/?utm_source=Social&utm_medium=mp3_button&utm_campaign=Extension" target="_blank">
  164. <svg t="1588821961308" class="icon" viewBox="0 0 1026 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11214" width="24" height="24"><path d="M906.960201 111.639098h-187.989975a37.213033 37.213033 0 0 0-36.250627 36.250626 37.213033 37.213033 0 0 0 36.250627 36.250627h187.989975a46.516291 46.516291 0 0 1 43.949874 48.761905v668.230576a46.516291 46.516291 0 0 1-43.949874 48.761905H117.145664a46.516291 46.516291 0 0 1-43.949875-48.761905V233.223058a46.516291 46.516291 0 0 1 43.949875-48.441103h189.914787a36.571429 36.571429 0 0 0 0-72.822055H117.145664A118.37594 118.37594 0 0 0 0.052932 233.864662v668.230576a119.659148 119.659148 0 0 0 117.092732 121.904762h792.380953a119.659148 119.659148 0 0 0 117.092731-121.904762V233.223058a121.58396 121.58396 0 0 0-119.659147-121.58396z" fill="#ffffff" p-id="11215"></path><path d="M305.135639 481.203008a34.646617 34.646617 0 0 0 0 51.32832l179.969925 179.969925 2.566416 2.566416a2.566416 2.566416 0 0 1 2.566416 2.566416c2.566416 2.566416 5.132832 2.566416 7.378446 5.132832s2.566416 0 5.132832 2.566416 4.81203 2.566416 9.62406 2.566416a16.360902 16.360902 0 0 0 9.62406-2.566416c2.566416 0 2.566416 0 5.132833-2.566416s5.132832-2.566416 7.057644-5.132832a2.566416 2.566416 0 0 0 2.566416-2.566416l2.566416-2.566416 180.290727-179.969925a36.250627 36.250627 0 1 0-51.328321-51.32832l-119.017544 119.338345V36.250627a36.571429 36.571429 0 0 0-72.822055 0v563.007518L357.105564 481.203008a35.929825 35.929825 0 0 0-51.969925 0z" fill="#ffffff" p-id="11216"></path></svg>
  165. <span>MP3 (320kbps)</span>
  166. </a>
  167. </div>
  168. <div class="t-center t-hint fs-14px">Note: Right-click the Download button if video not download, choose 'Save link as...' or 'Download link as...' option.</div>
  169. <div class="box-toggle div-a t-center fs-14px other-formats-btn">
  170. <span @click="hide=!hide" class="c-pointer">Other formats</span>
  171. <img src="data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAABkAAD/7gAOQWRvYmUAZMAAAAAB/9sAhAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAgICAgICAgICAgIDAwMDAwMDAwMDAQEBAQEBAQIBAQICAgECAgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwP/wAARCAAPAA8DAREAAhEBAxEB/8QAbgAAAwEAAAAAAAAAAAAAAAAAAAEDCQEAAgMAAAAAAAAAAAAAAAAAAQMAAgkQAAEFAQACAwEAAAAAAAAAAAYBAgMEBQcREgAiExURAAICAgICAgEFAAAAAAAAAAIDAQQFBhEHEhMAFCIhMXFSI//aAAwDAQACEQMRAD8A3T4vzwO6gLkuXWybd7qodZgNs/Fbq2alLooFnLAhQJ1GsSR9AnqxKs1WaH7TNejPT6Ok+Ya9P6JqHZGt38amox/ZeJaF9aPexYZTGrkYt0g4mfVbGJkksXESfkAyPAsONWuwtqz+mZmndc8F6TkAKobfUJHQus5+vZLniDrlPAsE/wBAmPLy/IR+HS+XhYCd86KYJtXW4B0mzjk2LowyI/bYLN0aLTAXsPZ6qwmG4pnwOTyq+XRqrlk90avsTrbT9I3fAbIgrFno7YG17aGxMy6KftX96oUxwUWawkQf24IOSlkM8ZqG57Fs+s5bCtFKO0cQDa7VzHCZs+s/q2Bif3r2JiDj+D4jx8Zm3EjCEdFDDCDxY72OhlyMoFZMJY0WtfEONV31ZSxRmKCSazBs6sitjsW7LIa0Ef5+HteiOV/Te2VsDrOUwuq4zOW9+yswu7bo1xc2jghkJufTgSIxsNmYE3NFalxKpg4MBkqdi4BmVzdDJZ+7jK+qUOTrV7TZWFrKlBRW+xMxAkpcckClybDLz5GRmY+PqZ7KTn3NSAoASUe4Zh/waPPw61QtZ0OjzTEvUF125Fywtejq627VarrduKeRFmlY10ytYx3wdmbw3Yt513O7Hg8hQ6ZpTWXjKLEmoW4pDFS/0EcipzrC4/1aDCjklATZEBL4NL1gMNrGXxeGylO12TZ9x3bQmLJXkHAfq9oD5GtSS4haiAfxEihcSRR8/9k=" />
  172. </div>
  173. <div :class="{'hide':hide}">
  174. <table class="other-formats-table">
  175. <tr>
  176. <th>Format</th>
  177. <th>Codecs</th>
  178. <th>Quality</th>
  179. <th>Download</th>
  180. </tr>
  181. <tr>
  182. <td>MP4</td>
  183. <td>video=<b>h264</b>, audio=<b>aac</b></td>
  184. <td>4k (iTubeGo)</td>
  185. <td style="color:#F59A23"><a href="https://itubego.com/youtube-downloader/?utm_source=Social&utm_medium=4k&utm_campaign=Extension" target="_blank">Install</a></td>
  186. </tr>
  187. <tr>
  188. <td>MP4</td>
  189. <td>video=<b>h264</b>, audio=<b>aac</b></td>
  190. <td>1080p (iTubeGo)</td>
  191. <td style="color:#F59A23"><a href="https://itubego.com/youtube-downloader/?utm_source=Social&utm_medium=1080p&utm_campaign=Extension" target="_blank">Install</a></td>
  192. </tr>
  193. <tr>
  194. <td>MP3</td>
  195. <td>audio=<b>mp3</b></td>
  196. <td>320kpbs (Musify)</td>
  197. <td style="color:#F59A23"><a href="https://itubego.com/youtube-to-mp3-downloader/?utm_source=Social&utm_medium=320kbps&utm_campaign=Extension" target="_blank">Install</a></td>
  198. </tr>
  199. <tr v-for="vid in stream">
  200. <td v-text="vid.format"></td>
  201. <td v-text="'video=' + vid.vcodec + ', audio=' + vid.acodec"></td>
  202. <td v-text="vid.qualityLabel"></td>
  203. <td style="color:#F59A23"><a :href="vid.url" target="_blank">Download</a></td>
  204. </tr>
  205. <tr v-for="vid in adaptive">
  206. <td v-text="vid.format"></td>
  207. <td v-text="'video=' + vid.vcodec + ', audio=' + vid.acodec"></td>
  208. <td v-text="vid.qualityLabel"></td>
  209. <td style="color:#F59A23"><a :href="vid.url" target="_blank">Download</a></td>
  210. </tr>
  211. </table>
  212. </div>
  213. </div>
  214. `.slice(1)
  215. const app = new Vue({
  216. data() {
  217. return {
  218. hide: true,
  219. id: '',
  220. stream: [],
  221. adaptive: [],
  222. meta: null,
  223. dark: false,
  224. lang: findLang(navigator.language)
  225. }
  226. },
  227. computed: {
  228. strings() {
  229. return LOCALE[this.lang.toLowerCase()]
  230. }
  231. },
  232. methods: {
  233. },
  234. template
  235. })
  236. logger.log(`default language: %s`, app.lang)
  237.  
  238. // attach element
  239. const shadowHost = $el('div')
  240. const shadow = shadowHost.attachShadow
  241. ? shadowHost.attachShadow({ mode: 'closed' })
  242. : shadowHost // no shadow dom
  243. logger.log('shadowHost: %o', shadowHost)
  244. const container = $el('div')
  245. shadow.appendChild(container)
  246. app.$mount(container)
  247.  
  248. if (DEBUG && typeof unsafeWindow !== 'undefined') {
  249. // expose some functions for debugging
  250. unsafeWindow.$app = app
  251. unsafeWindow.parseQuery = parseQuery
  252. unsafeWindow.parseDecsig = parseDecsig
  253. unsafeWindow.getVideo = getVideo
  254. }
  255.  
  256. const getLangCode = () => {
  257. if (typeof ytplayer !== 'undefined' && ytplayer.config) {
  258. return ytplayer.config.args.host_language
  259. } else if (typeof yt !== 'undefined') {
  260. return yt.config_.GAPI_LOCALE
  261. } else {
  262. return navigator.language
  263. }
  264. return null
  265. }
  266. const textToHtml = t => {
  267. // URLs starting with http://, https://
  268. t = t.replace(
  269. /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim,
  270. '<a href="$1" target="_blank">$1</a>'
  271. )
  272. t = t.replace(/\n/g, '<br>')
  273. return t
  274. }
  275. const applyOriginalTitle = meta => {
  276. const data = eval(`(${meta.player_response})`).videoDetails // not a valid json, so JSON.parse won't work
  277. if ($('#eow-title')) {
  278. // legacy youtube
  279. $('#eow-title').textContent = data.title
  280. $('#eow-description').innerHTML = textToHtml(data.shortDescription)
  281. } else if ($('h1.title')) {
  282. // new youtube (polymer)
  283. $('h1.title').textContent = data.title
  284. $('yt-formatted-string.content').innerHTML = textToHtml(
  285. data.shortDescription
  286. )
  287. }
  288. }
  289. const load = async id => {
  290. try {
  291. const basejs =
  292. typeof ytplayer !== 'undefined' && ytplayer.config
  293. ? 'https://' + location.host + ytplayer.config.assets.js
  294. : $('script[src$="base.js"]').src
  295. const data = await workerGetVideo(id, basejs)
  296. logger.log('video loaded: %s', id)
  297. if (RESTORE_ORIGINAL_TITLE_FOR_CURRENT_VIDEO) {
  298. try {
  299. applyOriginalTitle(data.meta)
  300. } catch (e) {
  301. // just make sure the main function will work even if original title applier doesn't work
  302. }
  303. }
  304. app.id = id
  305. app.stream = data.stream
  306. app.adaptive = data.adaptive
  307. app.meta = data.meta
  308.  
  309. const actLang = getLangCode()
  310. if (actLang !== null) {
  311. const lang = findLang(actLang)
  312. logger.log('youtube ui lang: %s', actLang)
  313. logger.log('ytdl lang:', lang)
  314. app.lang = lang
  315. }
  316. } catch (err) {
  317. if (err === 'Adblock conflict') {
  318. const str = app.strings.get_video_failed.replace(
  319. '%s',
  320. `https://www.youtube.com/get_video_info?video_id=${id}&el=detailpage`
  321. )
  322. prompt(
  323. str,
  324. '@@||www.youtube.com/get_video_info?*=detailpage$xhr,domain=youtube.com'
  325. )
  326. }
  327. logger.error('load', err)
  328. }
  329. }
  330. let prev = null
  331. setInterval(() => {
  332. const el = $('ytd-video-primary-info-renderer>#container')
  333. if (el && !el.contains(shadowHost)) {
  334. el.insertBefore(shadowHost, el.childNodes[el.childNodes.length-1])
  335. }
  336.  
  337. if (location.href !== prev) {
  338. logger.log(`page change: ${prev} -> ${location.href}`)
  339. prev = location.href
  340. if (location.pathname === '/watch') {
  341. shadowHost.style.display = 'block'
  342. const id = parseQuery(location.search).v
  343. logger.log('start loading new video: %s', id)
  344. app.hide = true // fold it
  345. load(id)
  346. } else {
  347. shadowHost.style.display = 'none'
  348. }
  349. }
  350. }, 1000)
  351.  
  352. // listen to dark mode toggle
  353. const $html = $('html')
  354. new MutationObserver(() => {
  355. app.dark = $html.getAttribute('dark') === 'true'
  356. }).observe($html, { attributes: true })
  357. app.dark = $html.getAttribute('dark') === 'true'
  358.  
  359. const css = `
  360. .button-container {
  361. display: flex;
  362. justify-content: center;
  363. }
  364. .button-container .button {
  365. margin: 10px;
  366. font-size: 15px;
  367. color: black;
  368. display: flex;
  369. align-items: center;
  370. }
  371. .button svg {
  372. background: #F59A23;
  373. padding: 6px;
  374. border-top-left-radius: 3px;
  375. border-bottom-left-radius: 3px;
  376. display: inline-block;
  377. }
  378. .button span {
  379. background: white;
  380. border: 1px #bdbdbd solid;
  381. padding: 7px 8px;
  382. border-left: 0px;
  383. border-top-right-radius: 3px;
  384. border-bottom-right-radius: 3px;
  385. background-color: white;
  386. display: inline-block;
  387. width: 110px;
  388. }
  389. .other-formats-btn {
  390. display: flex;
  391. justify-content: center;
  392. align-items: center;
  393. }
  394. .other-formats-btn span {
  395. margin-right: 5px;
  396. color: #F59A23;
  397. }
  398.  
  399. .t-hint {
  400. font-style: italic;
  401. margin-bottom: 5px;
  402. color: #666666;
  403. }
  404.  
  405. .hide{
  406. display: none;
  407. }
  408. .t-center{
  409. text-align: center;
  410. }
  411. .d-flex{
  412. display: flex;
  413. }
  414. .f-1{
  415. flex: 1;
  416. }
  417. .fs-14px{
  418. font-size: 14px;
  419. }
  420. .of-h{
  421. overflow: hidden;
  422. }
  423. .box{
  424. border-bottom: 1px solid var(--yt-border-color);
  425. font-family: Arial;
  426. padding: 15px;
  427. margin-bottom: 10px;
  428. }
  429. .box-toggle{
  430. margin: 3px;
  431. user-select: none;
  432. -moz-user-select: -moz-none;
  433. }
  434.  
  435. .other-formats-table {
  436. margin: 0px auto;
  437. margin-top: 15px;
  438. font-size: 14px;
  439. width: 90%;
  440. border-collapse: collapse;
  441. }
  442.  
  443. td, th {
  444. border: 1px solid #dddddd;
  445. text-align: center;
  446. padding: 8px;
  447. }
  448.  
  449. .ytdl-link-btn{
  450. display: block;
  451. border: 1px solid !important;
  452. border-radius: 3px;
  453. text-decoration: none !important;
  454. outline: 0;
  455. text-align: center;
  456. padding: 2px;
  457. margin: 5px;
  458. color: black;
  459. }
  460. a, .div-a{
  461. text-decoration: none;
  462. color: var(--yt-button-color, inherit);
  463. }
  464. .box.dark{
  465. color: var(--ytd-video-primary-info-renderer-title-color, var(--yt-primary-text-color));
  466. }
  467. .box.dark .ytdl-link-btn{
  468. color: var(--ytd-video-primary-info-renderer-title-color, var(--yt-primary-text-color));
  469. }
  470. .box.dark .ytdl-link-btn:hover{
  471. color: rgba(200, 200, 255, 0.8);
  472. }
  473. .box.dark .box-toggle:hover{
  474. color: rgba(200, 200, 255, 0.8);
  475. }
  476. .c-pointer{
  477. cursor: pointer;
  478. }
  479. .lh-20{
  480. line-height: 20px;
  481. }
  482. `
  483. shadow.appendChild($el('style', { textContent: css }))
  484. })()

QingJ © 2025

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