BetterYTM.user.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. // ==UserScript==
  2. // @name BetterYTM
  3. // @name:de BetterYTM
  4. // @namespace https://github.com/Sv443/BetterYTM#readme
  5. // @version 0.2.0
  6. // @license MIT
  7. // @author Sv443
  8. // @copyright Sv443 <[email protected]> (https://github.com/Sv443)
  9. // @description Improvements for YouTube Music
  10. // @description:de Verbesserungen für YouTube Music
  11. // @match https://music.youtube.com/*
  12. // @match https://www.youtube.com/*
  13. // @icon https://www.google.com/s2/favicons?domain=music.youtube.com
  14. // @run-at document-start
  15. // @connect self
  16. // @connect *.youtube.com
  17. // @downloadURL https://raw.githubusercontent.com/Sv443/BetterYTM/main/BetterYTM.user.js
  18. // @updateURL https://raw.githubusercontent.com/Sv443/BetterYTM/main/BetterYTM.user.js
  19. // ==/UserScript==
  20. /* Disclaimer: I am not affiliated with YouTube, Google, Alphabet or anyone else */
  21. /* C&D this, Susan 🖕 */
  22. /*
  23. █▀▀█ ▄▄▄ █ █ ▀ ▄▄▄ ▄▄▄▄ ▄▄▄
  24. ▀▀▄▄ █▄█ █▀ █▀ ▀█ █ █ █ ▄▄ █▄▄ ▀
  25. █▄▄█ █▄▄ █▄▄ █▄▄ ▄█▄ █ █ █▄▄█ ▄▄█ ▄
  26. */
  27. /**
  28. * This is where you can enable or disable features
  29. * If this userscript ever becomes something I might add like a menu to toggle these
  30. */
  31. const features = Object.freeze({
  32. /** Whether arrow keys should skip forwards and backwards by 10 seconds */
  33. arrowKeySupport: true,
  34. /** Whether to add a button or key combination (TODO) to switch between the YT and YTM sites on a video */
  35. switchBetweenSites: true,
  36. // /** The theme color - accepts any CSS color value - default is "#ff0000" */
  37. // themeColor: "#0f0",
  38. });
  39. //#MARKER types
  40. /** @typedef {"yt"|"ytm"} Domain */
  41. //#MARKER init
  42. const info = Object.freeze({
  43. name: GM.info.script.name, // eslint-disable-line no-undef
  44. version: GM.info.script.version, // eslint-disable-line no-undef
  45. namespace: GM.info.script.namespace, // eslint-disable-line no-undef
  46. });
  47. function init()
  48. {
  49. try
  50. {
  51. console.log(`${info.name} v${info.version} - ${info.namespace}`);
  52. document.addEventListener("DOMContentLoaded", onDomLoad);
  53. }
  54. catch(err)
  55. {
  56. console.error("BetterYTM Error:", err instanceof Error ? err : new Error(err));
  57. }
  58. }
  59. //#MARKER events
  60. /**
  61. * Called when the DOM has finished loading (after `DOMContentLoaded` is emitted)
  62. */
  63. function onDomLoad()
  64. {
  65. const domain = getDomain();
  66. if(features.arrowKeySupport && domain === "ytm")
  67. document.addEventListener("keydown", onKeyDown);
  68. if(features.switchBetweenSites)
  69. initSiteSwitch(domain);
  70. // if(features.themeColor != "#f00" && features.themeColor != "#ff0000")
  71. // applyTheme();
  72. }
  73. //#MARKER features
  74. //#SECTION arrow key skip
  75. /**
  76. * Called when the user presses keys
  77. * @param {KeyboardEvent} evt
  78. */
  79. function onKeyDown(evt)
  80. {
  81. if(["ArrowLeft", "ArrowRight"].includes(evt.code))
  82. {
  83. // ripped this stuff from the console, most of these are probably unnecessary but this was finnicky af and I am sick and tired of trial and error
  84. const defaultProps = {
  85. altKey: false,
  86. bubbles: true,
  87. cancelBubble: false,
  88. cancelable: true,
  89. charCode: 0,
  90. composed: true,
  91. ctrlKey: false,
  92. currentTarget: null,
  93. defaultPrevented: evt.defaultPrevented,
  94. explicitOriginalTarget: document.body,
  95. isTrusted: true,
  96. metaKey: false,
  97. originalTarget: document.body,
  98. repeat: false,
  99. shiftKey: false,
  100. srcElement: document.body,
  101. target: document.body,
  102. type: "keydown",
  103. view: window,
  104. };
  105. let invalidKey = false;
  106. let keyProps = {};
  107. switch(evt.code)
  108. {
  109. case "ArrowLeft":
  110. keyProps = {
  111. code: "KeyH",
  112. key: "h",
  113. keyCode: 72,
  114. which: 72,
  115. };
  116. break;
  117. case "ArrowRight":
  118. keyProps = {
  119. code: "KeyL",
  120. key: "l",
  121. keyCode: 76,
  122. which: 76,
  123. };
  124. break;
  125. default:
  126. // console.warn("BetterYTM - Unknown key", evt.code);
  127. invalidKey = true;
  128. break;
  129. }
  130. if(!invalidKey)
  131. document.body.dispatchEvent(new KeyboardEvent("keydown", { ...defaultProps, ...keyProps }));
  132. }
  133. }
  134. //#SECTION site switch
  135. /**
  136. * Initializes the site switch feature
  137. * @param {Domain} domain
  138. */
  139. function initSiteSwitch(domain)
  140. {
  141. // TODO:
  142. // extra features:
  143. // - keep video time
  144. document.addEventListener("keydown", (e) => {
  145. if(e.key == "F9")
  146. switchSite(domain === "yt" ? "ytm" : "yt");
  147. });
  148. }
  149. /**
  150. * Switches to the other site (between YT and YTM)
  151. * @param {Domain} newDomain
  152. */
  153. function switchSite(newDomain)
  154. {
  155. console.log(`BYTM/Debug: Switching from domain ${getDomain()} to ${newDomain}`);
  156. try
  157. {
  158. let subdomain;
  159. if(newDomain === "ytm")
  160. subdomain = "music";
  161. else if(newDomain === "yt")
  162. subdomain = "www";
  163. if(!subdomain)
  164. throw new TypeError(`Unrecognized domain '${newDomain}'`);
  165. const { pathname, search, hash } = new URL(location.href);
  166. const newSearch = search.includes("?") ? `${search}&t=${getVideoTime()}` : `?t=${getVideoTime()}`;
  167. const url = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
  168. console.info(`BetterYTM - switching to domain '${newDomain}' at ${url}`);
  169. location.href = url;
  170. }
  171. catch(err)
  172. {
  173. console.error(err instanceof Error ? err : new Error(err));
  174. }
  175. }
  176. /**
  177. * Returns the current video time in seconds
  178. * @param {Domain} [domain]
  179. * @returns {number|null} Returns null if video time is unavailable
  180. */
  181. function getVideoTime(domain)
  182. {
  183. if(typeof domain !== "string")
  184. domain = getDomain();
  185. if(domain === "ytm")
  186. {
  187. const pbEl = document.querySelector("#progress-bar");
  188. return pbEl.value ?? null;
  189. }
  190. else if(domain === "yt") // YT doesn't update the progress bar when it's hidden (YTM doesn't hide it) so TODO: come up with some solution here
  191. return 0;
  192. return null;
  193. }
  194. //#MARKER other
  195. /**
  196. * Returns the current domain as a string representation
  197. * @returns {Domain}
  198. */
  199. function getDomain()
  200. {
  201. const { hostname } = new URL(location.href);
  202. return hostname.toLowerCase().includes("music") ? "ytm" : "yt"; // other cases are caught by the `@match`es at the top
  203. }
  204. (() => init())(); // call init() when file is loaded