From e1f53b5592948a20c469251f92988b3dba19fb53 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 3 Nov 2025 09:00:30 -0500 Subject: [PATCH] Update latest version from CGPT --- background.js | 254 +++++++++++++++++++++++---------------------- manifest.json | 18 ++-- shared_settings.js | 25 +++++ 3 files changed, 167 insertions(+), 130 deletions(-) create mode 100644 shared_settings.js diff --git a/background.js b/background.js index 56d079c..55f258a 100644 --- a/background.js +++ b/background.js @@ -1,89 +1,99 @@ -// ======== Life Scrobbler Extension ======== -const SCROBBLE_ENDPOINT = "https://life.lab.unbl.ink/?source=Firefox&scrobble_url="; -const STOP_ENDPOINT = "https://life.lab.unbl.ink/?action=stop&scrobble_url="; +// --- LifeScrobbler background script --- +// Requires shared_settings.js for DEFAULT_SETTINGS and helpers. -// ====== 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", -}; +const activeTabs = new Map(); // tabId -> timeout +const currentURLs = new Map(); // tabId -> current URL +const pausedTabs = new Set(); // tabId -> paused state -// ====== 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)); -} +// --------------------------------------------------------------------------- +// Utility helpers +// --------------------------------------------------------------------------- async function getSettings() { - return new Promise((resolve) => { + 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 }); + const icons = { + paused: "icons/icon-paused.png", + wait: "icons/icon-wait.png", + scrobbled: "icons/icon-scrobbled.png" + }; + const path = icons[state] || "icons/icon-32.png"; + browser.browserAction.setIcon({ tabId, path }); } -// ====== Scrobble Start ====== -async function scrobbleStart(url, baseUrl) { - const endpoint = `${baseUrl}/?scrobble_url=${encodeURIComponent(url)}`; +// Enhanced blacklist matcher with wildcard and regex support +function isBlacklisted(url, blacklist) { try { - await fetch(endpoint, { method: "GET" }); - console.log("LifeScrobbler: scrobbled", url); - } catch (err) { - console.error("LifeScrobbler: failed scrobble", url, err); + const { hostname, href } = new URL(url); + const extraBlacklist = ["moz-extension://"]; // always skipped + const combined = [...blacklist, ...extraBlacklist]; + + return combined.some(entry => { + if (!entry) return false; + entry = entry.trim(); + + // Regex support + if (entry.startsWith("/") && entry.endsWith("/")) { + try { + const re = new RegExp(entry.slice(1, -1)); + return re.test(href); + } catch { + return false; + } + } + + // Wildcard domain support + if (entry.startsWith("*.")) { + const domain = entry.slice(2).toLowerCase(); + return ( + hostname.toLowerCase().endsWith("." + domain) || + hostname.toLowerCase() === domain + ); + } + + // Substring match + return href.includes(entry); + }); + } catch (e) { + console.warn("isBlacklisted error:", e); + return false; + } +} + +// Per-site delay override (e.g., amazon.com every 1 second) +function getDelayForUrl(url, settings) { + const hostname = new URL(url).hostname; + const overrides = settings.siteDelays || {}; + for (const [pattern, delay] of Object.entries(overrides)) { + if (hostname.includes(pattern)) { + return delay; + } + } + return settings.delaySeconds; +} + +// --------------------------------------------------------------------------- +// Scrobbling functions +// --------------------------------------------------------------------------- + +async function scrobbleStart(url, baseUrl, referrer = null) { + let endpoint = `${baseUrl}/?source=Firefox&scrobble_url=${encodeURIComponent(url)}`; + if (referrer) endpoint += `&referrer=${encodeURIComponent(referrer)}`; + + try { + await fetch(endpoint, { method: "GET" }); + console.log("LifeScrobbler: started", url, referrer ? `(ref: ${referrer})` : ""); + } catch (err) { + console.error("LifeScrobbler: start failed", url, err); } } -// ====== Scrobble Stop ====== async function scrobbleStop(url, baseUrl) { + if (!url) return; const endpoint = `${baseUrl}/?action=stop&scrobble_url=${encodeURIComponent(url)}`; try { await fetch(endpoint, { method: "GET" }); @@ -93,48 +103,35 @@ async function scrobbleStop(url, baseUrl) { } } -// ====== 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; -} +// --------------------------------------------------------------------------- +// Event handlers +// --------------------------------------------------------------------------- -// ====== Tab Updates ====== -browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { - if (changeInfo.status !== "complete" || !tab.url) return; +// Main listener: when navigating to a new URL +browser.webNavigation.onCommitted.addListener(async (details) => { + if (details.frameId !== 0) return; // only main frame + const tabId = details.tabId; + const newUrl = details.url; + const prevUrl = currentURLs.get(tabId) || null; + currentURLs.set(tabId, newUrl); - const { delay, blacklist, scrobbleBaseUrl, paused } = await getSettings(); - const url = tab.url; + const settings = await getSettings(); + const { paused, blacklist, scrobbleBaseUrl } = settings; - // Skip if globally paused or blacklisted - console.log( - "Globally paused? ", - paused, - " - Is this tab paused? ", - pausedTabs.has(tabId), - " - Is site blacklisted? ", - isBlacklisted(url, blacklist), - ); + // Skip if paused or blacklisted + if (paused || pausedTabs.has(tabId) || isBlacklisted(newUrl, blacklist)) return; - 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 ..."); + // Stop previous URL first (if valid) + if (prevUrl && !isBlacklisted(prevUrl, blacklist)) { + scrobbleStop(prevUrl, scrobbleBaseUrl); + } + // Schedule start scrobble + const siteDelay = getDelayForUrl(newUrl, settings); if (activeTabs.has(tabId)) clearTimeout(activeTabs.get(tabId)); + const timeout = setTimeout(() => { - scrobbleStart(url, scrobbleBaseUrl); + scrobbleStart(newUrl, scrobbleBaseUrl, prevUrl); updateIcon(tabId, "scrobbled"); setTimeout(() => updateIcon(tabId, "wait"), 3000); }, siteDelay * 1000); @@ -142,24 +139,35 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { activeTabs.set(tabId, timeout); }); -// ====== Pause Toggle via Toolbar Icon ====== +// Handle tab close (send stop) +browser.tabs.onRemoved.addListener(async (tabId) => { + const url = currentURLs.get(tabId); + if (!url) return; + const settings = await getSettings(); + const { blacklist, scrobbleBaseUrl } = settings; + if (isBlacklisted(url, blacklist)) return; + scrobbleStop(url, scrobbleBaseUrl); + activeTabs.delete(tabId); + currentURLs.delete(tabId); +}); + +// Toggle pause via toolbar icon click browser.browserAction.onClicked.addListener(async (tab) => { - const { paused } = await getSettings(); - const tabPaused = pausedTabs.has(tab.id) || paused; - setPaused(tab.id, !tabPaused); + const settings = await getSettings(); + const tabId = tab.id; + if (pausedTabs.has(tabId)) { + pausedTabs.delete(tabId); + updateIcon(tabId, "wait"); + } else { + pausedTabs.add(tabId); + updateIcon(tabId, "paused"); + } + // Persist pause state + await browser.storage.local.set({ pausedTabs: Array.from(pausedTabs) }); }); -browser.tabs.onRemoved.addListener((tabId) => { - if (activeTabs.has(tabId)) clearTimeout(activeTabs.get(tabId)); - pausedTabs.delete(tabId); -}); - -<<<<<<< HEAD -browser.webNavigation.onBeforeNavigate.addListener((details) => { - stopScrobble(details.tabId, details.url); -}); - -browser.browserAction.onClicked.addListener(async () => { - const { paused } = await getSettings(); - setPaused(!paused); -}); +// Restore paused tabs on startup +(async function restorePausedTabs() { + const data = await browser.storage.local.get({ pausedTabs: [] }); + for (const tabId of data.pausedTabs) pausedTabs.add(tabId); +})(); diff --git a/manifest.json b/manifest.json index 4b0114a..a5680c9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,20 +1,22 @@ { "manifest_version": 2, "name": "LifeScrobbler", - "version": "3.0.0", - "description": "Scrobble visited pages to life.lab.unbl.ink after a delay, skipping blacklisted or paused tabs.", + "version": "4.0.0", + "description": "Scrobbles visited URLs to a configurable endpoint after a delay.", "permissions": [ "tabs", "storage", "webNavigation", - "https://life.lab.unbl.ink/*" + "" ], "background": { - "scripts": ["background.js"] + "scripts": ["shared_settings.js", "background.js"] }, "browser_action": { "default_icon": { - "48": "icons/stop.png" + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png" }, "default_title": "LifeScrobbler" }, @@ -23,7 +25,9 @@ "open_in_tab": true }, "icons": { - "48": "icons/check.png", - "96": "icons/check.png" + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" } } diff --git a/shared_settings.js b/shared_settings.js new file mode 100644 index 0000000..3e9b8b0 --- /dev/null +++ b/shared_settings.js @@ -0,0 +1,25 @@ +const DEFAULT_SETTINGS = { + delaySeconds: 7, + scrobbleBaseUrl: "https://life.lab.unbl.ink", + 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 + } +};