Youtube Pro | v4

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