Update latest version from CGPT
This commit is contained in:
258
background.js
258
background.js
@ -1,89 +1,99 @@
|
|||||||
// ======== Life Scrobbler Extension ========
|
// --- LifeScrobbler background script ---
|
||||||
const SCROBBLE_ENDPOINT = "https://life.lab.unbl.ink/?source=Firefox&scrobble_url=";
|
// Requires shared_settings.js for DEFAULT_SETTINGS and helpers.
|
||||||
const STOP_ENDPOINT = "https://life.lab.unbl.ink/?action=stop&scrobble_url=";
|
|
||||||
|
|
||||||
// ====== Default Settings ======
|
const activeTabs = new Map(); // tabId -> timeout
|
||||||
const DEFAULT_SETTINGS = {
|
const currentURLs = new Map(); // tabId -> current URL
|
||||||
delay: 8,
|
const pausedTabs = new Set(); // tabId -> paused state
|
||||||
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 = {
|
// Utility helpers
|
||||||
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() {
|
async function getSettings() {
|
||||||
return new Promise((resolve) => {
|
return new Promise(resolve => {
|
||||||
browser.storage.local.get(DEFAULT_SETTINGS, 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) {
|
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 ======
|
// Enhanced blacklist matcher with wildcard and regex support
|
||||||
async function scrobbleStart(url, baseUrl) {
|
function isBlacklisted(url, blacklist) {
|
||||||
const endpoint = `${baseUrl}/?scrobble_url=${encodeURIComponent(url)}`;
|
|
||||||
try {
|
try {
|
||||||
await fetch(endpoint, { method: "GET" });
|
const { hostname, href } = new URL(url);
|
||||||
console.log("LifeScrobbler: scrobbled", url);
|
const extraBlacklist = ["moz-extension://"]; // always skipped
|
||||||
} catch (err) {
|
const combined = [...blacklist, ...extraBlacklist];
|
||||||
console.error("LifeScrobbler: failed scrobble", url, err);
|
|
||||||
|
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) {
|
async function scrobbleStop(url, baseUrl) {
|
||||||
|
if (!url) return;
|
||||||
const endpoint = `${baseUrl}/?action=stop&scrobble_url=${encodeURIComponent(url)}`;
|
const endpoint = `${baseUrl}/?action=stop&scrobble_url=${encodeURIComponent(url)}`;
|
||||||
try {
|
try {
|
||||||
await fetch(endpoint, { method: "GET" });
|
await fetch(endpoint, { method: "GET" });
|
||||||
@ -93,48 +103,35 @@ async function scrobbleStop(url, baseUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== Delay for given URL ======
|
// ---------------------------------------------------------------------------
|
||||||
function getDelayForUrl(url, settings) {
|
// Event handlers
|
||||||
try {
|
// ---------------------------------------------------------------------------
|
||||||
const u = new URL(url);
|
|
||||||
console.log(u);
|
// Main listener: when navigating to a new URL
|
||||||
for (const [domain, customDelay] of Object.entries(settings.siteDelays)) {
|
browser.webNavigation.onCommitted.addListener(async (details) => {
|
||||||
console.log(domain, customDelay, u.hostname, domain == u.hostname);
|
if (details.frameId !== 0) return; // only main frame
|
||||||
if (u.hostname.includes(domain)) {
|
const tabId = details.tabId;
|
||||||
console.log("Found custom delay of ", customDelay);
|
const newUrl = details.url;
|
||||||
return customDelay;
|
const prevUrl = currentURLs.get(tabId) || null;
|
||||||
|
currentURLs.set(tabId, newUrl);
|
||||||
|
|
||||||
|
const settings = await getSettings();
|
||||||
|
const { paused, blacklist, scrobbleBaseUrl } = settings;
|
||||||
|
|
||||||
|
// Skip if paused or blacklisted
|
||||||
|
if (paused || pausedTabs.has(tabId) || isBlacklisted(newUrl, blacklist)) return;
|
||||||
|
|
||||||
|
// Stop previous URL first (if valid)
|
||||||
|
if (prevUrl && !isBlacklisted(prevUrl, blacklist)) {
|
||||||
|
scrobbleStop(prevUrl, scrobbleBaseUrl);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} 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 ...");
|
|
||||||
|
|
||||||
|
// Schedule start scrobble
|
||||||
|
const siteDelay = getDelayForUrl(newUrl, settings);
|
||||||
if (activeTabs.has(tabId)) clearTimeout(activeTabs.get(tabId));
|
if (activeTabs.has(tabId)) clearTimeout(activeTabs.get(tabId));
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
scrobbleStart(url, scrobbleBaseUrl);
|
scrobbleStart(newUrl, scrobbleBaseUrl, prevUrl);
|
||||||
updateIcon(tabId, "scrobbled");
|
updateIcon(tabId, "scrobbled");
|
||||||
setTimeout(() => updateIcon(tabId, "wait"), 3000);
|
setTimeout(() => updateIcon(tabId, "wait"), 3000);
|
||||||
}, siteDelay * 1000);
|
}, siteDelay * 1000);
|
||||||
@ -142,24 +139,35 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
|||||||
activeTabs.set(tabId, timeout);
|
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) => {
|
browser.browserAction.onClicked.addListener(async (tab) => {
|
||||||
const { paused } = await getSettings();
|
const settings = await getSettings();
|
||||||
const tabPaused = pausedTabs.has(tab.id) || paused;
|
const tabId = tab.id;
|
||||||
setPaused(tab.id, !tabPaused);
|
if (pausedTabs.has(tabId)) {
|
||||||
});
|
|
||||||
|
|
||||||
browser.tabs.onRemoved.addListener((tabId) => {
|
|
||||||
if (activeTabs.has(tabId)) clearTimeout(activeTabs.get(tabId));
|
|
||||||
pausedTabs.delete(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) });
|
||||||
});
|
});
|
||||||
|
|
||||||
<<<<<<< HEAD
|
// Restore paused tabs on startup
|
||||||
browser.webNavigation.onBeforeNavigate.addListener((details) => {
|
(async function restorePausedTabs() {
|
||||||
stopScrobble(details.tabId, details.url);
|
const data = await browser.storage.local.get({ pausedTabs: [] });
|
||||||
});
|
for (const tabId of data.pausedTabs) pausedTabs.add(tabId);
|
||||||
|
})();
|
||||||
browser.browserAction.onClicked.addListener(async () => {
|
|
||||||
const { paused } = await getSettings();
|
|
||||||
setPaused(!paused);
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "LifeScrobbler",
|
"name": "LifeScrobbler",
|
||||||
"version": "3.0.0",
|
"version": "4.0.0",
|
||||||
"description": "Scrobble visited pages to life.lab.unbl.ink after a delay, skipping blacklisted or paused tabs.",
|
"description": "Scrobbles visited URLs to a configurable endpoint after a delay.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"tabs",
|
"tabs",
|
||||||
"storage",
|
"storage",
|
||||||
"webNavigation",
|
"webNavigation",
|
||||||
"https://life.lab.unbl.ink/*"
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background.js"]
|
"scripts": ["shared_settings.js", "background.js"]
|
||||||
},
|
},
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"48": "icons/stop.png"
|
"16": "icons/icon-16.png",
|
||||||
|
"32": "icons/icon-32.png",
|
||||||
|
"48": "icons/icon-48.png"
|
||||||
},
|
},
|
||||||
"default_title": "LifeScrobbler"
|
"default_title": "LifeScrobbler"
|
||||||
},
|
},
|
||||||
@ -23,7 +25,9 @@
|
|||||||
"open_in_tab": true
|
"open_in_tab": true
|
||||||
},
|
},
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "icons/check.png",
|
"16": "icons/icon-16.png",
|
||||||
"96": "icons/check.png"
|
"32": "icons/icon-32.png",
|
||||||
|
"48": "icons/icon-48.png",
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
shared_settings.js
Normal file
25
shared_settings.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user