Files
vrobbler-firefox-extension/background.js

174 lines
5.4 KiB
JavaScript

// --- LifeScrobbler background script ---
// Requires shared_settings.js for DEFAULT_SETTINGS and helpers.
const activeTabs = new Map(); // tabId -> timeout
const currentURLs = new Map(); // tabId -> current URL
const pausedTabs = new Set(); // tabId -> paused state
// ---------------------------------------------------------------------------
// Utility helpers
// ---------------------------------------------------------------------------
async function getSettings() {
return new Promise(resolve => {
browser.storage.local.get(DEFAULT_SETTINGS, resolve);
});
}
function updateIcon(tabId, state) {
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 });
}
// Enhanced blacklist matcher with wildcard and regex support
function isBlacklisted(url, blacklist) {
try {
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);
}
}
async function scrobbleStop(url, baseUrl) {
if (!url) return;
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);
}
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
// 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 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);
}
// Schedule start scrobble
const siteDelay = getDelayForUrl(newUrl, settings);
if (activeTabs.has(tabId)) clearTimeout(activeTabs.get(tabId));
const timeout = setTimeout(() => {
scrobbleStart(newUrl, scrobbleBaseUrl, prevUrl);
updateIcon(tabId, "scrobbled");
setTimeout(() => updateIcon(tabId, "wait"), 3000);
}, siteDelay * 1000);
activeTabs.set(tabId, timeout);
});
// 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 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) });
});
// 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);
})();