observers.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import { SelectorListenerOptions, SelectorObserver, SelectorObserverOptions } from "@sv443-network/userutils";
  2. import { emitInterface } from "./interface";
  3. import { error, getDomain } from "./utils";
  4. import type { Domain } from "./types";
  5. /** Names of all available Observer instances across all sites */
  6. export type ObserverName = SharedObserverName | YTMObserverName | YTObserverName;
  7. /** Observer names available to each site */
  8. export type ObserverNameByDomain<TDomain extends Domain> = SharedObserverName | (TDomain extends "ytm" ? YTMObserverName : YTObserverName);
  9. // Shared between YTM and YT
  10. export type SharedObserverName =
  11. | "body"; // the entire <body> element
  12. // YTM only
  13. export type YTMObserverName =
  14. | "browseResponse" // the /channel/UC... page
  15. | "navBar" // the navigation / title bar at the top of the page
  16. | "mainPanel" // the main content panel - includes things like the video element
  17. | "sideBar" // the sidebar on the left side of the page
  18. | "sideBarMini" // the minimized sidebar on the left side of the page
  19. | "sidePanel" // the side panel on the right side of the /watch page
  20. | "playerBar" // media controls bar at the bottom of the page
  21. | "playerBarInfo" // song title, artist, album, etc. inside the player bar
  22. | "playerBarMiddleButtons" // the buttons inside the player bar (like, dislike, lyrics, etc.)
  23. | "playerBarRightControls" // the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
  24. | "popupContainer"; // the container for popups (e.g. the queue popup)
  25. // YT only
  26. export type YTObserverName =
  27. | "ytGuide" // the left sidebar menu
  28. | "ytdBrowse" // channel pages for example
  29. | "ytChannelHeader" // header of a channel page
  30. | "watchFlexy" // the main content of the /watch page
  31. | "watchMetadata"; // the metadata section of the /watch page
  32. /** Options that are applied to every SelectorObserver instance */
  33. const defaultObserverOptions: SelectorObserverOptions = {
  34. disableOnNoListeners: false,
  35. enableOnAddListener: false,
  36. defaultDebounce: 100,
  37. defaultDebounceEdge: "rising",
  38. };
  39. /** Global SelectorObserver instances usable throughout the script for improved performance */
  40. export const globservers = {} as Record<ObserverName, SelectorObserver>;
  41. /** Call after DOM load to initialize all SelectorObserver instances */
  42. export function initObservers() {
  43. try {
  44. //#region both sites
  45. //#region body
  46. // -> the entire <body> element - use sparingly due to performance impacts!
  47. globservers.body = new SelectorObserver(document.body, {
  48. ...defaultObserverOptions,
  49. defaultDebounce: 150,
  50. subtree: false,
  51. });
  52. globservers.body.enable();
  53. switch(getDomain()) {
  54. case "ytm": {
  55. //#region YTM
  56. //#region browseResponse
  57. // -> for example the /channel/UC... page
  58. const browseResponseSelector = "ytmusic-browse-response";
  59. globservers.browseResponse = new SelectorObserver(browseResponseSelector, {
  60. ...defaultObserverOptions,
  61. subtree: true,
  62. });
  63. globservers.body.addListener(browseResponseSelector, {
  64. listener: () => globservers.browseResponse.enable(),
  65. });
  66. //#region navBar
  67. // -> the navigation / title bar at the top of the page
  68. const navBarSelector = "ytmusic-nav-bar";
  69. globservers.navBar = new SelectorObserver(navBarSelector, {
  70. ...defaultObserverOptions,
  71. subtree: false,
  72. });
  73. globservers.body.addListener(navBarSelector, {
  74. listener: () => globservers.navBar.enable(),
  75. });
  76. //#region mainPanel
  77. // -> the main content panel - includes things like the video element
  78. const mainPanelSelector = "ytmusic-player-page #main-panel";
  79. globservers.mainPanel = new SelectorObserver(mainPanelSelector, {
  80. ...defaultObserverOptions,
  81. subtree: true,
  82. });
  83. globservers.body.addListener(mainPanelSelector, {
  84. listener: () => globservers.mainPanel.enable(),
  85. });
  86. //#region sideBar
  87. // -> the sidebar on the left side of the page
  88. const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
  89. globservers.sideBar = new SelectorObserver(sidebarSelector, {
  90. ...defaultObserverOptions,
  91. subtree: true,
  92. });
  93. globservers.body.addListener(sidebarSelector, {
  94. listener: () => globservers.sideBar.enable(),
  95. });
  96. //#region sideBarMini
  97. // -> the minimized sidebar on the left side of the page
  98. const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
  99. globservers.sideBarMini = new SelectorObserver(sideBarMiniSelector, {
  100. ...defaultObserverOptions,
  101. subtree: true,
  102. });
  103. globservers.body.addListener(sideBarMiniSelector, {
  104. listener: () => globservers.sideBarMini.enable(),
  105. });
  106. //#region sidePanel
  107. // -> the side panel on the right side of the /watch page
  108. const sidePanelSelector = "#side-panel";
  109. globservers.sidePanel = new SelectorObserver(sidePanelSelector, {
  110. ...defaultObserverOptions,
  111. subtree: true,
  112. });
  113. globservers.body.addListener(sidePanelSelector, {
  114. listener: () => globservers.sidePanel.enable(),
  115. });
  116. //#region playerBar
  117. // -> media controls bar at the bottom of the page
  118. const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
  119. globservers.playerBar = new SelectorObserver(playerBarSelector, {
  120. ...defaultObserverOptions,
  121. defaultDebounce: 200,
  122. });
  123. globservers.body.addListener(playerBarSelector, {
  124. listener: () => {
  125. globservers.playerBar.enable();
  126. },
  127. });
  128. //#region playerBarInfo
  129. // -> song title, artist, album, etc. inside the player bar
  130. const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
  131. globservers.playerBarInfo = new SelectorObserver(playerBarInfoSelector, {
  132. ...defaultObserverOptions,
  133. attributes: true,
  134. attributeFilter: ["title"],
  135. });
  136. globservers.playerBar.addListener(playerBarInfoSelector, {
  137. listener: () => globservers.playerBarInfo.enable(),
  138. });
  139. //#region playerBarMiddleButtons
  140. // -> the buttons inside the player bar (like, dislike, lyrics, etc.)
  141. const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
  142. globservers.playerBarMiddleButtons = new SelectorObserver(playerBarMiddleButtonsSelector, {
  143. ...defaultObserverOptions,
  144. subtree: true,
  145. });
  146. globservers.playerBar.addListener(playerBarMiddleButtonsSelector, {
  147. listener: () => globservers.playerBarMiddleButtons.enable(),
  148. });
  149. //#region playerBarRightControls
  150. // -> the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
  151. const playerBarRightControls = "#right-controls";
  152. globservers.playerBarRightControls = new SelectorObserver(playerBarRightControls, {
  153. ...defaultObserverOptions,
  154. subtree: true,
  155. });
  156. globservers.playerBar.addListener(playerBarRightControls, {
  157. listener: () => globservers.playerBarRightControls.enable(),
  158. });
  159. //#region popupContainer
  160. // -> the container for popups (e.g. the queue popup)
  161. const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
  162. globservers.popupContainer = new SelectorObserver(popupContainerSelector, {
  163. ...defaultObserverOptions,
  164. subtree: true,
  165. });
  166. globservers.body.addListener(popupContainerSelector, {
  167. listener: () => globservers.popupContainer.enable(),
  168. });
  169. break;
  170. }
  171. case "yt": {
  172. //#region YT
  173. //#region ytGuide
  174. // -> the left sidebar menu
  175. const ytGuideSelector = "#content tp-yt-app-drawer#guide #guide-inner-content";
  176. globservers.ytGuide = new SelectorObserver(ytGuideSelector, {
  177. ...defaultObserverOptions,
  178. subtree: true,
  179. });
  180. globservers.body.addListener(ytGuideSelector, {
  181. listener: () => globservers.ytGuide.enable(),
  182. });
  183. //#region ytdBrowse
  184. // -> channel pages for example
  185. const ytdBrowseSelector = "ytd-app ytd-page-manager ytd-browse";
  186. globservers.ytdBrowse = new SelectorObserver(ytdBrowseSelector, {
  187. ...defaultObserverOptions,
  188. subtree: true,
  189. });
  190. globservers.body.addListener(ytdBrowseSelector, {
  191. listener: () => globservers.ytdBrowse.enable(),
  192. });
  193. //#region ytChannelHeader
  194. // -> header of a channel page
  195. const ytChannelHeaderSelector = "#header tp-yt-app-header #channel-header";
  196. globservers.ytChannelHeader = new SelectorObserver(ytChannelHeaderSelector, {
  197. ...defaultObserverOptions,
  198. subtree: true,
  199. });
  200. globservers.ytdBrowse.addListener(ytChannelHeaderSelector, {
  201. listener: () => globservers.ytChannelHeader.enable(),
  202. });
  203. //#region watchFlexy
  204. // -> the main content of the /watch page
  205. const watchFlexySelector = "ytd-app ytd-watch-flexy";
  206. globservers.watchFlexy = new SelectorObserver(watchFlexySelector, {
  207. ...defaultObserverOptions,
  208. subtree: true,
  209. });
  210. globservers.body.addListener(watchFlexySelector, {
  211. listener: () => globservers.watchFlexy.enable(),
  212. });
  213. //#region watchMetadata
  214. // -> the metadata section of the /watch page (title, channel, views, description, buttons, etc. but not comments)
  215. const watchMetadataSelector = "#columns #primary-inner ytd-watch-metadata";
  216. globservers.watchMetadata = new SelectorObserver(watchMetadataSelector, {
  217. ...defaultObserverOptions,
  218. subtree: true,
  219. });
  220. globservers.watchFlexy.addListener(watchMetadataSelector, {
  221. listener: () => globservers.watchMetadata.enable(),
  222. });
  223. // //#region ytMasthead
  224. // -> the masthead (title bar) at the top of the page
  225. // const mastheadSelector = "#content ytd-masthead#masthead";
  226. // globservers.ytMasthead = new SelectorObserver(mastheadSelector, {
  227. // ...defaultObserverOptions,
  228. // subtree: true,
  229. // });
  230. // globservers.body.addListener(mastheadSelector, {
  231. // listener: () => globservers.ytMasthead.enable(),
  232. // });
  233. }
  234. }
  235. //#region finalize
  236. emitInterface("bytm:observersReady");
  237. }
  238. catch(err) {
  239. error("Failed to initialize observers:", err);
  240. }
  241. }
  242. //#region add listener func
  243. /**
  244. * Interface function for adding listeners to the {@linkcode globservers}
  245. * @param selector Relative to the observer's root element, so the selector can only start at of the root element's children at the earliest!
  246. * @param options Options for the listener
  247. * @template TElem The type of the element that the listener will be attached to. If set to `0`, the type HTMLElement will be used.
  248. * @template TDomain This restricts which observers are available with the current domain
  249. */
  250. export function addSelectorListener<
  251. TElem extends HTMLElement | 0 = HTMLElement,
  252. TDomain extends Domain = "ytm"
  253. >(
  254. observerName: ObserverNameByDomain<TDomain>,
  255. selector: string,
  256. options: SelectorListenerOptions<
  257. TElem extends 0
  258. ? HTMLElement
  259. : TElem
  260. >
  261. ){
  262. globservers[observerName].addListener(selector, options);
  263. }