// Background script for the extension browser.storage.local.get("playlists").then(function (data) { if (!data.playlists) { console.log("pre-populating playlists"); browser.storage.local.set({ playlists: { "listening-long": [], "listening-short": [], "listening-misc": [], "watching-short": [], "watching-long": [], "slow-tv": [], }, }); } else { console.log("no need to pre-populate playlists"); } }); browser.storage.local.get("history").then(function (data) { if (!data.history) { console.log("pre-populating history"); browser.storage.local.set({ history: {}, }); } else { console.log("no need to pre-populate history"); } }); // Create context menu items when the extension is installed browser.runtime.onInstalled.addListener(() => { // Parent menu item browser.contextMenus.create({ id: "my-playlist-menu", title: "Add to Playlist", contexts: ["link"], targetUrlPatterns: ["https://*.youtube.com/watch*"], }); // Sub-menu items browser.contextMenus.create({ id: "listening-long", parentId: "my-playlist-menu", title: "Listening - Long", contexts: ["link"], }); browser.contextMenus.create({ id: "listening-short", parentId: "my-playlist-menu", title: "Listening - Short", contexts: ["link"], }); browser.contextMenus.create({ id: "listening-misc", parentId: "my-playlist-menu", title: "Listening - Misc", contexts: ["link"], }); // Separator browser.contextMenus.create({ id: "separator-1", parentId: "my-playlist-menu", type: "separator", contexts: ["link"], }); browser.contextMenus.create({ id: "watching-short", parentId: "my-playlist-menu", title: "Watching - Short", contexts: ["link"], }); browser.contextMenus.create({ id: "watching-long", parentId: "my-playlist-menu", title: "Watching - Long", contexts: ["link"], }); browser.contextMenus.create({ id: "slow-tv", parentId: "my-playlist-menu", title: "Slow TV", contexts: ["link"], }); }); // Context menu click handler browser.contextMenus.onClicked.addListener(async (item, _tab) => { PlaylistUtils.addLinkToPlaylist(item.menuItemId, item); }); // Function to navigate a tab to a new URL function navigateTab(tabId, url) { return browser.tabs.update(tabId, { url: url }); } async function updateTracking(message) { // Get current playlists from storage const { playlists } = await browser.storage.local.get("playlists"); // Find the video in playlists using shared utils const result = PlaylistUtils.findVideoInPlaylists(playlists, message.url); // If the video is not present in any playlist, do nothing if (!result) { return; } const [playlistName, videoIndex, _] = result; // Only update for pause and ended events if (message.type === "pause" || message.type === "ended") { // Create a copy of the playlist for immutability const updatedPlaylists = { ...playlists }; const targetPlaylist = [...updatedPlaylists[playlistName]]; // Get the current video object const video = { ...targetPlaylist[videoIndex] }; // Update the video's status based on message type if (message.type === "pause") { video.status = message.timestamp; // Set status to current timestamp } else if (message.type === "ended") { video.status = "done"; // Set status to "done" } // Update the video in the playlist targetPlaylist[videoIndex] = video; updatedPlaylists[playlistName] = targetPlaylist; // Save the updated playlists back to storage await browser.storage.local.set({ playlists: updatedPlaylists }); } } async function updateHistory(message) { const { history: currentHistory } = await browser.storage.local.get("history"); const q = new URL(message.url); const v = q.searchParams.get("v"); if (currentHistory[v]) { const { [v]: existing, ...rest } = currentHistory; const shouldIgnore = message.type === "playing" && existing?.history?.length > 0 && (existing.history[existing.history.length - 1].action === "play" || existing.history[existing.history.length - 1].action === "playing") && message.timestamp === existing.history[existing.history.length - 1].position; if (!shouldIgnore) { await browser.storage.local.set({ history: { [v]: { ...existing, title: message.title, duration: !isNaN(existing.duration) && isFinite(existing.duration) ? Math.max(message.duration, existing.duration) : message.duration, history: [ ...existing.history, { action: message.type, position: message.timestamp, timestamp: Date.now(), }, ], }, ...rest, }, }); } } else { await browser.storage.local.set({ history: { [v]: { url: message.url, title: message.title, duration: message.duration, history: [ { action: message.type, position: message.timestamp, timestamp: Date.now(), }, ], }, ...currentHistory, }, }); } } function findNext(playlists, url) { const result = PlaylistUtils.findVideoInPlaylists(playlists, url); if (!result) { return false; // Not found in any playlist } const [playlistName, videoIndex, isLastVideo] = result; if (isLastVideo) { return false; // Last video, no next video available } else { // Get next video in the playlist const nextVideo = playlists[playlistName][videoIndex + 1]; console.log("FOUND VIDEO", playlists[playlistName][videoIndex]); return nextVideo; } } async function navigateAndWait(tabId, url) { return new Promise((resolve) => { const listener = (details) => { if (details.tabId === tabId && details.frameId === 0) { browser.webNavigation.onCompleted.removeListener(listener); setTimeout(resolve, 500); } }; browser.webNavigation.onCompleted.addListener(listener); browser.tabs.update(tabId, { url }); }); } async function advance(tabId, url) { const { playlists } = await browser.storage.local.get("playlists"); const nextVideo = await findNext(playlists, url); if (nextVideo) { await navigateAndWait(tabId, nextVideo.url); await browser.tabs.sendMessage(tabId, { type: "autoplay" }); } } // Listen for messages from popup or content scripts browser.runtime.onMessage.addListener((message, sender, _sendResponse) => { console.log("MESSAGE", message, sender); switch (message.type) { case "play": case "playing": case "pause": updateTracking(message); updateHistory(message); break; case "ended": updateTracking(message); updateHistory(message); advance(sender.tab.id, message.url); break; } });