// 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-1": [], "listening-2": [], "listening-3": [], "watching-1": [], "watching-2": [], }, }); } 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-1", parentId: "my-playlist-menu", title: "Listening - 1", contexts: ["link"], }); browser.contextMenus.create({ id: "listening-2", parentId: "my-playlist-menu", title: "Listening - 2", contexts: ["link"], }); browser.contextMenus.create({ id: "listening-3", parentId: "my-playlist-menu", title: "Listening - 3", contexts: ["link"], }); // Separator browser.contextMenus.create({ id: "separator-1", parentId: "my-playlist-menu", type: "separator", contexts: ["link"], }); browser.contextMenus.create({ id: "watching-1", parentId: "my-playlist-menu", title: "Watching - 1", contexts: ["link"], }); browser.contextMenus.create({ id: "watching-2", parentId: "my-playlist-menu", title: "Watching - 2", contexts: ["link"], }); }); function findPlaylist(current, url) { // Extract the video id from URL query params let v; try { const urlObj = new URL(url); v = urlObj.searchParams.get("v"); } catch (e) { return false; } // If no video id found, return false if (!v) { return false; } // Check each playlist in the current set for (const pl in current) { const videos = current[pl]; // Check each video in the array for (const video of videos) { try { const itemUrl = new URL(video.url); const itemVParam = itemUrl.searchParams.get("v"); // If the "v" parameters match, return this pl if (itemVParam === v) { return pl; } } catch (e) { // Skip malformed URLs continue; } } } // No match found return false; } async function addLinkToPlaylist(plName, item) { const { playlists: currentPlaylists } = await browser.storage.local.get("playlists"); const alreadyHave = findPlaylist(currentPlaylists, item.linkUrl); if (alreadyHave) { console.log("already have that link in", alreadyHave); } else { const { [plName]: playlist, ...others } = currentPlaylists; await browser.storage.local.set({ playlists: { [plName]: [...playlist, { url: item.linkUrl, title: item.linkText }], ...others, }, }); } } // Context menu click handler browser.contextMenus.onClicked.addListener(async (item, _tab) => { 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 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, 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) { // Extract the "v" query parameter from input URL let v; try { const urlObj = new URL(url); v = urlObj.searchParams.get("v"); } catch (e) { return false; } // If no "v" parameter found, return false if (!v) { return false; } // Check each video in the playlists for (const pl in playlists) { const videos = playlists[pl]; // Check each video in the array for (let i = 0; i < videos.length; i++) { try { const itemUrl = new URL(videos[i].url); const itemVParam = itemUrl.searchParams.get("v"); // If the "v" parameters match if (itemVParam === v) { console.log("FOUND VIDEO", videos[i]); // Check if this is the last video in the array if (i === videos.length - 1) { return false; // Last video, no next video available } else { return videos[i + 1]; // Return the next video } } } catch (e) { // Skip malformed URLs continue; } } } // No match found return false; } async function navigateAndWait(tabId, url) { return new Promise((resolve) => { const listener = (details) => { if (details.tabId === tabId && details.frameId === 0) { browser.webNavigation.onCompleted.removeListener(listener); resolve(); } }; 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": updateHistory(message); break; case "ended": updateHistory(message); advance(sender.tab.id, message.url); break; } });