// ======== Life Scrobbler Extension ======== // Fully self-contained version with base64 icons // Stop scrobble code is commented out for now // ====== Default Settings ====== const DEFAULT_SETTINGS = { delay: 8, blacklist: [ "*.unbl.ink", "moz-extension://", "*.google.com", "gmail.com", "*.chatgpt.com", "*.ebay.com", "*.amazon.com", "*.merrysky.net", "gemgetter.clearlysharp.com/", "*.boardgamegeek.com", "*.duckduckgo.com", "*.geekgroup.app", "*.local", "*.service", "*.todoist.com", ], paused: false, siteDelays: { "readcomicsonline.ru": 1 }, scrobbleBaseUrl: "https://life.lab.unbl.ink", }; // ====== Base64 icons ====== const ICONS = { wait: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIA...", scrobbled: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIA...", stopped: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIA...", pause: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIA...", }; // ====== State ====== let activeTabs = new Map(); // tabId -> timeout let pausedTabs = new Set(); // tabIds that are paused // ====== Utility Functions ====== function matchPattern(url, pattern) { if (pattern.startsWith("*.")) { const domain = pattern.slice(2); return url.includes(domain); } return url.startsWith(pattern); } function isBlacklisted(url, blacklist) { return blacklist.some((p) => matchPattern(url, p)); } async function getSettings() { return new Promise((resolve) => { browser.storage.local.get(DEFAULT_SETTINGS, resolve); }); } async function setPaused(tabId, paused) { if (paused) pausedTabs.add(tabId); else pausedTabs.delete(tabId); updateIcon(tabId, paused ? "pause" : "wait"); // persist globally await browser.storage.local.set({ paused }); } function updateIcon(tabId, state) { browser.browserAction.setIcon({ path: ICONS[state], tabId }); } // ====== Scrobble Start ====== async function scrobbleStart(url, baseUrl) { const endpoint = `${baseUrl}/?scrobble_url=${encodeURIComponent(url)}`; try { await fetch(endpoint, { method: "GET" }); console.log("LifeScrobbler: scrobbled", url); } catch (err) { console.error("LifeScrobbler: failed scrobble", url, err); } } // ====== Scrobble Stop ====== async function scrobbleStop(url, baseUrl) { const endpoint = `${baseUrl}/?action=stop&scrobble_url=${encodeURIComponent(url)}`; try { await fetch(endpoint, { method: "GET" }); console.log("LifeScrobbler: stopped", url); } catch (err) { console.error("LifeScrobbler: stop failed", url, err); } } // ====== Delay for given URL ====== function getDelayForUrl(url, settings) { try { const u = new URL(url); console.log(u); for (const [domain, customDelay] of Object.entries(settings.siteDelays)) { console.log(domain, customDelay, u.hostname, domain == u.hostname); if (u.hostname.includes(domain)) { console.log("Found custom delay of ", customDelay); return customDelay; } } } catch (err) {} return settings.delay; } // ====== Tab Updates ====== browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { if (changeInfo.status !== "complete" || !tab.url) return; const { delay, blacklist, scrobbleBaseUrl, paused } = await getSettings(); const url = tab.url; // Skip if globally paused or blacklisted console.log( "Globally paused? ", paused, " - Is this tab paused? ", pausedTabs.has(tabId), " - Is site blacklisted? ", isBlacklisted(url, blacklist), ); if (paused || pausedTabs.has(tabId) || isBlacklisted(url, blacklist)) return; updateIcon(tabId, "wait"); const siteDelay = getDelayForUrl(url, await getSettings()); console.log("Waiting " + siteDelay + " seconds before scrobbling ..."); if (activeTabs.has(tabId)) clearTimeout(activeTabs.get(tabId)); const timeout = setTimeout(() => { scrobbleStart(url, scrobbleBaseUrl); updateIcon(tabId, "scrobbled"); setTimeout(() => updateIcon(tabId, "wait"), 3000); }, siteDelay * 1000); activeTabs.set(tabId, timeout); }); // ====== Pause Toggle via Toolbar Icon ====== browser.browserAction.onClicked.addListener(async (tab) => { const { paused } = await getSettings(); const tabPaused = pausedTabs.has(tab.id) || paused; setPaused(tab.id, !tabPaused); }); // ====== Clean up on Tab Close ====== browser.tabs.onRemoved.addListener((tabId) => { if (activeTabs.has(tabId)) clearTimeout(activeTabs.get(tabId)); pausedTabs.delete(tabId); }); // ====== Web Navigation stop code is commented ====== browser.webNavigation.onBeforeNavigate.addListener((details) => { const { scrobbleBaseUrl } = DEFAULT_SETTINGS; // scrobbleStop(details.url, scrobbleBaseUrl); // TODO need to fix this in Scrobbler to check if a running scrobble exists and stop if it does });