background.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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 findVideoInPlaylists(playlists, 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; // Invalid URL
  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 playlistName in playlists) {
  91. const videos = playlists[playlistName];
  92. // Check each video in the array
  93. for (let i = 0; i < videos.length; i++) {
  94. try {
  95. const itemUrl = new URL(videos[i].url);
  96. const itemVParam = itemUrl.searchParams.get("v");
  97. // If the "v" parameters match, return playlist info
  98. if (itemVParam === v) {
  99. const isLastVideo = i === videos.length - 1;
  100. return [playlistName, i, isLastVideo];
  101. }
  102. } catch (e) {
  103. // Skip malformed URLs
  104. continue;
  105. }
  106. }
  107. }
  108. // No match found
  109. return false;
  110. }
  111. function findPlaylist(current, url) {
  112. const result = findVideoInPlaylists(current, url);
  113. // If found, return just the playlist name
  114. return result ? result[0] : false;
  115. }
  116. async function addLinkToPlaylist(plName, item) {
  117. const { playlists: currentPlaylists } =
  118. await browser.storage.local.get("playlists");
  119. const alreadyHave = findPlaylist(currentPlaylists, item.linkUrl);
  120. if (alreadyHave) {
  121. console.log("already have that link in", alreadyHave);
  122. } else {
  123. const { [plName]: playlist, ...others } = currentPlaylists;
  124. await browser.storage.local.set({
  125. playlists: {
  126. [plName]: [...playlist, { url: item.linkUrl, title: item.linkText }],
  127. ...others,
  128. },
  129. });
  130. }
  131. }
  132. // Context menu click handler
  133. browser.contextMenus.onClicked.addListener(async (item, _tab) => {
  134. addLinkToPlaylist(item.menuItemId, item);
  135. });
  136. // Function to navigate a tab to a new URL
  137. function navigateTab(tabId, url) {
  138. return browser.tabs.update(tabId, { url: url });
  139. }
  140. async function updateHistory(message) {
  141. const { history: currentHistory } =
  142. await browser.storage.local.get("history");
  143. const q = new URL(message.url);
  144. const v = q.searchParams.get("v");
  145. if (currentHistory[v]) {
  146. const { [v]: existing, ...rest } = currentHistory;
  147. const shouldIgnore =
  148. message.type === "playing" &&
  149. existing?.history?.length > 0 &&
  150. (existing.history[existing.history.length - 1].action === "play" ||
  151. existing.history[existing.history.length - 1].action === "playing") &&
  152. message.timestamp ===
  153. existing.history[existing.history.length - 1].position;
  154. if (!shouldIgnore) {
  155. await browser.storage.local.set({
  156. history: {
  157. [v]: {
  158. ...existing,
  159. duration:
  160. !isNaN(existing.duration) && isFinite(existing.duration)
  161. ? Math.max(message.duration, existing.duration)
  162. : message.duration,
  163. history: [
  164. ...existing.history,
  165. {
  166. action: message.type,
  167. position: message.timestamp,
  168. timestamp: Date.now(),
  169. },
  170. ],
  171. },
  172. ...rest,
  173. },
  174. });
  175. }
  176. } else {
  177. await browser.storage.local.set({
  178. history: {
  179. [v]: {
  180. url: message.url,
  181. title: message.title,
  182. duration: message.duration,
  183. history: [
  184. {
  185. action: message.type,
  186. position: message.timestamp,
  187. timestamp: Date.now(),
  188. },
  189. ],
  190. },
  191. ...currentHistory,
  192. },
  193. });
  194. }
  195. }
  196. function findNext(playlists, url) {
  197. const result = findVideoInPlaylists(playlists, url);
  198. if (!result) {
  199. return false; // Not found in any playlist
  200. }
  201. const [playlistName, videoIndex, isLastVideo] = result;
  202. if (isLastVideo) {
  203. return false; // Last video, no next video available
  204. } else {
  205. // Get next video in the playlist
  206. const nextVideo = playlists[playlistName][videoIndex + 1];
  207. console.log("FOUND VIDEO", playlists[playlistName][videoIndex]);
  208. return nextVideo;
  209. }
  210. }
  211. async function navigateAndWait(tabId, url) {
  212. return new Promise((resolve) => {
  213. const listener = (details) => {
  214. if (details.tabId === tabId && details.frameId === 0) {
  215. browser.webNavigation.onCompleted.removeListener(listener);
  216. setTimeout(resolve, 500);
  217. }
  218. };
  219. browser.webNavigation.onCompleted.addListener(listener);
  220. browser.tabs.update(tabId, { url });
  221. });
  222. }
  223. async function advance(tabId, url) {
  224. const { playlists } = await browser.storage.local.get("playlists");
  225. const nextVideo = await findNext(playlists, url);
  226. if (nextVideo) {
  227. await navigateAndWait(tabId, nextVideo.url);
  228. await browser.tabs.sendMessage(tabId, { type: "autoplay" });
  229. }
  230. }
  231. // Listen for messages from popup or content scripts
  232. browser.runtime.onMessage.addListener((message, sender, _sendResponse) => {
  233. console.log("MESSAGE", message, sender);
  234. switch (message.type) {
  235. case "play":
  236. case "playing":
  237. case "pause":
  238. updateHistory(message);
  239. break;
  240. case "ended":
  241. updateHistory(message);
  242. advance(sender.tab.id, message.url);
  243. break;
  244. }
  245. });