YouTube Qualities Size

Shows file size for each quality in YouTube

  1. // ==UserScript==
  2. // @name YouTube Qualities Size
  3. // @namespace ytqz.smz.k
  4. // @version 1.2.6
  5. // @description Shows file size for each quality in YouTube
  6. // @author Abdelrahman Khalil
  7. // @match https://www.youtube.com/*
  8. // @license MIT
  9. // ==/UserScript==
  10.  
  11. ;(() => {
  12. const DEFAULT_CODEC = 'vp9'
  13. const ALT_CODEC = 'avc1'
  14. const CACHE = {}
  15.  
  16. // --------------------------------
  17. // API Stuff.
  18. // --------------------------------
  19. // NOT WORKING
  20. /*const fetchInfo = (videoId, detailpage = false) => {
  21. let url = `https://www.youtube.com/get_video_info?video_id=${videoId}&html5=1`
  22.  
  23. // Retrive full info when video is copyrighted.
  24. if (detailpage) url += '&el=detailpage'
  25.  
  26. return fetch(url)
  27. }
  28.  
  29. // Youtube sends data as ugly ass url queries.
  30. // Parse it using URLSearchParams then get value of player_response which is stringified JSON.
  31. const parseInfo = uglyInfo =>
  32. JSON.parse(getQuery(uglyInfo, 'player_response')).streamingData
  33. .adaptiveFormats
  34. */
  35. const getFormats = async videoId => {
  36. /*let info = await fetchInfo(videoId).then(res => res.text())
  37.  
  38. if (!info.includes('adaptiveFormats'))
  39. info = await fetchInfo(videoId, true).then(res => res.text())
  40.  
  41. return parseInfo(info)*/
  42.  
  43. //return ytplayer.config.args.raw_player_response.streamingData.adaptiveFormats
  44. console.clear()
  45. console.log(ytplayer)
  46.  
  47. const body = {
  48. videoId,
  49. context: {
  50. client: {
  51. clientName: 'WEB',
  52. clientVersion: ytcfg.data_.INNERTUBE_CLIENT_VERSION
  53. }
  54. }
  55. }
  56.  
  57. return await fetch("https://www.youtube.com/youtubei/v1/player?key=" + ytcfg.data_.INNERTUBE_API_KEY, {method: "POST", body: JSON.stringify(body)}).then(res => res.json()).then(data => data.streamingData
  58. .adaptiveFormats)
  59. }
  60.  
  61. // YouTube separates audio and video.
  62. const getAudioContentLength = formats =>
  63. formats.find(
  64. f =>
  65. f.audioQuality === 'AUDIO_QUALITY_MEDIUM' &&
  66. f.mimeType.includes('opus')
  67. ).contentLength || 0
  68.  
  69. // Filter formats per quality.
  70. // Returns video content Length for all codecs summed by opus medium-quality audio content length.
  71. const mapFormats = (formats, qualityLabel, audioCL) =>
  72. formats
  73. .filter(f => f.qualityLabel === qualityLabel && f.contentLength)
  74. .map(vf => ({
  75. [matchCodec(vf.mimeType)]: toMBytes(vf.contentLength, audioCL)
  76. }))
  77. .reduce((r, c) => Object.assign(r, c), {})
  78.  
  79. // --------------------------------
  80. // DOM Stuff.
  81. // --------------------------------
  82. const createYQSNode = mappedFormats => {
  83. let YQSElement = document.createElement('yt-quality-size')
  84. let isRTL = document.body.getAttribute('dir') === 'rtl'
  85.  
  86. YQSElement.style.float = isRTL ? 'left' : 'right'
  87. YQSElement.style.textAlign = 'right'
  88. YQSElement.style.marginRight = '8px'
  89. YQSElement.setAttribute('dir', 'ltr')
  90.  
  91. YQSElement.setAttribute('title', objectStringify(mappedFormats))
  92.  
  93. let textNode = document.createTextNode(
  94. mappedFormats[DEFAULT_CODEC] || mappedFormats[ALT_CODEC] || ''
  95. )
  96.  
  97. YQSElement.appendChild(textNode)
  98.  
  99. return YQSElement
  100. }
  101.  
  102. // Hook YQS element to each quality.
  103. const hookYQS = async addedNode => {
  104. let doesYQMExist =
  105. addedNode && addedNode.classList.contains('ytp-quality-menu'),
  106. doesYQSNotExist = !document.querySelector('yt-quality-size')
  107.  
  108. if (doesYQMExist && doesYQSNotExist) {
  109. let YQM = addedNode,
  110. videoId = getQuery(location.search, 'v')
  111.  
  112. if (!CACHE[videoId]) CACHE[videoId] = await getFormats(videoId)
  113.  
  114. let formats = CACHE[videoId],
  115. qualitiesNode = YQM.querySelectorAll('span'),
  116. audioCL = getAudioContentLength(formats)
  117.  
  118. qualitiesNode.forEach((qualityNode, index) => {
  119. if (index === qualitiesNode.length - 1) return
  120.  
  121. let qualityLabel = matchQLabel(qualityNode.textContent),
  122. mappedFormats = mapFormats(formats, qualityLabel, audioCL),
  123. YQSNode = createYQSNode(mappedFormats)
  124.  
  125. qualityNode.appendChild(YQSNode)
  126. })
  127. }
  128. }
  129.  
  130. // Listen to page navigation and observe settings-menu if it's /watch.
  131. const onPageUpdate = () => {
  132. let SettingsMenuElement = document.querySelector('.ytp-settings-menu')
  133.  
  134. if (SettingsMenuElement) {
  135. removeEventListener('yt-page-data-updated', onPageUpdate)
  136. new MutationObserver(([{ addedNodes }]) => {
  137. hookYQS(addedNodes[0])
  138. }).observe(SettingsMenuElement, { childList: true })
  139. }
  140. }
  141.  
  142. addEventListener('yt-page-data-updated', onPageUpdate)
  143.  
  144. // --------------------------------
  145. // Utils
  146. // --------------------------------
  147. const getQuery = (string, query) => new URLSearchParams(string).get(query)
  148.  
  149. const matchCodec = mimeType => mimeType.replace(/(?!=").+="|\..+|"/g, '')
  150. const matchQLabel = qLabel => qLabel.replace(/\s.+/, '')
  151.  
  152. const toMBytes = (vCL, aCL) => {
  153. let videoAudioMB = (parseInt(vCL) + parseInt(aCL)) / 1048576
  154. return (Math.round(videoAudioMB) || videoAudioMB.toFixed(2)) + ' MB'
  155. }
  156.  
  157. const objectStringify = obj =>
  158. JSON.stringify(obj, null, '‎')
  159. .replace(/{|}|"|,/g, '')
  160. .trim()
  161. })()