Watch9 Reconstruct

Restores the old watch layout from before 2019

  1. // ==UserScript==
  2. // @name Watch9 Reconstruct
  3. // @version 2.5.0
  4. // @description Restores the old watch layout from before 2019
  5. // @author Aubrey P.
  6. // @icon https://www.youtube.com/favicon.ico
  7. // @namespace aubymori
  8. // @license Unlicense
  9. // @match www.youtube.com/*
  10. // @grant none
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. const w9rOptions = {
  15. oldAutoplay: true, // Classic autoplay renderer with "Up next" text
  16. removeBloatButtons: true // Removes "Clip", "Thanks", "Download", etc.
  17. }
  18.  
  19. /**
  20. * Localization strings.
  21. *
  22. * See LOCALIZATION.md in the GitHub repo.
  23. */
  24. const w9ri18n = {
  25. en: {
  26. subSuffixMatch: /( subscribers)|( subscriber)/,
  27. nonPublishMatch: /(Premier)|(Stream)|(Start)/,
  28. publishedOn: "Published on %s",
  29. uploadedOn: "Uploaded on %s",
  30. upNext: "Up next",
  31. autoplay: "Autoplay",
  32. autoplayTip: "When autoplay is enabled, a suggested video will automatically play next."
  33. },
  34. ja: {
  35. subSuffixMatch: /(チャンネル登録者数 )|(人)/g,
  36. nonPublishMatch: /(公開済)|(開始済)/g,
  37. publishedOn: "%s に公開",
  38. uploadedOn: "%s にアップロード",
  39. upNext: "自動再生",
  40. autoplay: "次の動画",
  41. autoplayTip: "自動再生を有効にすると、関連動画が自動的に再生されます。"
  42. },
  43. pl: {
  44. subSuffixMatch: /( subskrybentów)|( subskrybent)/,
  45. nonPublishMatch: /(Data premiery: )|(adawane na żywo )|(Transmisja zaczęła się )/,
  46. publishedOn: "Przesłany %s",
  47. uploadedOn: "Przesłany %s",
  48. upNext: "Następny",
  49. autoplay: "Autoodtwarzanie",
  50. autoplayTip: "Jeśli masz włączone autoodtwarzanie, jako następny włączy się automatycznie proponowany film."
  51. },
  52. fil: {
  53. subSuffixMatch: /(na)|( subscribers)|( subscriber)|(\s)/g,
  54. nonPublishMatch: /(simula)/,
  55. publishedOn: "Na-publish noong %s",
  56. uploadedOn: "Na-upload noong %s",
  57. upNext: "Susunod",
  58. autoplay: "I-autoplay",
  59. autoplayTip: "Kapag naka-enable ang autoplay, awtomatikong susunod na magpe-play ang isang iminumungkahing video."
  60. },
  61. fr: {
  62. subSuffixMatch: /( abonnés)|( abonné)|( d’abonnés)|( d’abonné)/g,
  63. nonPublishMatch: /(Diffus)|(Sortie)/g,
  64. publishedOn: "Publiée le %s",
  65. uploadedOn: "Mise en ligne le %s",
  66. upNext: "À suivre",
  67. autoplay: "Lecture automatique",
  68. autoplayTip: "Lorsque cette fonctionnalité est activée, une vidéo issue des suggestions est automatiquement lancée à la suite de la lecture en cours."
  69. },
  70. es: {
  71. subSuffixMatch: /( de suscriptores)|( suscriptor)/g,
  72. nonPublishMatch: /(directo)|(Fecha)/g,
  73. publishedOn: "Publicado el %s",
  74. uploadedOn: "Subido el %s",
  75. upNext: "A continuación",
  76. autoplay: "Reproducción automática",
  77. autoplayTip: "Si la reproducción automática está habilitada, se reproducirá automáticamente un vídeo a continuación."
  78. },
  79. pt: {
  80. subSuffixMatch: /( de subscritores)|( subscritor)/g,
  81. nonPublishMatch: /(Stream)|(Estreou)/g,
  82. publishedOn: "Publicado a %s",
  83. uploadedOn: "Carregado a %s",
  84. upNext: "Próximo",
  85. autoplay: "Reprodução automática",
  86. autoplayTip: "Quando a reprodução automática é ativada, um vídeo sugerido será executado automaticamente em seguida."
  87. },
  88. ru: {
  89. subSuffixMatch: /( подписчиков)|( подписчик)/g,
  90. nonPublishMatch: /(Сейчас смотрят:)|(Прямой эфир состоялся)|(Дата премьеры:)/g,
  91. publishedOn: "Дата публикации: %s",
  92. uploadedOn: "Дата публикации: %s",
  93. upNext: "Следующее видео",
  94. autoplay: "Автовоспроизведение",
  95. autoplayTip: "Если функция включена, то следующий ролик начнет воспроизводиться автоматически."
  96. }
  97. };
  98.  
  99. /**
  100. * Wait for a selector to exist
  101. *
  102. * @param {string} selector CSS Selector
  103. * @param {HTMLElement} base Element to search inside
  104. * @returns {Node}
  105. */
  106. async function waitForElm(selector, base = document) {
  107. if (!selector) return null;
  108. if (!base.querySelector) return null;
  109. while (base.querySelector(selector) == null) {
  110. await new Promise(r => requestAnimationFrame(r));
  111. };
  112. return base.querySelector(selector);
  113. };
  114.  
  115. /**
  116. * Get a string from the localization strings.
  117. *
  118. * @param {string} string Name of string to get
  119. * @param {string} hl Language to use.
  120. * @returns {string}
  121. */
  122. function getString(string, hl = "en") {
  123. if (!string) return "ERROR";
  124. if (w9ri18n[hl]) {
  125. if (w9ri18n[hl][string]) {
  126. return w9ri18n[hl][string];
  127. } else if (w9ri18n.en[string]) {
  128. return w9ri18n.en[string];
  129. } else {
  130. return "ERROR";
  131. }
  132. } else {
  133. if (w9ri18n.en[string]) return w9ri18n.en[string];
  134. return "ERROR";
  135. }
  136. }
  137.  
  138. /**
  139. * Format upload date string to include "Published on" or "Uploaded on" if applicable.
  140. *
  141. * @param {string} dateStr dateText from InnerTube ("Sep 13, 2022", "Premiered 2 hours ago", etc.)
  142. * @param {boolean} isPublic Is the video public?
  143. * @param {string} hl Language to use.
  144. * @returns {string}
  145. */
  146. function formatUploadDate(dateStr, isPublic, hl = "en") {
  147. var nonPublishMatch = getString("nonPublishMatch", hl);
  148. var string = isPublic ? getString("publishedOn", hl) : getString("uploadedOn", hl);
  149. if (nonPublishMatch.test(dateStr)) {
  150. return dateStr;
  151. } else {
  152. return string.replace("%s", dateStr);
  153. }
  154. }
  155.  
  156. /**
  157. * Format subscriber count string to only include count.
  158. *
  159. * @param {string} count Subscriber count string from InnerTube ("374K subscribers", "No subscribers", etc.)
  160. * @param {string} hl Language to use.
  161. * @returns {string}
  162. */
  163. function formatSubCount(count, hl = "en") {
  164. if (count == null) return "";
  165. var tmp = count.replace(getString("subSuffixMatch", hl), "");
  166. return tmp;
  167. }
  168.  
  169. /**
  170. * Parse document.cookie
  171. *
  172. * @returns {object}
  173. */
  174. function parseCookies() {
  175. var c = document.cookie.split(";"), o = {};
  176. for (var i = 0, j = c.length; i < j; i++) {
  177. var s = c[i].split("=");
  178. var n = s[0].replace(" ", "");
  179. s.splice(0, 1);
  180. s = s.join("=");
  181. o[n] = s;
  182. }
  183. return o;
  184. }
  185.  
  186. /**
  187. * Parse YouTube's PREF cookie.
  188. *
  189. * @param {string} pref PREF cookie content
  190. * @returns {object}
  191. */
  192. function parsePref(pref) {
  193. var a = pref.split("&"), o = {};
  194. for (var i = 0, j = a.length; i < j; i++) {
  195. var b = a[i].split("=");
  196. o[b[0]] = b[1];
  197. }
  198. return o;
  199. }
  200.  
  201. /**
  202. * Is autoplay enabled?
  203. *
  204. * @returns {boolean}
  205. */
  206. function autoplayState() {
  207. var cookies = parseCookies();
  208. if (cookies.PREF) {
  209. var pref = parsePref(cookies.PREF);
  210. if (pref.f5) {
  211. return !(pref.f5 & 8192)
  212. } else {
  213. return true; // default
  214. }
  215. } else {
  216. return true;
  217. }
  218. }
  219.  
  220. /**
  221. * Toggle autoplay.
  222. *
  223. * @returns {void}
  224. */
  225. function clickAutoplay() {
  226. var player = document.querySelector("#movie_player");
  227. var autoplay;
  228. if (autoplay = player.querySelector(".ytp-autonav-toggle-button-container")) {
  229. autoplay.parentNode.click();
  230. } else {
  231. var settings = player.querySelector('.ytp-settings-button');
  232. settings.click();settings.click();
  233. var item = player.querySelector('.ytp-menuitem[role="menuitemcheckbox"]');
  234. item.click();
  235. }
  236. }
  237.  
  238. /**
  239. * Should the Autoplay renderer be inserted?
  240. * (Basically, if there's a playlist active)
  241. *
  242. * @returns {boolean}
  243. */
  244. function shouldHaveAutoplay() {
  245. var playlist;
  246. if (playlist = document.querySelector("#playlist.ytd-watch-flexy")) {
  247. if (playlist.hidden && playlist.hidden == true) {
  248. return true;
  249. } else {
  250. return false;
  251. }
  252. } else {
  253. return true;
  254. }
  255. }
  256.  
  257. /**
  258. * Is a value in an array?
  259. *
  260. * @param {*} needle Value to search
  261. * @param {Array} haystack Array to search
  262. * @returns {boolean}
  263. */
  264. function inArray(needle, haystack) {
  265. for (var i = 0; i < haystack.length; i++) {
  266. if (needle == haystack[i]) return true;
  267. }
  268. return false;
  269. }
  270.  
  271. /**
  272. * Remove bloaty action buttons.
  273. *
  274. * @returns {void}
  275. */
  276. function removeBloatButtons() {
  277. var primaryInfo = document.querySelector("ytd-video-primary-info-renderer");
  278. var actionBtns = primaryInfo.data.videoActions.menuRenderer.topLevelButtons;
  279.  
  280. // Remove the action buttons accordingly.
  281. for (var i = 0; i < actionBtns.length; i++) {
  282. if (actionBtns[i].downloadButtonRenderer) {
  283. actionBtns.splice(i, 1);
  284. i--;
  285. } else if (actionBtns[i].buttonRenderer) {
  286. if (inArray(actionBtns[i].buttonRenderer.icon.iconType, ["MONEY_HEART", "CONTENT_CUT"])) {
  287. actionBtns.splice(i, 1);
  288. i--;
  289. }
  290. }
  291. }
  292.  
  293. // Refresh the primary info's data.
  294. var tmp = primaryInfo.data;
  295. primaryInfo.data = {};
  296. primaryInfo.data = tmp;
  297. }
  298.  
  299. /**
  300. * Is the current video public? Or is it unlisted/private?
  301. *
  302. * @returns {boolean}
  303. */
  304. function isVideoPublic() {
  305. const primaryInfo = document.querySelector("ytd-video-primary-info-renderer");
  306. if (primaryInfo.data.badges == null) return true;
  307. const badges = primaryInfo.data.badges;
  308.  
  309. for (var i = 0; i < badges.length; i++) {
  310. var iconType = badges[i].metadataBadgeRenderer.icon.iconType;
  311. if (iconType == "PRIVACY_UNLISTED" || iconType == "PRIVACY_PRIVATE") {
  312. return false;
  313. }
  314. }
  315. return true;
  316. }
  317.  
  318. /**
  319. * Get sidebar data.
  320. *
  321. * @returns {object}
  322. */
  323. async function getSidebarData() {
  324. const secondaryResults = document.querySelector("ytd-watch-next-secondary-results-renderer");
  325. const resultData = secondaryResults.data.results;
  326. var response = {};
  327.  
  328. if (yt.config_.LOGGED_IN == false) {
  329. response.element = await waitForElm("#items.ytd-watch-next-secondary-results-renderer");
  330. response.data = resultData;
  331. response.class = "ytd-watch-next-secondary-results-renderer";
  332. return response;
  333. } else {
  334. var tmp;
  335. if (tmp = resultData[0].relatedChipCloudRenderer) {
  336. response.element = await waitForElm("#contents.ytd-item-section-renderer", secondaryResults);
  337. response.data = resultData[1].itemSectionRenderer.contents;
  338. response.class = "ytd-item-section-renderer";
  339. return response;
  340. } else {
  341. response.element = await waitForElm("#items.ytd-watch-next-secondary-results-renderer");
  342. response.data = resultData;
  343. response.class = "ytd-watch-next-secondary-results-renderer";
  344. return response;
  345. }
  346. }
  347. }
  348.  
  349. /**
  350. * Build the classic compact autoplay renderer.
  351. *
  352. * @returns {void}
  353. */
  354. async function buildAutoplay() {
  355. // Prevent it from building autoplay twice
  356. if (document.querySelector("ytd-compact-autoplay-renderer") != null) return;
  357.  
  358. const watchFlexy = document.querySelector("ytd-watch-flexy");
  359. const sidebarItems = await getSidebarData();
  360. const language = yt.config_.HL.split("-")[0] ?? "en";
  361. const autoplayStub = `
  362. <ytd-compact-autoplay-renderer class="style-scope ${ sidebarItems.class }">
  363. <div id="head" class="style-scope ytd-compact-autoplay-renderer">
  364. <div id="upnext" class="style-scope ytd-compact-autoplay-renderer"></div>
  365. <div id="autoplay" class="style-scope ytd-compact-autoplay-renderer"></div>
  366. <tp-yt-paper-toggle-button id="toggle" noink="" class="style-scope ytd-compact-autoplay-renderer" role="button" aria-pressed="" tabindex="0" style="touch-action: pan-y;" toggles="" aria-disabled="false" aria-label="">
  367. <tp-yt-paper-tooltip id="tooltip" class="style-scope ytd-compact-autoplay-renderer" role="tooltip" tabindex="-1">${ getString("autoplayTip", language) }</tp-yt-paper-tooltip>
  368. </tp-yt-paper-toggle-button>
  369. </div>
  370. <div id="contents" class="style-scope ytd-compact-autoplay-renderer"></div>
  371. </ytd-compact-autoplay-renderer>
  372. `;
  373.  
  374.  
  375. // Insert the autoplay stub.
  376. sidebarItems.element.insertAdjacentHTML("beforebegin", autoplayStub);
  377. var autoplayRenderer = sidebarItems.element.parentNode.querySelector("ytd-compact-autoplay-renderer");
  378.  
  379. // Apply the appropriate localized text.
  380. autoplayRenderer.querySelector("#upnext").innerText = getString("upNext", language);
  381. autoplayRenderer.querySelector("#autoplay").innerText = getString("autoplay", language);
  382.  
  383. // Add event listener to toggle
  384. autoplayRenderer.querySelector("#toggle").addEventListener("click", clickAutoplay);
  385.  
  386. // Copy first video from data into autoplay renderer
  387. var firstVideo;
  388. for (var i = 0; i < sidebarItems.data.length; i++) {
  389. if (sidebarItems.data[i].compactVideoRenderer) {
  390. firstVideo = sidebarItems.data[i];
  391. break;
  392. }
  393. }
  394.  
  395. var videoRenderer = document.createElement("ytd-compact-video-renderer");
  396. videoRenderer.data = firstVideo.compactVideoRenderer;
  397. videoRenderer.classList.add("style-scope", "ytd-compact-autoplay-renderer")
  398. videoRenderer.setAttribute("lockup", "true");
  399. videoRenderer.setAttribute("thumbnail-width", "168");
  400. autoplayRenderer.querySelector("#contents").appendChild(videoRenderer);
  401.  
  402. // Add the interval to update toggle if it isn't already.
  403. if (!watchFlexy.getAttribute("autoplay-interval-active")) {
  404. setInterval(() => {
  405. if (autoplayState()) {
  406. autoplayRenderer.querySelector("#toggle").setAttribute("checked", "");
  407. } else {
  408. autoplayRenderer.querySelector("#toggle").removeAttribute("checked");
  409. }
  410. }, 100);
  411. }
  412. }
  413.  
  414. /**
  415. * Build new Watch9 elements and tweak currently existing elements accordingly.
  416. *
  417. * @returns {void}
  418. */
  419. function buildWatch9() {
  420. const watchFlexy = document.querySelector("ytd-watch-flexy");
  421. const primaryInfo = watchFlexy.querySelector("ytd-video-primary-info-renderer");
  422. const secondaryInfo = watchFlexy.querySelector("ytd-video-secondary-info-renderer");
  423. const viewCount = primaryInfo.querySelector("ytd-video-view-count-renderer");
  424. const subBtn = secondaryInfo.querySelector("#subscribe-button tp-yt-paper-button");
  425. const uploadDate = secondaryInfo.querySelector(".date.ytd-video-secondary-info-renderer"); // Old unused element that we inject the date into
  426. const language = yt.config_.HL.split("-")[0] ?? "en";
  427.  
  428. // Let script know we've done this initial build
  429. watchFlexy.setAttribute("watch9-built", "");
  430.  
  431. // Publish date
  432. var newUploadDate = formatUploadDate(primaryInfo.data.dateText.simpleText, isVideoPublic(), language);
  433. uploadDate.innerText = newUploadDate;
  434.  
  435. // Sub count
  436. var newSubCount;
  437. if (secondaryInfo.data.owner.videoOwnerRenderer.subscriberCountText) {
  438. newSubCount = formatSubCount(secondaryInfo.data.owner.videoOwnerRenderer.subscriberCountText.simpleText, language);
  439. } else {
  440. newSubCount = "0";
  441. }
  442. var w9rSubCount = document.createElement("yt-formatted-string");
  443. w9rSubCount.classList.add("style-scope", "deemphasize");
  444. w9rSubCount.text = {
  445. simpleText: newSubCount
  446. };
  447. subBtn.insertAdjacentElement("beforeend", w9rSubCount);
  448.  
  449. // Bloat buttons
  450. if (w9rOptions.removeBloatButtons) removeBloatButtons();
  451.  
  452. // Autoplay
  453. if (w9rOptions.oldAutoplay && shouldHaveAutoplay()) buildAutoplay();
  454. }
  455.  
  456. /**
  457. * Update currently existing Watch9 elements.
  458. *
  459. * @returns {void}
  460. */
  461. function updateWatch9() {
  462. const primaryInfo = document.querySelector("ytd-video-primary-info-renderer");
  463. const secondaryInfo = document.querySelector("ytd-video-secondary-info-renderer");
  464. const subCnt = secondaryInfo.querySelector("yt-formatted-string.deemphasize");
  465. const uploadDate = secondaryInfo.querySelector(".date.ytd-video-secondary-info-renderer");
  466. const language = yt.config_.HL.split("-")[0] ?? "en";
  467.  
  468. // Publish date
  469. var newUploadDate = formatUploadDate(primaryInfo.data.dateText.simpleText, isVideoPublic(), language);
  470. uploadDate.innerText = newUploadDate;
  471.  
  472. // Sub count
  473. var newSubCount = formatSubCount(secondaryInfo.data.owner.videoOwnerRenderer.subscriberCountText.simpleText, language);
  474. subCnt.text = {
  475. simpleText: newSubCount
  476. };
  477.  
  478. // Bloat buttons
  479. if (w9rOptions.removeBloatButtons) removeBloatButtons();
  480.  
  481. // Autoplay
  482. if (w9rOptions.oldAutoplay && shouldHaveAutoplay()) buildAutoplay();
  483. }
  484.  
  485. /**
  486. * Run the Watch9 build/update functions.
  487. */
  488. document.addEventListener("yt-page-data-updated", (e) => {
  489. if (e.detail.pageType == "watch") {
  490. if (document.querySelector("ytd-compact-autoplay-renderer")) {
  491. document.querySelector("ytd-compact-autoplay-renderer").remove();
  492. }
  493.  
  494. if (document.querySelector("ytd-watch-flexy").getAttribute("watch9-built") != null) {
  495. updateWatch9();
  496. } else {
  497. buildWatch9();
  498. }
  499. }
  500. });
  501.  
  502. /**
  503. * Inject styles.
  504. */
  505. document.addEventListener("DOMContentLoaded", function tmp() {
  506. document.head.insertAdjacentHTML("beforeend", `
  507. <style id="watch9-fix">
  508. /* Hide Watch11 */
  509. ytd-watch-metadata {
  510. display: none !important;
  511. }
  512.  
  513. /* Force Watch10 to display */
  514. #meta-contents[hidden],
  515. #info-contents[hidden] {
  516. display: block !important;
  517. }
  518.  
  519. ytd-video-view-count-renderer[small] {
  520. font-size: 1.6rem !important;
  521. line-height: 2.2rem !important;
  522. }
  523.  
  524. yt-formatted-string.deemphasize {
  525. opacity: .85;
  526. margin-left: 6px;
  527. }
  528.  
  529. yt-formatted-string.deemphasize:empty {
  530. margin-left: 0;
  531. }
  532.  
  533. /**
  534. * Prevent sub count from appearing on the "Edit video" button since
  535. * it uses the same element as subscribe button
  536. */
  537. ytd-button-renderer.style-primary yt-formatted-string.deemphasize {
  538. display: none;
  539. }
  540.  
  541. #info-strings.ytd-video-primary-info-renderer,
  542. #owner-sub-count.ytd-video-owner-renderer {
  543. display: none !important;
  544. }
  545. </style>
  546. `);
  547. if (w9rOptions.oldAutoplay) document.head.insertAdjacentHTML("beforeend", `
  548. <style id="compact-autoplay-fix">
  549. yt-related-chip-cloud-renderer {
  550. display: none;
  551. }
  552.  
  553. ytd-compact-autoplay-renderer {
  554. padding-bottom: 8px;
  555. border-bottom: 1px solid var(--yt-spec-10-percent-layer);
  556. margin-bottom: 16px;
  557. display: flex;
  558. flex-direction: column;
  559. }
  560.  
  561. ytd-compact-autoplay-renderer ytd-compact-video-renderer {
  562. margin: 0 !important;
  563. padding-bottom: 8px;
  564. }
  565.  
  566. #head.ytd-compact-autoplay-renderer {
  567. margin-bottom: 12px;
  568. display: flex;
  569. align-items: center;
  570. }
  571.  
  572. #upnext.ytd-compact-autoplay-renderer {
  573. color: var(--yt-spec-text-primary);
  574. font-size: 1.6rem;
  575. flex-grow: 1;
  576. }
  577.  
  578. #autoplay.ytd-compact-autoplay-renderer {
  579. color: var(--yt-spec-text-secondary);
  580. font-size: 1.3rem;
  581. font-weight: 500;
  582. text-transform: uppercase;
  583. line-height: 1;
  584. }
  585.  
  586. #toggle.ytd-compact-autoplay-renderer {
  587. margin-left: 8px;
  588. }
  589.  
  590. ytd-watch-next-secondary-results-renderer #contents.ytd-item-section-renderer > * {
  591. margin-top: 0 !important;
  592. margin-bottom: var(--ytd-item-section-item-margin,16px);
  593. }
  594.  
  595. #items.ytd-watch-next-secondary-results-renderer > ytd-compact-video-renderer:first-of-type,
  596. ytd-watch-next-secondary-results-renderer #contents.ytd-item-section-renderer > ytd-compact-video-renderer:first-of-type {
  597. display: none !important;
  598. }
  599. </style>
  600. `);
  601. document.removeEventListener("DOMContentLoaded", tmp);
  602. });