// Background service worker importScripts("atproto.js"); importScripts("musicbrainz.js"); const atproto = new ATProtoClient(); const musicbrainz = new MusicBrainzClient(); let scrobbleQueue = []; let pendingScrobbleTimeout = null; let currentTrack = null; let recentTracks = []; // Initialize on install chrome.runtime.onInstalled.addListener(async () => { console.log("NTS Radio Scrobbler installed"); await atproto.loadSession(); await loadRecentTracks(); }); // Load session on startup chrome.runtime.onStartup.addListener(async () => { await atproto.loadSession(); await loadRecentTracks(); }); // Listen for messages from content script chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "NEW_TRACK") { handleNewTrack(message.data); sendResponse({ received: true }); } else if (message.type === "LOGIN") { handleLogin(message.data) .then(sendResponse) .catch((error) => sendResponse({ error: error.message })); return true; // Keep channel open for async response } else if (message.type === "LOGOUT") { handleLogout() .then(sendResponse) .catch((error) => sendResponse({ error: error.message })); return true; } else if (message.type === "GET_AUTH_STATUS") { sendResponse({ authenticated: atproto.isAuthenticated() }); } else if (message.type === "GET_CURRENT_TRACK") { sendResponse({ track: currentTrack }); } else if (message.type === "GET_RECENT_TRACKS") { sendResponse({ tracks: recentTracks }); } }); async function handleNewTrack(trackInfo) { // Store current track currentTrack = trackInfo; // Notify popup of track update chrome.runtime.sendMessage({ type: 'TRACK_UPDATE' }).catch(() => { // Ignore errors if popup is not open }); if (!atproto.isAuthenticated()) { console.log("Not authenticated - skipping scrobble"); return; } // Check if auto-scrobble is enabled const settings = await chrome.storage.local.get("autoScrobble"); if (settings.autoScrobble === false) { return; } // Cancel any pending scrobble if (pendingScrobbleTimeout) { clearTimeout(pendingScrobbleTimeout); pendingScrobbleTimeout = null; } // Wait 30 seconds before scrobbling pendingScrobbleTimeout = setTimeout(async () => { try { // Check for duplicates if (isDuplicate(trackInfo)) { console.log('Skipping duplicate:', `${trackInfo.artist} - ${trackInfo.track}`); return; } // Fetch MusicBrainz metadata const mbInfo = await musicbrainz.searchRecording(trackInfo.artist, trackInfo.track); if (mbInfo) { trackInfo.musicbrainz = mbInfo; } await atproto.createScrobbleRecord(trackInfo); console.log('✓ Scrobbled:', `${trackInfo.artist} - ${trackInfo.track}`); // Add to recent tracks addToRecentTracks(trackInfo); // Notify popup of scrobble success chrome.runtime.sendMessage({ type: 'SCROBBLE_SUCCESS' }).catch(() => { // Ignore errors if popup is not open }); // Show notification chrome.notifications.create({ type: "basic", iconUrl: "icons/icon48.png", title: "Track Scrobbled", message: `${trackInfo.artist} - ${trackInfo.track}`, }); } catch (error) { console.error("Failed to scrobble:", error); // If token expired, try to refresh and retry if (error.message.includes("ExpiredToken") || error.message.includes("token")) { try { console.log("Token expired, refreshing..."); await atproto.refreshSession(); console.log("Token refreshed, retrying scrobble..."); await atproto.createScrobbleRecord(trackInfo); console.log('✓ Scrobbled (after refresh):', `${trackInfo.artist} - ${trackInfo.track}`); // Add to recent tracks addToRecentTracks(trackInfo); // Notify popup of scrobble success chrome.runtime.sendMessage({ type: 'SCROBBLE_SUCCESS' }).catch(() => { // Ignore errors if popup is not open }); chrome.notifications.create({ type: "basic", iconUrl: "icons/icon48.png", title: "Track Scrobbled", message: `${trackInfo.artist} - ${trackInfo.track}`, }); } catch (refreshError) { console.error("Failed after token refresh:", refreshError); } } } finally { pendingScrobbleTimeout = null; } }, 30000); // 30 seconds } async function handleLogin({ identifier, password, pdsUrl }) { try { const session = await atproto.login(identifier, password, pdsUrl); return { success: true, session }; } catch (error) { return { success: false, error: error.message }; } } async function handleLogout() { await atproto.logout(); return { success: true }; } async function loadRecentTracks() { const data = await chrome.storage.local.get('recentTracks'); if (data.recentTracks) { recentTracks = data.recentTracks; } } async function saveRecentTracks() { await chrome.storage.local.set({ recentTracks }); } function isDuplicate(trackInfo) { // Check if this track was recently scrobbled return recentTracks.some(track => track.artist === trackInfo.artist && track.track === trackInfo.track ); } function addToRecentTracks(trackInfo) { // Add to recent tracks recentTracks.unshift({ artist: trackInfo.artist, track: trackInfo.track, timestamp: Date.now() }); // Keep only last 5 recentTracks = recentTracks.slice(0, 5); // Save to storage saveRecentTracks(); }