background.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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 updateTracking(message) {
  141. // Get current playlists from storage
  142. const { playlists } = await browser.storage.local.get("playlists");
  143. // Find the video in playlists
  144. const result = findVideoInPlaylists(playlists, message.url);
  145. // If the video is not present in any playlist, do nothing
  146. if (!result) {
  147. return;
  148. }
  149. const [playlistName, videoIndex, _] = result;
  150. // Only update for pause and ended events
  151. if (message.type === "pause" || message.type === "ended") {
  152. // Create a copy of the playlist for immutability
  153. const updatedPlaylists = { ...playlists };
  154. const targetPlaylist = [...updatedPlaylists[playlistName]];
  155. // Get the current video object
  156. const video = { ...targetPlaylist[videoIndex] };
  157. // Update the video's status based on message type
  158. if (message.type === "pause") {
  159. video.status = message.timestamp; // Set status to current timestamp
  160. } else if (message.type === "ended") {
  161. video.status = "done"; // Set status to "done"
  162. }
  163. // Update the video in the playlist
  164. targetPlaylist[videoIndex] = video;
  165. updatedPlaylists[playlistName] = targetPlaylist;
  166. // Save the updated playlists back to storage
  167. await browser.storage.local.set({ playlists: updatedPlaylists });
  168. }
  169. }
  170. async function updateHistory(message) {
  171. const { history: currentHistory } =
  172. await browser.storage.local.get("history");
  173. const q = new URL(message.url);
  174. const v = q.searchParams.get("v");
  175. if (currentHistory[v]) {
  176. const { [v]: existing, ...rest } = currentHistory;
  177. const shouldIgnore =
  178. message.type === "playing" &&
  179. existing?.history?.length > 0 &&
  180. (existing.history[existing.history.length - 1].action === "play" ||
  181. existing.history[existing.history.length - 1].action === "playing") &&
  182. message.timestamp ===
  183. existing.history[existing.history.length - 1].position;
  184. if (!shouldIgnore) {
  185. await browser.storage.local.set({
  186. history: {
  187. [v]: {
  188. ...existing,
  189. title: message.title,
  190. duration:
  191. !isNaN(existing.duration) && isFinite(existing.duration)
  192. ? Math.max(message.duration, existing.duration)
  193. : message.duration,
  194. history: [
  195. ...existing.history,
  196. {
  197. action: message.type,
  198. position: message.timestamp,
  199. timestamp: Date.now(),
  200. },
  201. ],
  202. },
  203. ...rest,
  204. },
  205. });
  206. }
  207. } else {
  208. await browser.storage.local.set({
  209. history: {
  210. [v]: {
  211. url: message.url,
  212. title: message.title,
  213. duration: message.duration,
  214. history: [
  215. {
  216. action: message.type,
  217. position: message.timestamp,
  218. timestamp: Date.now(),
  219. },
  220. ],
  221. },
  222. ...currentHistory,
  223. },
  224. });
  225. }
  226. }
  227. function findNext(playlists, url) {
  228. const result = findVideoInPlaylists(playlists, url);
  229. if (!result) {
  230. return false; // Not found in any playlist
  231. }
  232. const [playlistName, videoIndex, isLastVideo] = result;
  233. if (isLastVideo) {
  234. return false; // Last video, no next video available
  235. } else {
  236. // Get next video in the playlist
  237. const nextVideo = playlists[playlistName][videoIndex + 1];
  238. console.log("FOUND VIDEO", playlists[playlistName][videoIndex]);
  239. return nextVideo;
  240. }
  241. }
  242. async function navigateAndWait(tabId, url) {
  243. return new Promise((resolve) => {
  244. const listener = (details) => {
  245. if (details.tabId === tabId && details.frameId === 0) {
  246. browser.webNavigation.onCompleted.removeListener(listener);
  247. setTimeout(resolve, 500);
  248. }
  249. };
  250. browser.webNavigation.onCompleted.addListener(listener);
  251. browser.tabs.update(tabId, { url });
  252. });
  253. }
  254. async function advance(tabId, url) {
  255. const { playlists } = await browser.storage.local.get("playlists");
  256. const nextVideo = await findNext(playlists, url);
  257. if (nextVideo) {
  258. await navigateAndWait(tabId, nextVideo.url);
  259. await browser.tabs.sendMessage(tabId, { type: "autoplay" });
  260. }
  261. }
  262. // Listen for messages from popup or content scripts
  263. browser.runtime.onMessage.addListener((message, sender, _sendResponse) => {
  264. console.log("MESSAGE", message, sender);
  265. switch (message.type) {
  266. case "play":
  267. case "playing":
  268. case "pause":
  269. updateTracking(message);
  270. updateHistory(message);
  271. break;
  272. case "ended":
  273. updateTracking(message);
  274. updateHistory(message);
  275. advance(sender.tab.id, message.url);
  276. break;
  277. }
  278. });