background.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // Background script for the extension
  2. browser.storage.local.get("playlists").then(function (data) {
  3. if (!data.playlists) {
  4. console.log("pre-populating playlists");
  5. browser.storage.local.set({
  6. playlists: {
  7. "listening-1": [],
  8. "listening-2": [],
  9. "listening-3": [],
  10. "watching-1": [],
  11. "watching-2": [],
  12. },
  13. });
  14. } else {
  15. console.log("no need to pre-populate playlists");
  16. }
  17. });
  18. browser.storage.local.get("history").then(function (data) {
  19. if (!data.history) {
  20. console.log("pre-populating history");
  21. browser.storage.local.set({
  22. history: {},
  23. });
  24. } else {
  25. console.log("no need to pre-populate history");
  26. }
  27. });
  28. // Create context menu items when the extension is installed
  29. browser.runtime.onInstalled.addListener(() => {
  30. // Parent menu item
  31. browser.contextMenus.create({
  32. id: "my-playlist-menu",
  33. title: "Add to Playlist",
  34. contexts: ["link"],
  35. targetUrlPatterns: ["https://*.youtube.com/watch*"],
  36. });
  37. // Sub-menu items
  38. browser.contextMenus.create({
  39. id: "listening-1",
  40. parentId: "my-playlist-menu",
  41. title: "Listening - 1",
  42. contexts: ["link"],
  43. });
  44. browser.contextMenus.create({
  45. id: "listening-2",
  46. parentId: "my-playlist-menu",
  47. title: "Listening - 2",
  48. contexts: ["link"],
  49. });
  50. browser.contextMenus.create({
  51. id: "listening-3",
  52. parentId: "my-playlist-menu",
  53. title: "Listening - 3",
  54. contexts: ["link"],
  55. });
  56. // Separator
  57. browser.contextMenus.create({
  58. id: "separator-1",
  59. parentId: "my-playlist-menu",
  60. type: "separator",
  61. contexts: ["link"],
  62. });
  63. browser.contextMenus.create({
  64. id: "watching-1",
  65. parentId: "my-playlist-menu",
  66. title: "Watching - 1",
  67. contexts: ["link"],
  68. });
  69. browser.contextMenus.create({
  70. id: "watching-2",
  71. parentId: "my-playlist-menu",
  72. title: "Watching - 2",
  73. contexts: ["link"],
  74. });
  75. });
  76. function findPlaylist(current, url) {
  77. // Extract the video id from URL query params
  78. let v;
  79. try {
  80. const urlObj = new URL(url);
  81. v = urlObj.searchParams.get("v");
  82. } catch (e) {
  83. return false;
  84. }
  85. // If no video id found, return false
  86. if (!v) {
  87. return false;
  88. }
  89. // Check each playlist in the current set
  90. for (const pl in current) {
  91. const videos = current[pl];
  92. // Check each video in the array
  93. for (const video of videos) {
  94. try {
  95. const itemUrl = new URL(video.url);
  96. const itemVParam = itemUrl.searchParams.get("v");
  97. // If the "v" parameters match, return this pl
  98. if (itemVParam === v) {
  99. return pl;
  100. }
  101. } catch (e) {
  102. // Skip malformed URLs
  103. continue;
  104. }
  105. }
  106. }
  107. // No match found
  108. return false;
  109. }
  110. async function addLinkToPlaylist(plName, item) {
  111. const { playlists: currentPlaylists } =
  112. await browser.storage.local.get("playlists");
  113. const alreadyHave = findPlaylist(currentPlaylists, item.linkUrl);
  114. if (alreadyHave) {
  115. console.log("already have that link in", alreadyHave);
  116. } else {
  117. const { [plName]: playlist, ...others } = currentPlaylists;
  118. await browser.storage.local.set({
  119. playlists: {
  120. [plName]: [...playlist, { url: item.linkUrl, title: item.linkText }],
  121. ...others,
  122. },
  123. });
  124. }
  125. }
  126. // Context menu click handler
  127. browser.contextMenus.onClicked.addListener(async (item, _tab) => {
  128. addLinkToPlaylist(item.menuItemId, item);
  129. });
  130. // Function to navigate a tab to a new URL
  131. function navigateTab(tabId, url) {
  132. return browser.tabs.update(tabId, { url: url });
  133. }
  134. async function updateHistory(message) {
  135. const { history: currentHistory } =
  136. await browser.storage.local.get("history");
  137. const q = new URL(message.url);
  138. const v = q.searchParams.get("v");
  139. if (currentHistory[v]) {
  140. const { [v]: existing, ...rest } = currentHistory;
  141. const shouldIgnore =
  142. message.type === "playing" &&
  143. existing?.history?.length > 0 &&
  144. (existing.history[existing.history.length - 1].action === "play" ||
  145. existing.history[existing.history.length - 1].action === "playing") &&
  146. message.timestamp ===
  147. existing.history[existing.history.length - 1].position;
  148. if (!shouldIgnore) {
  149. await browser.storage.local.set({
  150. history: {
  151. [v]: {
  152. ...existing,
  153. duration:
  154. !isNaN(existing.duration) && isFinite(existing.duration)
  155. ? Math.max(message.duration, existing.duration)
  156. : message.duration,
  157. history: [
  158. ...existing.history,
  159. {
  160. action: message.type,
  161. position: message.timestamp,
  162. timestamp: Date.now(),
  163. },
  164. ],
  165. },
  166. ...rest,
  167. },
  168. });
  169. }
  170. } else {
  171. await browser.storage.local.set({
  172. history: {
  173. [v]: {
  174. url: message.url,
  175. title: message.title,
  176. duration: message.duration,
  177. history: [
  178. {
  179. action: message.type,
  180. position: message.timestamp,
  181. timestamp: Date.now(),
  182. },
  183. ],
  184. },
  185. ...currentHistory,
  186. },
  187. });
  188. }
  189. }
  190. function findNext(playlists, url) {
  191. // Extract the "v" query parameter from input URL
  192. let v;
  193. try {
  194. const urlObj = new URL(url);
  195. v = urlObj.searchParams.get("v");
  196. } catch (e) {
  197. return false;
  198. }
  199. // If no "v" parameter found, return false
  200. if (!v) {
  201. return false;
  202. }
  203. // Check each video in the playlists
  204. for (const pl in playlists) {
  205. const videos = playlists[pl];
  206. // Check each video in the array
  207. for (let i = 0; i < videos.length; i++) {
  208. try {
  209. const itemUrl = new URL(videos[i].url);
  210. const itemVParam = itemUrl.searchParams.get("v");
  211. // If the "v" parameters match
  212. if (itemVParam === v) {
  213. console.log("FOUND VIDEO", videos[i]);
  214. // Check if this is the last video in the array
  215. if (i === videos.length - 1) {
  216. return false; // Last video, no next video available
  217. } else {
  218. return videos[i + 1]; // Return the next video
  219. }
  220. }
  221. } catch (e) {
  222. // Skip malformed URLs
  223. continue;
  224. }
  225. }
  226. }
  227. // No match found
  228. return false;
  229. }
  230. async function navigateAndWait(tabId, url) {
  231. return new Promise((resolve) => {
  232. const listener = (details) => {
  233. if (details.tabId === tabId && details.frameId === 0) {
  234. browser.webNavigation.onCompleted.removeListener(listener);
  235. setTimeout(resolve, 500);
  236. }
  237. };
  238. browser.webNavigation.onCompleted.addListener(listener);
  239. browser.tabs.update(tabId, { url });
  240. });
  241. }
  242. async function advance(tabId, url) {
  243. const { playlists } = await browser.storage.local.get("playlists");
  244. const nextVideo = await findNext(playlists, url);
  245. if (nextVideo) {
  246. await navigateAndWait(tabId, nextVideo.url);
  247. await browser.tabs.sendMessage(tabId, { type: "autoplay" });
  248. }
  249. }
  250. // Listen for messages from popup or content scripts
  251. browser.runtime.onMessage.addListener((message, sender, _sendResponse) => {
  252. console.log("MESSAGE", message, sender);
  253. switch (message.type) {
  254. case "play":
  255. case "playing":
  256. case "pause":
  257. updateHistory(message);
  258. break;
  259. case "ended":
  260. updateHistory(message);
  261. advance(sender.tab.id, message.url);
  262. break;
  263. }
  264. });