background.js 8.6 KB

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