Browser extension to watch YouTube videos without the distracting cruft around it, in the full window.

youtubecinema/extension/ background.js
195 lines
5.8 KiB

  1. // Be compatible with Chrome|ium. We do not need the full webextension-polyfill.
  2. if (typeof browser === 'undefined') {
  3. this.browser = chrome;
  4. }
  5. // Keep a set of tabIds for which the user disabled the cinema mode (with the pageAction button)
  6. var tabsWithCinemaModeDisabled = {};
  7. function hasCinemaModeEnabled(tabId) {
  8. return !tabsWithCinemaModeDisabled[tabId];
  9. }
  10. function appearsToHaveCinemaModeEnabled(tabId, url) {
  11. return (
  12. hasCinemaModeEnabled(tabId)
  13. // When viewing an embedded video with cinema mode disabled, behave as if it was enabled.
  14. || isEmbeddedVideo(url)
  15. );
  16. }
  17. // Our request filters should make use of this test unnecessary, but I prefer to keep it explicit.
  18. function isYoutube(url) {
  19. return new URL(url).hostname.endsWith('.youtube.com');
  20. }
  21. function isEmbeddedVideo(url) {
  22. return (
  23. isYoutube(url)
  24. && new URL(url).pathname.startsWith('/embed/')
  25. );
  26. }
  27. function isCruftedVideo(url) {
  28. return (
  29. isYoutube(url)
  30. && new URL(url).pathname === '/watch'
  31. );
  32. }
  33. function timeStringToSeconds(t) {
  34. // If t is an integer, it indicates the number of seconds.
  35. if (/^\d+$/.test(t)) {
  36. return t;
  37. }
  38. // If t looks like e.g. 1h20m30s, convert to seconds.
  39. match = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/)
  40. if (match) {
  41. var seconds = (
  42. (Number(match[3]) || 0)
  43. + (Number(match[2]) || 0) * 60
  44. + (Number(match[1]) || 0) * 3600
  45. )
  46. return seconds.toString();
  47. }
  48. // No idea. Let's just return it.
  49. return t
  50. }
  51. function cruftedToEmbeddableVideoUrl(url) {
  52. url = new URL(url);
  53. // Move the video's id into the path
  54. var videoId = url.searchParams.get('v');
  55. url.pathname = '/embed/' + videoId;
  56. url.searchParams.delete('v');
  57. // Rename start time parameter if present, converting to seconds if needed.
  58. var startTime = url.searchParams.get('t');
  59. if (startTime !== null) {
  60. url.searchParams.set('start', timeStringToSeconds(startTime))
  61. url.searchParams.delete('t')
  62. }
  63. // Tweak some other settings for pleasant viewing. See API docs: https://developers.google.com/youtube/player_parameters
  64. url.searchParams.set('rel', '0'); // no suggestions after my video, please.
  65. url.searchParams.set('iv_load_policy', '3'); // no video annotations, thanks.
  66. url.searchParams.set('modestbranding', '1'); // no YouTube branding either.
  67. url.searchParams.set('autoplay', '1'); // do play my video! :)
  68. return url.href;
  69. }
  70. function embeddableToCruftedVideoUrl(url) {
  71. url = new URL(url);
  72. // Move the video's id into the query parameters.
  73. var videoId = url.pathname.match(/\/embed\/(.*)/)[1];
  74. url.pathname = '/watch';
  75. url.searchParams.set('v', videoId);
  76. // Rename start time parameter if present.
  77. var startTime = url.searchParams.get('start');
  78. if (startTime !== null) {
  79. url.searchParams.set('t', startTime)
  80. url.searchParams.delete('start')
  81. }
  82. // Remove added settings specific to embedded videos.
  83. url.searchParams.delete('rel');
  84. url.searchParams.delete('modestbranding');
  85. url.searchParams.delete('iv_load_policy');
  86. url.searchParams.delete('autoplay');
  87. return url.href;
  88. }
  89. // Turn a video url into its embeddable (full-window) version, or vice versa if not in cinema mode.
  90. function makeNewUrl(url, cinemaModeEnabled) {
  91. if (cinemaModeEnabled) {
  92. if (isCruftedVideo(url)) {
  93. return cruftedToEmbeddableVideoUrl(url);
  94. }
  95. }
  96. else {
  97. if (isEmbeddedVideo(url)) {
  98. return embeddableToCruftedVideoUrl(url);
  99. }
  100. }
  101. return undefined;
  102. }
  103. // Redirect crufted videos to their full-window version.
  104. function onBeforeRequestListener(details) {
  105. if (hasCinemaModeEnabled(details.tabId)) {
  106. var newUrl = makeNewUrl(details.url, true);
  107. // Return if we were not visiting a embedded youtube video
  108. if (newUrl === undefined) return;
  109. return {redirectUrl: newUrl};
  110. }
  111. }
  112. browser.webRequest.onBeforeRequest.addListener(
  113. onBeforeRequestListener,
  114. {urls: ["*://*.youtube.com/watch*"]},
  115. ['blocking']
  116. );
  117. // Show the pageAction button if looking at a video (either with or without cruft).
  118. var youtubeUrlFilter = {url: [{hostSuffix: '.youtube.com'}]}
  119. browser.webNavigation.onCommitted.addListener(handleNavigation, youtubeUrlFilter);
  120. browser.webNavigation.onHistoryStateUpdated.addListener(handleNavigation, youtubeUrlFilter);
  121. function handleNavigation(details) {
  122. if (details.frameId !== 0) {
  123. return;
  124. }
  125. // If we are on youtube, show the button.
  126. if (isYoutube(details.url)) {
  127. // Show the pageAction button.
  128. browser.pageAction.show(details.tabId);
  129. setIconActive(details.tabId, appearsToHaveCinemaModeEnabled(details.tabId, details.url))
  130. // In Chrome|ium, listeners stay across page changes, in Firefox they don't. So check first.
  131. if (!browser.pageAction.onClicked.hasListener(handlePageAction))
  132. browser.pageAction.onClicked.addListener(handlePageAction);
  133. }
  134. }
  135. // Enable/Disable cinema mode when the pageAction button is clicked.
  136. function handlePageAction(tab) {
  137. var enable = !appearsToHaveCinemaModeEnabled(tab.id, tab.url)
  138. if (enable) {
  139. delete tabsWithCinemaModeDisabled[tab.id];
  140. } else {
  141. tabsWithCinemaModeDisabled[tab.id] = true;
  142. }
  143. setIconActive(tab.id, enable);
  144. // Relocate this page to reflect the new mode.
  145. var newUrl = makeNewUrl(tab.url, hasCinemaModeEnabled(tab.id));
  146. if (newUrl)
  147. browser.tabs.update(tab.id, {url: newUrl});
  148. }
  149. var activeIcons = {
  150. '19': '/img/icon_active/19.png',
  151. '38': '/img/icon_active/38.png',
  152. '48': '/img/icon_active/48.png',
  153. '96': '/img/icon_active/96.png',
  154. '128': '/img/icon_active/128.png',
  155. }
  156. var inactiveIcons = {
  157. '19': '/img/icon_inactive/19.png',
  158. '38': '/img/icon_inactive/38.png',
  159. '48': '/img/icon_inactive/48.png',
  160. '96': '/img/icon_inactive/96.png',
  161. '128': '/img/icon_inactive/128.png',
  162. }
  163. function setIconActive(tabId, active) {
  164. browser.pageAction.setIcon({
  165. tabId: tabId,
  166. path: active ? activeIcons : inactiveIcons,
  167. })
  168. }