BetterYTM.user.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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. "use-strict";
  24. /*
  25. █▀▀█ ▄▄▄ █ █ ▀ ▄▄▄ ▄▄▄▄ ▄▄▄
  26. ▀▀▄▄ █▄█ █▀ █▀ ▀█ █ █ █ ▄▄ █▄▄ ▀
  27. █▄▄█ █▄▄ █▄▄ █▄▄ ▄█▄ █ █ █▄▄█ ▄▄█ ▄
  28. */
  29. /**
  30. * This is where you can enable or disable features
  31. * If this userscript ever becomes something I might add like a menu to toggle these
  32. */
  33. const features = Object.freeze({
  34. // --- Quality of Life ---
  35. /** Whether arrow keys should skip forwards and backwards by 10 seconds */
  36. arrowKeySupport: true,
  37. /** Whether to remove the "Upgrade" / YT Music Premium tab */
  38. removeUpgradeTab: true,
  39. // --- Extra Features ---
  40. /** Whether to add a button or key combination (TODO) to switch between the YT and YTM sites on a video */
  41. switchBetweenSites: true,
  42. // --- Other ---
  43. /** Set to true to remove the watermark next to the YTM logo */
  44. removeWatermark: false,
  45. // /** The theme color - accepts any CSS color value - default is "#ff0000" */
  46. // themeColor: "#0f0",
  47. });
  48. //#MARKER types
  49. /** @typedef {"yt"|"ytm"} Domain */
  50. //#MARKER init
  51. const info = Object.freeze({
  52. name: GM.info.script.name, // eslint-disable-line no-undef
  53. version: GM.info.script.version, // eslint-disable-line no-undef
  54. namespace: GM.info.script.namespace, // eslint-disable-line no-undef
  55. });
  56. function init()
  57. {
  58. try
  59. {
  60. console.log(`${info.name} v${info.version} - ${info.namespace}`);
  61. document.addEventListener("DOMContentLoaded", onDomLoad);
  62. }
  63. catch(err)
  64. {
  65. console.error("BetterYTM - General Error:", err instanceof Error ? err : new Error(err));
  66. }
  67. }
  68. //#MARKER events
  69. /**
  70. * Called when the DOM has finished loading (after `DOMContentLoaded` is emitted)
  71. */
  72. function onDomLoad()
  73. {
  74. const domain = getDomain();
  75. if(features.arrowKeySupport && domain === "ytm")
  76. document.addEventListener("keydown", onKeyDown);
  77. if(features.switchBetweenSites)
  78. initSiteSwitch(domain);
  79. if(features.removeUpgradeTab)
  80. {
  81. const tabElem = document.querySelector(`.ytmusic-nav-bar ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]`);
  82. tabElem.innerHTML = "";
  83. tabElem.outerHTML = "";
  84. }
  85. if(!features.removeWatermark)
  86. {
  87. const watermark = document.createElement("a");
  88. watermark.id = "betterytm-watermark";
  89. watermark.className = "style-scope ytmusic-nav-bar";
  90. watermark.innerText = info.name;
  91. watermark.title = `${info.name} v${info.version}`;
  92. watermark.href = info.namespace;
  93. watermark.target = "_blank";
  94. watermark.rel = "noopener noreferrer";
  95. const style = `
  96. #betterytm-watermark {
  97. position: absolute;
  98. left: 45px;
  99. top: 43px;
  100. z-index: 10;
  101. color: white;
  102. text-decoration: none;
  103. cursor: pointer;
  104. }
  105. #betterytm-watermark:hover {
  106. text-decoration: underline;
  107. }
  108. `;
  109. const styleElem = document.createElement("style");
  110. if(styleElem.styleSheet)
  111. styleElem.styleSheet.cssText = style;
  112. else
  113. styleElem.appendChild(document.createTextNode(style));
  114. document.querySelector("head").appendChild(styleElem);
  115. const logoElem = document.querySelector("#left-content");
  116. logoElem.parentNode.insertBefore(watermark, logoElem.nextSibling);
  117. }
  118. // if(features.themeColor != "#f00" && features.themeColor != "#ff0000")
  119. // applyTheme();
  120. }
  121. //#MARKER features
  122. //#SECTION arrow key skip
  123. /**
  124. * Called when the user presses keys
  125. * @param {KeyboardEvent} evt
  126. */
  127. function onKeyDown(evt)
  128. {
  129. if(["ArrowLeft", "ArrowRight"].includes(evt.code))
  130. {
  131. // 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
  132. const defaultProps = {
  133. altKey: false,
  134. bubbles: true,
  135. cancelBubble: false,
  136. cancelable: true,
  137. charCode: 0,
  138. composed: true,
  139. ctrlKey: false,
  140. currentTarget: null,
  141. defaultPrevented: evt.defaultPrevented,
  142. explicitOriginalTarget: document.body,
  143. isTrusted: true,
  144. metaKey: false,
  145. originalTarget: document.body,
  146. repeat: false,
  147. shiftKey: false,
  148. srcElement: document.body,
  149. target: document.body,
  150. type: "keydown",
  151. view: window,
  152. };
  153. let invalidKey = false;
  154. let keyProps = {};
  155. switch(evt.code)
  156. {
  157. case "ArrowLeft":
  158. keyProps = {
  159. code: "KeyH",
  160. key: "h",
  161. keyCode: 72,
  162. which: 72,
  163. };
  164. break;
  165. case "ArrowRight":
  166. keyProps = {
  167. code: "KeyL",
  168. key: "l",
  169. keyCode: 76,
  170. which: 76,
  171. };
  172. break;
  173. default:
  174. // console.warn("BetterYTM - Unknown key", evt.code);
  175. invalidKey = true;
  176. break;
  177. }
  178. if(!invalidKey)
  179. document.body.dispatchEvent(new KeyboardEvent("keydown", { ...defaultProps, ...keyProps }));
  180. }
  181. }
  182. //#SECTION site switch
  183. /**
  184. * Initializes the site switch feature
  185. * @param {Domain} domain
  186. */
  187. function initSiteSwitch(domain)
  188. {
  189. // TODO:
  190. // extra features:
  191. // - keep video time
  192. document.addEventListener("keydown", (e) => {
  193. if(e.key == "F9")
  194. switchSite(domain === "yt" ? "ytm" : "yt");
  195. });
  196. }
  197. /**
  198. * Switches to the other site (between YT and YTM)
  199. * @param {Domain} newDomain
  200. */
  201. function switchSite(newDomain)
  202. {
  203. console.log(`BYTM/Debug: Switching from domain ${getDomain()} to ${newDomain}`);
  204. try
  205. {
  206. let subdomain;
  207. if(newDomain === "ytm")
  208. subdomain = "music";
  209. else if(newDomain === "yt")
  210. subdomain = "www";
  211. if(!subdomain)
  212. throw new TypeError(`Unrecognized domain '${newDomain}'`);
  213. const { pathname, search, hash } = new URL(location.href);
  214. const newSearch = search.includes("?") ? `${search}&t=${getVideoTime()}` : `?t=${getVideoTime()}`;
  215. const url = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
  216. console.info(`BetterYTM - switching to domain '${newDomain}' at ${url}`);
  217. location.href = url;
  218. }
  219. catch(err)
  220. {
  221. console.error(err instanceof Error ? err : new Error(err));
  222. }
  223. }
  224. /**
  225. * Returns the current video time in seconds
  226. * @param {Domain} [domain]
  227. * @returns {number|null} Returns null if video time is unavailable
  228. */
  229. function getVideoTime(domain)
  230. {
  231. if(typeof domain !== "string")
  232. domain = getDomain();
  233. if(domain === "ytm")
  234. {
  235. const pbEl = document.querySelector("#progress-bar");
  236. return pbEl.value ?? null;
  237. }
  238. 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
  239. return 0;
  240. return null;
  241. }
  242. //#MARKER other
  243. /**
  244. * Returns the current domain as a string representation
  245. * @returns {Domain}
  246. */
  247. function getDomain()
  248. {
  249. const { hostname } = new URL(location.href);
  250. return hostname.toLowerCase().includes("music") ? "ytm" : "yt"; // other cases are caught by the `@match`es at the top
  251. }
  252. init(); // call init() when script is loaded
  253. })();