Youtube Pro | v5

Youtube Pro is a script which allows you to return dislikes ,download youtube videos and block anoying ads powered by Return YouTube Dislike,local youtube video downloader and Youtube Pro

  1. // ==UserScript==
  2. // @name Youtube Pro | v5
  3. // @version v5
  4. // @description Youtube Pro is a script which allows you to return dislikes ,download youtube videos and block anoying ads powered by Return YouTube Dislike,local youtube video downloader and Youtube Pro
  5. // @namespace OrangeMonkey Scripts
  6. // @author devsoniexpert72,maple3142 and Anarios & JRWR
  7. // @match https://*.youtube.com/*
  8. // @require https://unpkg.com/vue@2.6.10/dist/vue.js
  9. // @require https://unpkg.com/xfetch-js@0.3.4/xfetch.min.js
  10. // @require https://unpkg.com/@ffmpeg/ffmpeg@0.6.1/dist/ffmpeg.min.js
  11. // @require https://bundle.run/p-queue@6.3.0
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_info
  14. // @grant GM_setValue
  15. // @grant GM_addStyle
  16. // @match *://*.youtube.com/*
  17. // @exclude *://music.youtube.com/*
  18. // @exclude *://*.music.youtube.com/*
  19. // @compatible chrome
  20. // @compatible firefox
  21. // @compatible opera
  22. // @compatible safari
  23. // @compatible edge
  24. // @grant GM.xmlHttpRequest
  25. // @connect youtube.com
  26. // @grant GM_addStyle
  27. // @run-at document-end
  28. // @grant GM.xmlHttpRequest
  29. // @grant unsafeWindow
  30. // @run-at document-end
  31. // @connect googlevideo.com
  32. // @license MIT
  33. // @exclude *://music.youtube.com/*
  34. // @exclude *://*.music.youtube.com/*
  35. // @compatible chrome
  36. // @compatible firefox
  37. // @compatible opera
  38. // @compatible safari
  39. // @compatible edge
  40. // @run-at document-end
  41. // ==/UserScript==
  42.  
  43. ;(function () {
  44. 'use strict'
  45. if (
  46. window.top === window.self &&
  47. GM_info.scriptHandler === 'Tampermonkey' &&
  48. GM_info.version === '4.18.0' &&
  49. GM_getValue('tampermonkey_breaks_should_alert', true)
  50. ) {
  51. alert(
  52. `Tampermonkey recently release a breaking change / bug in version 4.18.0 that breaks this script, which is fixed in newer version of Tampermonkey right now. You should update it or switch to Violentmonkey instead.`
  53. )
  54. GM_setValue('tampermonkey_breaks_should_alert', false)
  55. }
  56. const DEBUG = true
  57. const createLogger = (console, tag) =>
  58. Object.keys(console)
  59. .map(k => [k, (...args) => (DEBUG ? console[k](tag + ': ' + args[0], ...args.slice(1)) : void 0)])
  60. .reduce((acc, [k, fn]) => ((acc[k] = fn), acc), {})
  61. const logger = createLogger(console, 'YTDL')
  62. const sleep = ms => new Promise(res => setTimeout(res, ms))
  63.  
  64. const LANG_FALLBACK = 'en'
  65. const LOCALE = {
  66. en: {
  67. togglelinks: 'Show/Hide Links',
  68. stream: 'Stream',
  69. adaptive: ' ',
  70. videoid: 'Video ID: ',
  71. inbrowser_adaptive_merger: ' ',
  72. dlmp4: ' ',
  73. get_video_failed: 'Failed to get video infomation for unknown reason, refresh the page may work.',
  74. live_stream_disabled_message: 'Local YouTube Downloader is not available for live stream'
  75. },
  76. 'zh-tw': {
  77. togglelinks: '顯示 / 隱藏連結',
  78. stream: '串流 Stream',
  79. adaptive: '自適應 Adaptive (沒有聲音)',
  80. videoid: '影片 ID: ',
  81. inbrowser_adaptive_merger: '線上自適應影片及音訊合成工具 (FFmpeg)',
  82. dlmp4: '一鍵下載高畫質 mp4',
  83. get_video_failed: '無法取得影片資訊,重新整理頁面可能會有效果。',
  84. live_stream_disabled_message: '因為是直播的緣故,本地 YouTube 下載器的功能是停用的。'
  85. },
  86. 'zh-hk': {
  87. togglelinks: '顯示/隱藏連結',
  88. stream: '串流 Stream',
  89. adaptive: '自動適應 Adaptive (沒有聲音)',
  90. videoid: '影片 ID: ',
  91. inbrowser_adaptive_merger: '網上自動適應影片及音訊合成工具 (FFmpeg)',
  92. dlmp4: '一 click 下載高畫質 mp4',
  93. get_video_failed: '無法取得影片資訊,重新整理頁面可能會有效果。',
  94. live_stream_disabled_message: '本地 YouTube 下載器無法用於直播。'
  95. },
  96. zh: {
  97. togglelinks: '显示/隐藏链接',
  98. stream: '串流 Stream',
  99. adaptive: '自适应 Adaptive (没有声音)',
  100. videoid: '视频 ID: ',
  101. inbrowser_adaptive_merger: '线上自适应视频及音频合成工具 (FFmpeg)',
  102. dlmp4: '一键下载高画质 mp4',
  103. get_video_failed: '无法取得影片资讯,重新整理页面可能会有效果。',
  104. live_stream_disabled_message: '因为是直播,本地 YouTube 下载器的功能已被禁用。'
  105. },
  106. ja: {
  107. togglelinks: 'リンク表示・非表示',
  108. stream: 'ストリーミング',
  109. adaptive: 'アダプティブ(音無し)',
  110. videoid: 'ビデオ ID: ',
  111. inbrowser_adaptive_merger: 'ビデオとオーディオを合併するオンラインツール (FFmpeg)',
  112. dlmp4: 'ワンクリックで高解像度の mp4 をダウンロード',
  113. live_stream_disabled_message: 'ライブ配信のため、ローカル YouTube ダウンローダーは無効になっています。'
  114. },
  115. kr: {
  116. togglelinks: '링크 보이기 · 숨기기',
  117. stream: '스트리밍',
  118. adaptive: '적응 (어댑티브)',
  119. videoid: '비디오 ID: ',
  120. inbrowser_adaptive_merger: '비디오와 오디오를 합병하는 온라인 도구 (FFmpeg)',
  121. dlmp4: '한 번의 클릭으로 고해상도 mp4 다운로드'
  122. },
  123. es: {
  124. togglelinks: 'Mostrar/Ocultar Links',
  125. stream: 'Stream',
  126. adaptive: 'Adaptable',
  127. videoid: 'Id del Video: ',
  128. inbrowser_adaptive_merger: 'Acoplar Audio a Video (FFmpeg)'
  129. },
  130. he: {
  131. togglelinks: 'הצג/הסתר קישורים',
  132. stream: 'סטרים',
  133. adaptive: 'אדפטיבי',
  134. videoid: 'מזהה סרטון: '
  135. },
  136. fr: {
  137. togglelinks: 'Afficher/Masquer les liens',
  138. stream: 'Stream',
  139. adaptive: 'Adaptative',
  140. videoid: 'ID vidéo: ',
  141. inbrowser_adaptive_merger: 'Fusionner vidéos et audios adaptatifs dans le navigateur (FFmpeg)',
  142. dlmp4: 'Téléchargez la plus haute résolution mp4 en un clic'
  143. },
  144. pl: {
  145. togglelinks: 'Pokaż/Ukryj Linki',
  146. stream: 'Stream',
  147. adaptive: 'Adaptywne',
  148. videoid: 'ID filmu: ',
  149. inbrowser_adaptive_merger: 'Połącz audio i wideo adaptywne w przeglądarce (FFmpeg)',
  150. dlmp4: 'Pobierz .mp4 w najwyższej jakości'
  151. },
  152. hi: {
  153. togglelinks: 'लिंक टॉगल करें',
  154. stream: 'स्ट्रीमिंग (Stream)',
  155. adaptive: 'अनुकूली (Adaptive)',
  156. videoid: 'वीडियो आईडी: {{id}}'
  157. },
  158. ru: {
  159. togglelinks: 'Показать/Cкрыть ссылки',
  160. stream: 'Поток',
  161. adaptive: 'Адаптивный',
  162. videoid: 'Идентификатор видео: ',
  163. inbrowser_adaptive_merger: 'Адаптивное слияние видео и аудио онлайн (FFmpeg)',
  164. dlmp4: 'Скачать mp4 в высоком разрешении в один клик',
  165. get_video_failed:
  166. 'Не удалось получить информацию о видео по неизвестной причине, попробуйте обновить страницу.',
  167. live_stream_disabled_message: 'Локальный загрузчик YouTube недоступен для прямой трансляции'
  168. },
  169. ua: {
  170. togglelinks: 'Показати/Приховати посилання',
  171. stream: 'Потік',
  172. adaptive: 'Адаптивний',
  173. videoid: 'Ідентифікатор відео: ',
  174. inbrowser_adaptive_merger: 'Адаптивне злиття відео і аудіо онлайн (FFmpeg)',
  175. dlmp4: 'Завантажити mp4 у високій роздільній здатності в один клік',
  176. get_video_failed:
  177. 'Не вдалося отримати інформацію про відео з невідомої причини, спробуйте оновити сторінку.',
  178. live_stream_disabled_message: 'Локальний завантажувач YouTube недоступний для прямої трансляції'
  179. },
  180. cs: {
  181. togglelinks: 'Zobrazit/Skrýt odkazy',
  182. stream: 'Stream',
  183. adaptive: 'Adaptivní',
  184. videoid: 'ID videa: ',
  185. inbrowser_adaptive_merger: 'Online nástroj pro sloučení videa a audia (FFmpeg)',
  186. dlmp4: 'Stáhnout video mp4 jedním kliknutím ve vysokém rozlišení',
  187. get_video_failed: 'Nepodařilo se nahrát informace o videu. Zkuste obnovit stránku (F5).',
  188. live_stream_disabled_message: 'Local YouTube Downloader není dostupný pro živé vysílání'
  189. }
  190. }
  191. for (const [lang, data] of Object.entries(LOCALE)) {
  192. if (lang === LANG_FALLBACK) continue
  193. for (const key of Object.keys(LOCALE[LANG_FALLBACK])) {
  194. if (!(key in data)) {
  195. data[key] = LOCALE[LANG_FALLBACK][key]
  196. }
  197. }
  198. }
  199. const findLang = l => {
  200. l = l.replace('-Hant', '') // special case for zh-Hant-TW
  201. // language resolution logic: zh-tw --(if not exists)--> zh --(if not exists)--> LANG_FALLBACK(en)
  202. l = l.toLowerCase().replace('_', '-')
  203. if (l in LOCALE) return l
  204. else if (l.length > 2) return findLang(l.split('-')[0])
  205. else return LANG_FALLBACK
  206. }
  207. const getLangCode = () => {
  208. const html = document.querySelector('html')
  209. if (html) {
  210. return html.lang
  211. } else {
  212. return navigator.language
  213. }
  214. }
  215. const $ = (s, x = document) => x.querySelector(s)
  216. const $el = (tag, opts) => {
  217. const el = document.createElement(tag)
  218. Object.assign(el, opts)
  219. return el
  220. }
  221. const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  222. const parseDecsig = data => {
  223. try {
  224. if (data.startsWith('var script')) {
  225. // they inject the script via script tag
  226. const obj = {}
  227. const document = {
  228. createElement: () => obj,
  229. head: { appendChild: () => {} }
  230. }
  231. eval(data)
  232. data = obj.innerHTML
  233. }
  234. const fnnameresult = /=([a-zA-Z0-9\$_]+?)\(decodeURIComponent/.exec(data)
  235. const fnname = fnnameresult[1]
  236. const _argnamefnbodyresult = new RegExp(escapeRegExp(fnname) + '=function\\((.+?)\\){((.+)=\\2.+?)}').exec(
  237. data
  238. )
  239. const [_, argname, fnbody] = _argnamefnbodyresult
  240. const helpernameresult = /;([a-zA-Z0-9$_]+?)\..+?\(/.exec(fnbody)
  241. const helpername = helpernameresult[1]
  242. const helperresult = new RegExp('var ' + escapeRegExp(helpername) + '={[\\s\\S]+?};').exec(data)
  243. const helper = helperresult[0]
  244. logger.log(`parsedecsig result: %s=>{%s\n%s}`, argname, helper, fnbody)
  245. return new Function([argname], helper + '\n' + fnbody)
  246. } catch (e) {
  247. logger.error('parsedecsig error: %o', e)
  248. logger.info('script content: %s', data)
  249. logger.info(
  250. 'If you encounter this error, please copy the full "script content" to https://pastebin.com/ for me.'
  251. )
  252. }
  253. }
  254. const parseQuery = s => [...new URLSearchParams(s).entries()].reduce((acc, [k, v]) => ((acc[k] = v), acc), {})
  255. const parseResponse = (id, playerResponse, decsig) => {
  256. logger.log(`video %s playerResponse: %o`, id, playerResponse)
  257. let stream = []
  258. if (playerResponse.streamingData.formats) {
  259. stream = playerResponse.streamingData.formats.map(x =>
  260. Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
  261. )
  262. logger.log(`video %s stream: %o`, id, stream)
  263. for (const obj of stream) {
  264. if (obj.s) {
  265. obj.s = decsig(obj.s)
  266. obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`
  267. }
  268. }
  269. }
  270.  
  271. let adaptive = []
  272. if (playerResponse.streamingData.adaptiveFormats) {
  273. adaptive = playerResponse.streamingData.adaptiveFormats.map(x =>
  274. Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
  275. )
  276. logger.log(`video %s adaptive: %o`, id, adaptive)
  277. for (const obj of adaptive) {
  278. if (obj.s) {
  279. obj.s = decsig(obj.s)
  280. obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`
  281. }
  282. }
  283. }
  284. logger.log(`video %s result: %o`, id, { stream, adaptive })
  285. return { stream, adaptive, details: playerResponse.videoDetails, playerResponse }
  286. }
  287.  
  288. // video downloader
  289. const xhrDownloadUint8Array = async ({ url, contentLength }, progressCb) => {
  290. if (typeof contentLength === 'string') contentLength = parseInt(contentLength)
  291. progressCb({
  292. loaded: 0,
  293. total: contentLength,
  294. speed: 0
  295. })
  296. const chunkSize = 65536
  297. const getBuffer = (start, end) =>
  298. fetch(url + `&range=${start}-${end ? end - 1 : ''}`).then(r => r.arrayBuffer())
  299. const data = new Uint8Array(contentLength)
  300. let downloaded = 0
  301. const queue = new pQueue.default({ concurrency: 6 })
  302. const startTime = Date.now()
  303. const ps = []
  304. for (let start = 0; start < contentLength; start += chunkSize) {
  305. const exceeded = start + chunkSize > contentLength
  306. const curChunkSize = exceeded ? contentLength - start : chunkSize
  307. const end = exceeded ? null : start + chunkSize
  308. const p = queue.add(() => {
  309. console.log('dl start', url, start, end)
  310. return getBuffer(start, end)
  311. .then(buf => {
  312. console.log('dl done', url, start, end)
  313. downloaded += curChunkSize
  314. data.set(new Uint8Array(buf), start)
  315. const ds = (Date.now() - startTime + 1) / 1000
  316. progressCb({
  317. loaded: downloaded,
  318. total: contentLength,
  319. speed: downloaded / ds
  320. })
  321. })
  322. .catch(err => {
  323. queue.clear()
  324. alert('Download error')
  325. })
  326. })
  327. ps.push(p)
  328. }
  329. await Promise.all(ps)
  330. return data
  331. }
  332.  
  333. const ffWorker = FFmpeg.createWorker({
  334. logger: DEBUG ? m => logger.log(m.message) : () => {}
  335. })
  336. let ffWorkerLoaded = false
  337. const mergeVideo = async (video, audio) => {
  338. if (!ffWorkerLoaded) await ffWorker.load()
  339. await ffWorker.write('video.mp4', video)
  340. await ffWorker.write('audio.mp4', audio)
  341. await ffWorker.run('-i video.mp4 -i audio.mp4 -c copy output.mp4', {
  342. input: ['video.mp4', 'audio.mp4'],
  343. output: 'output.mp4'
  344. })
  345. const { data } = await ffWorker.read('output.mp4')
  346. await ffWorker.remove('output.mp4')
  347. return data
  348. }
  349. const triggerDownload = (url, filename) => {
  350. const a = document.createElement('a')
  351. a.href = url
  352. a.download = filename
  353. document.body.appendChild(a)
  354. a.click()
  355. a.remove()
  356. }
  357. const dlModalTemplate = `
  358. <div style="width: 100%; height: 100%;">
  359. <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>
  360. <div v-else style="height: 100%; width: 100%; display: flex; flex-direction: column;">
  361. <div style="flex: 1; margin: 10px;">
  362. <p style="font-size: 24px;">Video</p>
  363. <progress style="width: 100%;" :value="video.progress" min="0" max="100"></progress>
  364. <div style="display: flex; justify-content: space-between;">
  365. <span>{{video.speed}} kB/s</span>
  366. <span>{{video.loaded}}/{{video.total}} MB</span>
  367. </div>
  368. </div>
  369. <div style="flex: 1; margin: 10px;">
  370. <p style="font-size: 24px;">Audio</p>
  371. <progress style="width: 100%;" :value="audio.progress" min="0" max="100"></progress>
  372. <div style="display: flex; justify-content: space-between;">
  373. <span>{{audio.speed}} kB/s</span>
  374. <span>{{audio.loaded}}/{{audio.total}} MB</span>
  375. </div>
  376. </div>
  377. </div>
  378. </div>
  379. `
  380. function openDownloadModel(adaptive, title) {
  381. const win = open(
  382. '',
  383. 'Video Download',
  384. `toolbar=no,height=${screen.height / 2},width=${screen.width / 2},left=${screenLeft},top=${screenTop}`
  385. )
  386. const div = win.document.createElement('div')
  387. win.document.body.appendChild(div)
  388. win.document.title = `Downloading "${title}"`
  389. const dlModalApp = new Vue({
  390. template: dlModalTemplate,
  391. data() {
  392. return {
  393. video: {
  394. progress: 0,
  395. total: 0,
  396. loaded: 0,
  397. speed: 0
  398. },
  399. audio: {
  400. progress: 0,
  401. total: 0,
  402. loaded: 0,
  403. speed: 0
  404. },
  405. merging: false
  406. }
  407. },
  408. methods: {
  409. async start(adaptive, title) {
  410. win.onbeforeunload = () => true
  411. // YouTube's default order is descending by video quality
  412. const videoObj = adaptive
  413. .filter(x => x.mimeType.includes('video/mp4') || x.mimeType.includes('video/webm'))
  414. .map(v => {
  415. const [_, quality, fps] = /(\d+)p(\d*)/.exec(v.qualityLabel)
  416. v.qualityNum = parseInt(quality)
  417. v.fps = fps ? parseInt(fps) : 30
  418. return v
  419. })
  420. .sort((a, b) => {
  421. if (a.qualityNum === b.qualityNum) return b.fps - a.fps // ex: 30-60=-30, then a will be put before b
  422. return b.qualityNum - a.qualityNum
  423. })[0]
  424. const audioObj = adaptive.find(x => x.mimeType.includes('audio/mp4'))
  425. const vPromise = xhrDownloadUint8Array(videoObj, e => {
  426. this.video.progress = (e.loaded / e.total) * 100
  427. this.video.loaded = (e.loaded / 1024 / 1024).toFixed(2)
  428. this.video.total = (e.total / 1024 / 1024).toFixed(2)
  429. this.video.speed = (e.speed / 1024).toFixed(2)
  430. })
  431. const aPromise = xhrDownloadUint8Array(audioObj, e => {
  432. this.audio.progress = (e.loaded / e.total) * 100
  433. this.audio.loaded = (e.loaded / 1024 / 1024).toFixed(2)
  434. this.audio.total = (e.total / 1024 / 1024).toFixed(2)
  435. this.audio.speed = (e.speed / 1024).toFixed(2)
  436. })
  437. const [varr, aarr] = await Promise.all([vPromise, aPromise])
  438. this.merging = true
  439. win.onunload = () => {
  440. // trigger download when user close it
  441. const bvurl = URL.createObjectURL(new Blob([varr]))
  442. const baurl = URL.createObjectURL(new Blob([aarr]))
  443. triggerDownload(bvurl, title + '-videoonly.mp4')
  444. triggerDownload(baurl, title + '-audioonly.mp4')
  445. }
  446. const result = await Promise.race([mergeVideo(varr, aarr), sleep(1000 * 25).then(() => null)])
  447. if (!result) {
  448. alert('An error has occurred when merging video')
  449. const bvurl = URL.createObjectURL(new Blob([varr]))
  450. const baurl = URL.createObjectURL(new Blob([aarr]))
  451. triggerDownload(bvurl, title + '-videoonly.mp4')
  452. triggerDownload(baurl, title + '-audioonly.mp4')
  453. return this.close()
  454. }
  455. this.merging = false
  456. const url = URL.createObjectURL(new Blob([result]))
  457. triggerDownload(url, title + '.mp4')
  458. win.onbeforeunload = null
  459. win.onunload = null
  460. win.close()
  461. }
  462. }
  463. }).$mount(div)
  464. dlModalApp.start(adaptive, title)
  465. }
  466.  
  467. const template = `
  468. <div class="box" :class="{'dark':dark}">
  469. <template v-if="!isLiveStream">
  470. <div v-if="adaptive.length" class="of-h t-center c-pointer lh-20">
  471. <a class="fs-14px" @click="dlmp4" v-text="strings.dlmp4"></a>
  472. </div>
  473. <div @click="hide=!hide" class="box-toggle div-a t-center fs-14px c-pointer lh-20" v-text="strings.togglelinks"></div>
  474. <div :class="{'hide':hide}">
  475. <div class="t-center fs-14px" v-text="strings.videoid+id"></div>
  476. <div class="d-flex">
  477. <div class="CLASS2 ">
  478. <div class="t-center fs-14px" v-text="strings.stream"></div>
  479. <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>
  480. </div>
  481. <div class="CLASS1">
  482. </template>
  483. </div>
  484. `.slice(1)
  485. const app = new Vue({
  486. data() {
  487. return {
  488. hide: true,
  489. id: '',
  490. isLiveStream: false,
  491. stream: [],
  492. adaptive: [],
  493. details: null,
  494. dark: false,
  495. lang: findLang(getLangCode())
  496. }
  497. },
  498. computed: {
  499. strings() {
  500. return LOCALE[this.lang.toLowerCase()]
  501. }
  502. },
  503. methods: {
  504. dlmp4() {
  505. openDownloadModel(this.adaptive, this.details.title)
  506. },
  507. formatStreamText(vid) {
  508. return [vid.qualityLabel, vid.quality].filter(x => x).join(': ')
  509. },
  510. formatAdaptiveText(vid) {
  511. let str = [vid.qualityLabel, vid.mimeType].filter(x => x).join(': ')
  512. if (vid.mimeType.includes('audio')) {
  513. str += ` ${Math.round(vid.bitrate / 1000)}kbps`
  514. }
  515. return str
  516. }
  517. },
  518. template
  519. })
  520. logger.log(`default language: %s`, app.lang)
  521.  
  522. // attach element
  523. const shadowHost = $el('div')
  524. const shadow = shadowHost.attachShadow ? shadowHost.attachShadow({ mode: 'closed' }) : shadowHost // no shadow dom
  525. logger.log('shadowHost: %o', shadowHost)
  526. const container = $el('div')
  527. shadow.appendChild(container)
  528. app.$mount(container)
  529.  
  530. if (DEBUG && typeof unsafeWindow !== 'undefined') {
  531. // expose some functions for debugging
  532. unsafeWindow.$app = app
  533. unsafeWindow.parseQuery = parseQuery
  534. unsafeWindow.parseDecsig = parseDecsig
  535. unsafeWindow.parseResponse = parseResponse
  536. }
  537. const load = async playerResponse => {
  538. try {
  539. const basejs =
  540. (typeof ytplayer !== 'undefined' && 'config' in ytplayer && ytplayer.config.assets
  541. ? 'https://' + location.host + ytplayer.config.assets.js
  542. : 'web_player_context_config' in ytplayer
  543. ? 'https://' + location.host + ytplayer.web_player_context_config.jsUrl
  544. : null) || $('script[src$="base.js"]').src
  545. const decsig = await xf.get(basejs).text(parseDecsig)
  546. const id = parseQuery(location.search).v
  547. const data = parseResponse(id, playerResponse, decsig)
  548. logger.log('video loaded: %s', id)
  549. app.isLiveStream = data.playerResponse.playabilityStatus.liveStreamability != null
  550. app.id = id
  551. app.stream = data.stream
  552. app.adaptive = data.adaptive
  553. app.details = data.details
  554.  
  555. const actLang = getLangCode()
  556. if (actLang != null) {
  557. const lang = findLang(actLang)
  558. logger.log('youtube ui lang: %s', actLang)
  559. logger.log('ytdl lang:', lang)
  560. app.lang = lang
  561. }
  562. } catch (err) {
  563. alert(app.strings.get_video_failed)
  564. logger.error('load', err)
  565. }
  566. }
  567.  
  568. // hook fetch response
  569. const ff = fetch
  570. unsafeWindow.fetch = (...args) => {
  571. if (args[0] instanceof Request) {
  572. return ff(...args).then(resp => {
  573. if (resp.url.includes('player')) {
  574. resp.clone().json().then(load)
  575. }
  576. return resp
  577. })
  578. }
  579. return ff(...args)
  580. }
  581.  
  582. // attach element
  583. const it = setInterval(() => {
  584. const el =
  585. $('ytd-watch-metadata') ||
  586. $('#info-contents') ||
  587. $('#watch-header') ||
  588. $('.page-container:not([hidden]) ytm-item-section-renderer>lazy-list')
  589. if (el && !el.contains(shadowHost)) {
  590. el.appendChild(shadowHost)
  591. clearInterval(it)
  592. }
  593. }, 100)
  594.  
  595. // init
  596. unsafeWindow.addEventListener('load', () => {
  597. const firstResp = unsafeWindow?.ytplayer?.config?.args?.raw_player_response
  598. if (firstResp) {
  599. load(firstResp)
  600. }
  601. })
  602.  
  603. // listen to dark mode toggle
  604. const $html = $('html')
  605. new MutationObserver(() => {
  606. app.dark = $html.getAttribute('dark') !== null
  607. }).observe($html, { attributes: true })
  608. app.dark = $html.getAttribute('dark') !== null
  609.  
  610. const css = `
  611. .hide{
  612. display: none;
  613. }
  614. .t-center{
  615. text-align: center;
  616. }
  617. .d-flex{
  618. display: flex;
  619. }
  620. .f-1{
  621. flex: 1;
  622. }
  623. .fs-14px{
  624. font-size: 14px;
  625. }
  626. .of-h{
  627. overflow: hidden;
  628. }
  629. .box{
  630. padding-top: .5em;
  631. padding-bottom: .5em;
  632. border-bottom: 1px solid var(--yt-border-color);
  633. font-family: Arial;
  634. }
  635. .box-toggle{
  636. margin: 3px;
  637. user-select: none;
  638. -moz-user-select: -moz-none;
  639. }
  640. .ytdl-link-btn{
  641. display: block;
  642. border: 1px solid !important;
  643. border-radius: 3px;
  644. text-decoration: none !important;
  645. outline: 0;
  646. text-align: center;
  647. padding: 2px;
  648. margin: 5px;
  649. color: black;
  650. }
  651. a, .div-a{
  652. text-decoration: none;
  653. color: var(--yt-button-color, inherit);
  654. }
  655. a:hover, .div-a:hover{
  656. color: var(--yt-spec-call-to-action, blue);
  657. }
  658. .box.dark{
  659. color: var(--yt-endpoint-color, var(--yt-spec-text-primary));
  660. }
  661. .box.dark .ytdl-link-btn{
  662. color: var(--yt-endpoint-color, var(--yt-spec-text-primary));
  663. }
  664. .box.dark .ytdl-link-btn:hover{
  665. color: rgba(200, 200, 255, 0.8);
  666. }
  667. .box.dark .box-toggle:hover{
  668. color: rgba(200, 200, 255, 0.8);
  669. }
  670. .c-pointer{
  671. cursor: pointer;
  672. }
  673. .lh-20{
  674. line-height: 20px;
  675. }
  676. `
  677. shadow.appendChild($el('style', { textContent: css }))
  678. })()
  679.  
  680.  
  681. // This function will search for the aria-label "Download" and delete the element.
  682. function checkAndDeleteDownloadElement() {
  683. const downloadElements = document.querySelectorAll('[aria-label="Download"]');
  684.  
  685. if (downloadElements.length > 0) {
  686. downloadElements.forEach(element => {
  687. element.parentNode.removeChild(element);
  688. });
  689. }
  690. }
  691.  
  692. // Call the function checkAndDeleteDownloadElement every 1 second (1000 milliseconds).
  693. setInterval(checkAndDeleteDownloadElement, 10);
  694.  
  695. setInterval(function() {
  696. var element = document.getElementById("primary-entry");
  697. if (element) {
  698. element.parentNode.removeChild(element);
  699. }
  700. }, 10);
  701.  
  702. setInterval(function() {
  703. (function() {
  704. function t(t) {
  705. const e = t.querySelector(".ytp-ad-skip-button-modern.ytp-button");
  706. e && e.click()
  707. }
  708.  
  709. function e(t, e) {
  710. const n = t.querySelector("video");
  711. n && e && (n.playbackRate = 16, n.muted = !0)
  712. }
  713.  
  714. function n(n, s) {
  715. for (const s of n) {
  716. if ("attributes" === s.type && "class" === s.attributeName) {
  717. const t = s.target,
  718. n = t.classList.contains("ad-showing") || t.classList.contains("ad-interrupting");
  719. e(t, n)
  720. }
  721. "childList" === s.type && s.addedNodes.length && t(s.target)
  722. }
  723. }! function s() {
  724. const i = document.querySelector("#movie_player");
  725. if (i) {
  726. new MutationObserver(n).observe(i, {
  727. attributes: !0,
  728. childList: !0,
  729. subtree: !0
  730. });
  731. const s = i.classList.contains("ad-showing") || i.classList.contains("ad-interrupting");
  732. e(i, s), t(i)
  733. } else setTimeout(s, 50)
  734. }()
  735. })();
  736. }, 10);
  737.  
  738. const extConfig = {
  739. // BEGIN USER OPTIONS
  740. // You may change the following variables to allowed values listed in the corresponding brackets (* means default). Keep the style and keywords intact.
  741. showUpdatePopup: false, // [true, false*] Show a popup tab after extension update (See what's new)
  742. disableVoteSubmission: false, // [true, false*] Disable like/dislike submission (Stops counting your likes and dislikes)
  743. coloredThumbs: false, // [true, false*] Colorize thumbs (Use custom colors for thumb icons)
  744. coloredBar: false, // [true, false*] Colorize ratio bar (Use custom colors for ratio bar)
  745. colorTheme: "classic", // [classic*, accessible, neon] Color theme (red/green, blue/yellow, pink/cyan)
  746. numberDisplayFormat: "compactShort", // [compactShort*, compactLong, standard] Number format (For non-English locale users, you may be able to improve appearance with a different option. Please file a feature request if your locale is not covered)
  747. numberDisplayRoundDown: true, // [true*, false] Round down numbers (Show rounded down numbers)
  748. tooltipPercentageMode: "none", // [none*, dash_like, dash_dislike, both, only_like, only_dislike] Mode of showing percentage in like/dislike bar tooltip.
  749. numberDisplayReformatLikes: false, // [true, false*] Re-format like numbers (Make likes and dislikes format consistent)
  750. rateBarEnabled: false // [true, false*] Enables ratio bar under like/dislike buttons
  751. // END USER OPTIONS
  752. };
  753.  
  754. const LIKED_STATE = "LIKED_STATE";
  755. const DISLIKED_STATE = "DISLIKED_STATE";
  756. const NEUTRAL_STATE = "NEUTRAL_STATE";
  757. let previousState = 3; //1=LIKED, 2=DISLIKED, 3=NEUTRAL
  758. let likesvalue = 0;
  759. let dislikesvalue = 0;
  760. let preNavigateLikeButton = null;
  761.  
  762. let isMobile = location.hostname == "m.youtube.com";
  763. let isShorts = () => location.pathname.startsWith("/shorts");
  764. let mobileDislikes = 0;
  765. function cLog(text, subtext = "") {
  766. subtext = subtext.trim() === "" ? "" : `(${subtext})`;
  767. console.log(`[Return YouTube Dislikes] ${text} ${subtext}`);
  768. }
  769.  
  770. function isInViewport(element) {
  771. const rect = element.getBoundingClientRect();
  772. const height = innerHeight || document.documentElement.clientHeight;
  773. const width = innerWidth || document.documentElement.clientWidth;
  774. return (
  775. // When short (channel) is ignored, the element (like/dislike AND short itself) is
  776. // hidden with a 0 DOMRect. In this case, consider it outside of Viewport
  777. !(rect.top == 0 && rect.left == 0 && rect.bottom == 0 && rect.right == 0) &&
  778. rect.top >= 0 &&
  779. rect.left >= 0 &&
  780. rect.bottom <= height &&
  781. rect.right <= width
  782. );
  783. }
  784.  
  785. function getButtons() {
  786. if (isShorts()) {
  787. let elements = document.querySelectorAll(
  788. isMobile
  789. ? "ytm-like-button-renderer"
  790. : "#like-button > ytd-like-button-renderer"
  791. );
  792. for (let element of elements) {
  793. if (isInViewport(element)) {
  794. return element;
  795. }
  796. }
  797. }
  798. if (isMobile) {
  799. return (
  800. document.querySelector(".slim-video-action-bar-actions .segmented-buttons") ??
  801. document.querySelector(".slim-video-action-bar-actions")
  802. );
  803. }
  804. if (document.getElementById("menu-container")?.offsetParent === null) {
  805. return (
  806. document.querySelector("ytd-menu-renderer.ytd-watch-metadata > div") ??
  807. document.querySelector("ytd-menu-renderer.ytd-video-primary-info-renderer > div")
  808. );
  809. } else {
  810. return document
  811. .getElementById("menu-container")
  812. ?.querySelector("#top-level-buttons-computed");
  813. }
  814. }
  815.  
  816. function getDislikeButton() {
  817. if (getButtons().children[0].tagName ===
  818. "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER")
  819. {
  820. if (getButtons().children[0].children[1] === undefined) {
  821. return document.querySelector("#segmented-dislike-button");
  822. } else {
  823. return getButtons().children[0].children[1];
  824. }
  825. } else {
  826. if (getButtons().querySelector("segmented-like-dislike-button-view-model")) {
  827. const dislikeViewModel = getButtons().querySelector("dislike-button-view-model");
  828. if (!dislikeViewModel) cLog("Dislike button wasn't added to DOM yet...");
  829. return dislikeViewModel;
  830. } else {
  831. return getButtons().children[1];
  832. }
  833. }
  834. }
  835.  
  836. function getLikeButton() {
  837. return getButtons().children[0].tagName ===
  838. "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER"
  839. ? document.querySelector("#segmented-like-button") !== null ? document.querySelector("#segmented-like-button") : getButtons().children[0].children[0]
  840. : getButtons().querySelector("like-button-view-model") ?? getButtons().children[0];
  841. }
  842.  
  843. function getLikeTextContainer() {
  844. return (
  845. getLikeButton().querySelector("#text") ??
  846. getLikeButton().getElementsByTagName("yt-formatted-string")[0] ??
  847. getLikeButton().querySelector("span[role='text']")
  848. );
  849. }
  850.  
  851.  
  852. function getDislikeTextContainer() {
  853. const dislikeButton = getDislikeButton();
  854. let result =
  855. dislikeButton?.querySelector("#text") ??
  856. dislikeButton?.getElementsByTagName("yt-formatted-string")[0] ??
  857. dislikeButton?.querySelector("span[role='text']")
  858. if (result === null) {
  859. let textSpan = document.createElement("span");
  860. textSpan.id = "text";
  861. textSpan.style.marginLeft = "6px";
  862. dislikeButton?.querySelector("button").appendChild(textSpan);
  863. if (dislikeButton) dislikeButton.querySelector("button").style.width = "auto";
  864. result = textSpan;
  865. }
  866. return result;
  867. }
  868.  
  869. function createObserver(options, callback) {
  870. const observerWrapper = new Object();
  871. observerWrapper.options = options;
  872. observerWrapper.observer = new MutationObserver(callback);
  873. observerWrapper.observe = function (element) { this.observer.observe(element, this.options); }
  874. observerWrapper.disconnect = function () { this.observer.disconnect(); }
  875. return observerWrapper;
  876. }
  877.  
  878. let shortsObserver = null;
  879.  
  880. if (isShorts() && !shortsObserver) {
  881. cLog("Initializing shorts mutation observer");
  882. shortsObserver = createObserver({
  883. attributes: true
  884. }, (mutationList) => {
  885. mutationList.forEach((mutation) => {
  886. if (
  887. mutation.type === "attributes" &&
  888. mutation.target.nodeName === "TP-YT-PAPER-BUTTON" &&
  889. mutation.target.id === "button"
  890. ) {
  891. cLog("Short thumb button status changed");
  892. if (mutation.target.getAttribute("aria-pressed") === "true") {
  893. mutation.target.style.color =
  894. mutation.target.parentElement.parentElement.id === "like-button"
  895. ? getColorFromTheme(true)
  896. : getColorFromTheme(false);
  897. } else {
  898. mutation.target.style.color = "unset";
  899. }
  900. return;
  901. }
  902. cLog(
  903. "Unexpected mutation observer event: " + mutation.target + mutation.type
  904. );
  905. });
  906. });
  907. }
  908.  
  909. function isVideoLiked() {
  910. if (isMobile) {
  911. return (
  912. getLikeButton().querySelector("button").getAttribute("aria-label") ==
  913. "true"
  914. );
  915. }
  916. return getLikeButton().classList.contains("style-default-active");
  917. }
  918.  
  919. function isVideoDisliked() {
  920. if (isMobile) {
  921. return (
  922. getDislikeButton()?.querySelector("button").getAttribute("aria-label") ==
  923. "true"
  924. );
  925. }
  926. return getDislikeButton()?.classList.contains("style-default-active");
  927. }
  928.  
  929. function isVideoNotLiked() {
  930. if (isMobile) {
  931. return !isVideoLiked();
  932. }
  933. return getLikeButton().classList.contains("style-text");
  934. }
  935.  
  936. function isVideoNotDisliked() {
  937. if (isMobile) {
  938. return !isVideoDisliked();
  939. }
  940. return getDislikeButton()?.classList.contains("style-text");
  941. }
  942.  
  943. function checkForUserAvatarButton() {
  944. if (isMobile) {
  945. return;
  946. }
  947. if (document.querySelector("#avatar-btn")) {
  948. return true;
  949. } else {
  950. return false;
  951. }
  952. }
  953.  
  954. function getState() {
  955. if (isVideoLiked()) {
  956. return LIKED_STATE;
  957. }
  958. if (isVideoDisliked()) {
  959. return DISLIKED_STATE;
  960. }
  961. return NEUTRAL_STATE;
  962. }
  963.  
  964. function setLikes(likesCount) {
  965. if (isMobile) {
  966. getButtons().children[0].querySelector(".button-renderer-text").innerText =
  967. likesCount;
  968. return;
  969. }
  970. getLikeTextContainer().innerText = likesCount;
  971. }
  972.  
  973. function setDislikes(dislikesCount) {
  974. if (isMobile) {
  975. mobileDislikes = dislikesCount;
  976. return;
  977. }
  978. getDislikeTextContainer()?.removeAttribute('is-empty');
  979. getDislikeTextContainer().innerText = dislikesCount;
  980. }
  981.  
  982. function getLikeCountFromButton() {
  983. try {
  984. if (isShorts()) {
  985. //Youtube Shorts don't work with this query. It's not necessary; we can skip it and still see the results.
  986. //It should be possible to fix this function, but it's not critical to showing the dislike count.
  987. return false;
  988. }
  989. let likeButton = getLikeButton()
  990. .querySelector("yt-formatted-string#text") ??
  991. getLikeButton().querySelector("button");
  992.  
  993. let likesStr = likeButton.getAttribute("aria-label")
  994. .replace(/\D/g, "");
  995. return likesStr.length > 0 ? parseInt(likesStr) : false;
  996. }
  997. catch {
  998. return false;
  999. }
  1000.  
  1001. }
  1002.  
  1003. (typeof GM_addStyle != "undefined"
  1004. ? GM_addStyle
  1005. : (styles) => {
  1006. let styleNode = document.createElement("style");
  1007. styleNode.type = "text/css";
  1008. styleNode.innerText = styles;
  1009. document.head.appendChild(styleNode);
  1010. })(`
  1011. #return-youtube-dislike-bar-container {
  1012. background: var(--yt-spec-icon-disabled);
  1013. border-radius: 2px;
  1014. }
  1015.  
  1016. #return-youtube-dislike-bar {
  1017. background: var(--yt-spec-text-primary);
  1018. border-radius: 2px;
  1019. transition: all 0.15s ease-in-out;
  1020. }
  1021.  
  1022. .ryd-tooltip {
  1023. position: absolute;
  1024. display: block;
  1025. height: 2px;
  1026. bottom: -10px;
  1027. }
  1028.  
  1029. .ryd-tooltip-bar-container {
  1030. width: 100%;
  1031. height: 2px;
  1032. position: absolute;
  1033. padding-top: 6px;
  1034. padding-bottom: 12px;
  1035. top: -6px;
  1036. }
  1037.  
  1038. ytd-menu-renderer.ytd-watch-metadata {
  1039. overflow-y: visible !important;
  1040. }
  1041. #top-level-buttons-computed {
  1042. position: relative !important;
  1043. }
  1044. `);
  1045.  
  1046. function createRateBar(likes, dislikes) {
  1047. if (isMobile || !extConfig.rateBarEnabled) {
  1048. return;
  1049. }
  1050. let rateBar = document.getElementById("return-youtube-dislike-bar-container");
  1051.  
  1052. const widthPx =
  1053. getLikeButton().clientWidth +
  1054. (getDislikeButton()?.clientWidth ?? 52);
  1055.  
  1056. const widthPercent =
  1057. likes + dislikes > 0 ? (likes / (likes + dislikes)) * 100 : 50;
  1058.  
  1059. var likePercentage = parseFloat(widthPercent.toFixed(1));
  1060. const dislikePercentage = (100 - likePercentage).toLocaleString();
  1061. likePercentage = likePercentage.toLocaleString();
  1062.  
  1063. var tooltipInnerHTML;
  1064. switch (extConfig.tooltipPercentageMode) {
  1065. case "dash_like":
  1066. tooltipInnerHTML = `${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}&nbsp;&nbsp;-&nbsp;&nbsp;${likePercentage}%`;
  1067. break;
  1068. case "dash_dislike":
  1069. tooltipInnerHTML = `${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}&nbsp;&nbsp;-&nbsp;&nbsp;${dislikePercentage}%`;
  1070. break;
  1071. case "both":
  1072. tooltipInnerHTML = `${likePercentage}%&nbsp;/&nbsp;${dislikePercentage}%`;
  1073. break;
  1074. case "only_like":
  1075. tooltipInnerHTML = `${likePercentage}%`;
  1076. break;
  1077. case "only_dislike":
  1078. tooltipInnerHTML = `${dislikePercentage}%`;
  1079. break;
  1080. default:
  1081. tooltipInnerHTML = `${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}`;
  1082. }
  1083.  
  1084. if (!rateBar && !isMobile) {
  1085. let colorLikeStyle = "";
  1086. let colorDislikeStyle = "";
  1087. if (extConfig.coloredBar) {
  1088. colorLikeStyle = "; background-color: " + getColorFromTheme(true);
  1089. colorDislikeStyle = "; background-color: " + getColorFromTheme(false);
  1090. }
  1091.  
  1092. getButtons().insertAdjacentHTML(
  1093. "beforeend",
  1094. `
  1095. <div class="ryd-tooltip" style="width: ${widthPx}px">
  1096. <div class="ryd-tooltip-bar-container">
  1097. <div
  1098. id="return-youtube-dislike-bar-container"
  1099. style="width: 100%; height: 2px;${colorDislikeStyle}"
  1100. >
  1101. <div
  1102. id="return-youtube-dislike-bar"
  1103. style="width: ${widthPercent}%; height: 100%${colorDislikeStyle}"
  1104. ></div>
  1105. </div>
  1106. </div>
  1107. <tp-yt-paper-tooltip position="top" id="ryd-dislike-tooltip" class="style-scope ytd-sentiment-bar-renderer" role="tooltip" tabindex="-1">
  1108. <!--css-build:shady-->${tooltipInnerHTML}
  1109. </tp-yt-paper-tooltip>
  1110. </div>
  1111. `
  1112. );
  1113. let descriptionAndActionsElement = document.getElementById("top-row");
  1114. descriptionAndActionsElement.style.borderBottom =
  1115. "1px solid var(--yt-spec-10-percent-layer)";
  1116. descriptionAndActionsElement.style.paddingBottom = "10px";
  1117. } else {
  1118. document.querySelector(
  1119. ".ryd-tooltip"
  1120. ).style.width = widthPx + "px";
  1121. document.getElementById("return-youtube-dislike-bar").style.width =
  1122. widthPercent + "%";
  1123.  
  1124. if (extConfig.coloredBar) {
  1125. document.getElementById(
  1126. "return-youtube-dislike-bar-container"
  1127. ).style.backgroundColor = getColorFromTheme(false);
  1128. document.getElementById(
  1129. "return-youtube-dislike-bar"
  1130. ).style.backgroundColor = getColorFromTheme(true);
  1131. }
  1132. }
  1133. }
  1134.  
  1135. function setState() {
  1136. cLog("Fetching votes...");
  1137. let statsSet = false;
  1138.  
  1139. fetch(
  1140. `https://returnyoutubedislikeapi.com/votes?videoId=${getVideoId()}`
  1141. ).then((response) => {
  1142. response.json().then((json) => {
  1143. if (json && !("traceId" in response) && !statsSet) {
  1144. const { dislikes, likes } = json;
  1145. cLog(`Received count: ${dislikes}`);
  1146. likesvalue = likes;
  1147. dislikesvalue = dislikes;
  1148. setDislikes(numberFormat(dislikes));
  1149. if (extConfig.numberDisplayReformatLikes === true) {
  1150. const nativeLikes = getLikeCountFromButton();
  1151. if (nativeLikes !== false) {
  1152. setLikes(numberFormat(nativeLikes));
  1153. }
  1154. }
  1155. createRateBar(likes, dislikes);
  1156. if (extConfig.coloredThumbs === true) {
  1157. const dislikeButton = getDislikeButton();
  1158. if (isShorts()) {
  1159. // for shorts, leave deactived buttons in default color
  1160. const shortLikeButton = getLikeButton().querySelector(
  1161. "tp-yt-paper-button#button"
  1162. );
  1163. const shortDislikeButton = dislikeButton?.querySelector(
  1164. "tp-yt-paper-button#button"
  1165. );
  1166. if (shortLikeButton.getAttribute("aria-pressed") === "true") {
  1167. shortLikeButton.style.color = getColorFromTheme(true);
  1168. }
  1169. if (shortDislikeButton &&
  1170. shortDislikeButton.getAttribute("aria-pressed") === "true")
  1171. {
  1172. shortDislikeButton.style.color = getColorFromTheme(false);
  1173. }
  1174. shortsObserver.observe(shortLikeButton);
  1175. shortsObserver.observe(shortDislikeButton);
  1176. } else {
  1177. getLikeButton().style.color = getColorFromTheme(true);
  1178. if (dislikeButton) dislikeButton.style.color = getColorFromTheme(false);
  1179. }
  1180. }
  1181. }
  1182. });
  1183. });
  1184. }
  1185.  
  1186. function updateDOMDislikes() {
  1187. setDislikes(numberFormat(dislikesvalue));
  1188. createRateBar(likesvalue, dislikesvalue);
  1189. }
  1190.  
  1191. function likeClicked() {
  1192. if (checkForUserAvatarButton() == true) {
  1193. if (previousState == 1) {
  1194. likesvalue--;
  1195. updateDOMDislikes();
  1196. previousState = 3;
  1197. } else if (previousState == 2) {
  1198. likesvalue++;
  1199. dislikesvalue--;
  1200. updateDOMDislikes();
  1201. previousState = 1;
  1202. } else if (previousState == 3) {
  1203. likesvalue++;
  1204. updateDOMDislikes();
  1205. previousState = 1;
  1206. }
  1207. if (extConfig.numberDisplayReformatLikes === true) {
  1208. const nativeLikes = getLikeCountFromButton();
  1209. if (nativeLikes !== false) {
  1210. setLikes(numberFormat(nativeLikes));
  1211. }
  1212. }
  1213. }
  1214. }
  1215.  
  1216. function dislikeClicked() {
  1217. if (checkForUserAvatarButton() == true) {
  1218. if (previousState == 3) {
  1219. dislikesvalue++;
  1220. updateDOMDislikes();
  1221. previousState = 2;
  1222. } else if (previousState == 2) {
  1223. dislikesvalue--;
  1224. updateDOMDislikes();
  1225. previousState = 3;
  1226. } else if (previousState == 1) {
  1227. likesvalue--;
  1228. dislikesvalue++;
  1229. updateDOMDislikes();
  1230. previousState = 2;
  1231. if (extConfig.numberDisplayReformatLikes === true) {
  1232. const nativeLikes = getLikeCountFromButton();
  1233. if (nativeLikes !== false) {
  1234. setLikes(numberFormat(nativeLikes));
  1235. }
  1236. }
  1237. }
  1238. }
  1239. }
  1240.  
  1241. function setInitialState() {
  1242. setState();
  1243. }
  1244.  
  1245. function getVideoId() {
  1246. const urlObject = new URL(window.location.href);
  1247. const pathname = urlObject.pathname;
  1248. if (pathname.startsWith("/clip")) {
  1249. return document.querySelector("meta[itemprop='videoId']").content;
  1250. } else {
  1251. if (pathname.startsWith("/shorts")) {
  1252. return pathname.slice(8);
  1253. }
  1254. return urlObject.searchParams.get("v");
  1255. }
  1256. }
  1257.  
  1258. function isVideoLoaded() {
  1259. if (isMobile) {
  1260. return document.getElementById("player").getAttribute("loading") == "false";
  1261. }
  1262. const videoId = getVideoId();
  1263.  
  1264. return (
  1265. document.querySelector(`ytd-watch-flexy[video-id='${videoId}']`) !== null
  1266. );
  1267. }
  1268.  
  1269. function roundDown(num) {
  1270. if (num < 1000) return num;
  1271. const int = Math.floor(Math.log10(num) - 2);
  1272. const decimal = int + (int % 3 ? 1 : 0);
  1273. const value = Math.floor(num / 10 ** decimal);
  1274. return value * 10 ** decimal;
  1275. }
  1276.  
  1277. function numberFormat(numberState) {
  1278. let numberDisplay;
  1279. if (extConfig.numberDisplayRoundDown === false) {
  1280. numberDisplay = numberState;
  1281. } else {
  1282. numberDisplay = roundDown(numberState);
  1283. }
  1284. return getNumberFormatter(extConfig.numberDisplayFormat).format(
  1285. numberDisplay
  1286. );
  1287. }
  1288.  
  1289. function getNumberFormatter(optionSelect) {
  1290. let userLocales;
  1291. if (document.documentElement.lang) {
  1292. userLocales = document.documentElement.lang;
  1293. } else if (navigator.language) {
  1294. userLocales = navigator.language;
  1295. } else {
  1296. try {
  1297. userLocales = new URL(
  1298. Array.from(document.querySelectorAll("head > link[rel='search']"))
  1299. ?.find((n) => n?.getAttribute("href")?.includes("?locale="))
  1300. ?.getAttribute("href")
  1301. )?.searchParams?.get("locale");
  1302. } catch {
  1303. cLog(
  1304. "Cannot find browser locale. Use en as default for number formatting."
  1305. );
  1306. userLocales = "en";
  1307. }
  1308. }
  1309.  
  1310. let formatterNotation;
  1311. let formatterCompactDisplay;
  1312. switch (optionSelect) {
  1313. case "compactLong":
  1314. formatterNotation = "compact";
  1315. formatterCompactDisplay = "long";
  1316. break;
  1317. case "standard":
  1318. formatterNotation = "standard";
  1319. formatterCompactDisplay = "short";
  1320. break;
  1321. case "compactShort":
  1322. default:
  1323. formatterNotation = "compact";
  1324. formatterCompactDisplay = "short";
  1325. }
  1326.  
  1327. const formatter = Intl.NumberFormat(userLocales, {
  1328. notation: formatterNotation,
  1329. compactDisplay: formatterCompactDisplay,
  1330. });
  1331. return formatter;
  1332. }
  1333.  
  1334. function getColorFromTheme(voteIsLike) {
  1335. let colorString;
  1336. switch (extConfig.colorTheme) {
  1337. case "accessible":
  1338. if (voteIsLike === true) {
  1339. colorString = "dodgerblue";
  1340. } else {
  1341. colorString = "gold";
  1342. }
  1343. break;
  1344. case "neon":
  1345. if (voteIsLike === true) {
  1346. colorString = "aqua";
  1347. } else {
  1348. colorString = "magenta";
  1349. }
  1350. break;
  1351. case "classic":
  1352. default:
  1353. if (voteIsLike === true) {
  1354. colorString = "lime";
  1355. } else {
  1356. colorString = "red";
  1357. }
  1358. }
  1359. return colorString;
  1360. }
  1361.  
  1362. let smartimationObserver = null;
  1363.  
  1364. function setEventListeners(evt) {
  1365. let jsInitChecktimer;
  1366.  
  1367. function checkForJS_Finish() {
  1368. //console.log();
  1369. if (isShorts() || (getButtons()?.offsetParent && isVideoLoaded())) {
  1370. const buttons = getButtons();
  1371. const dislikeButton = getDislikeButton();
  1372.  
  1373. if (preNavigateLikeButton !== getLikeButton() && dislikeButton) {
  1374. cLog("Registering button listeners...");
  1375. try {
  1376. getLikeButton().addEventListener("click", likeClicked);
  1377. dislikeButton?.addEventListener("click", dislikeClicked);
  1378. getLikeButton().addEventListener("touchstart", likeClicked);
  1379. dislikeButton?.addEventListener("touchstart", dislikeClicked);
  1380. dislikeButton?.addEventListener("focusin", updateDOMDislikes);
  1381. dislikeButton?.addEventListener("focusout", updateDOMDislikes);
  1382. preNavigateLikeButton = getLikeButton();
  1383.  
  1384. if (!smartimationObserver) {
  1385. smartimationObserver = createObserver({
  1386. attributes: true,
  1387. subtree: true
  1388. }, updateDOMDislikes);
  1389. smartimationObserver.container = null;
  1390. }
  1391.  
  1392. const smartimationContainer = buttons.querySelector('yt-smartimation');
  1393. if (smartimationContainer &&
  1394. smartimationObserver.container != smartimationContainer)
  1395. {
  1396. cLog("Initializing smartimation mutation observer");
  1397. smartimationObserver.disconnect();
  1398. smartimationObserver.observe(smartimationContainer);
  1399. smartimationObserver.container = smartimationContainer;
  1400. }
  1401. } catch {
  1402. return;
  1403. } //Don't spam errors into the console
  1404. }
  1405. if (dislikeButton) {
  1406. setInitialState();
  1407. clearInterval(jsInitChecktimer);
  1408. }
  1409. }
  1410. }
  1411.  
  1412. cLog("Setting up...");
  1413. jsInitChecktimer = setInterval(checkForJS_Finish, 111);
  1414. }
  1415.  
  1416. (function () {
  1417. "use strict";
  1418. window.addEventListener("yt-navigate-finish", setEventListeners, true);
  1419. setEventListeners();
  1420. })();
  1421. if (isMobile) {
  1422. let originalPush = history.pushState;
  1423. history.pushState = function (...args) {
  1424. window.returnDislikeButtonlistenersSet = false;
  1425. setEventListeners(args[2]);
  1426. return originalPush.apply(history, args);
  1427. };
  1428. setInterval(() => {
  1429. const dislikeButton = getDislikeButton();
  1430. if(dislikeButton?.querySelector(".button-renderer-text") === null){
  1431. getDislikeTextContainer().innerText = mobileDislikes;
  1432. }
  1433. else{
  1434. if (dislikeButton) dislikeButton.querySelector(".button-renderer-text").innerText =
  1435. mobileDislikes;
  1436. }
  1437. }, 1000);
  1438. }
  1439.