// Shared capture helpers (parseSalesHistoryHtml, parseListingsHtml, // buildResearchJson, combineReleaseData) — same module the popup loads. importScripts('monster_capture.js'); // ── MV3 keepalive ───────────────────────────────────────────────────── // chrome.alarms fires even when the service worker is hibernated, waking it. // Combined with the setInterval pollers below, this keeps PliceCogs's COGS // proxy poll loop alive indefinitely without needing an open discogs tab. chrome.alarms.create('plicecogs-keepalive', { periodInMinutes: 0.5 }); chrome.alarms.onAlarm.addListener(() => { /* wake-only, no work */ }); // YouTube Cookie Management // BLAGGINATE bridge connection variables let blagginateTabId = null; let isBlagginateConnected = false; let lastBlagginateConnectionAttempt = 0; let blagginateConnectionAttempts = 0; // Listen for YouTube tab updates chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete' && tab.url?.includes('youtube.com')) { chrome.storage.local.get(['autoUpdate'], function(result) { if (result.autoUpdate === true) { updateCookies(); } }); } }); function updateCookies() { console.log('Starting updateCookies function...'); return new Promise((resolve, reject) => { try { chrome.cookies.getAll({ domain: ".youtube.com" }, function(cookies) { if (chrome.runtime.lastError) { reject(chrome.runtime.lastError); return; } console.log(`Found ${cookies.length} cookies`); // Format cookies in Netscape format let cookieText = "# Netscape HTTP Cookie File\n"; cookieText += "# https://curl.haxx.se/rfc/cookie_spec.html\n"; cookieText += "# This file was generated by YouTube Cookie Extractor\n\n"; cookies.forEach(cookie => { cookieText += [ ".youtube.com", "TRUE", cookie.path, cookie.secure ? "TRUE" : "FALSE", Math.floor(cookie.expirationDate || (Date.now() / 1000) + 31536000), cookie.name, cookie.value ].join('\t') + '\n'; }); // Store cookie text in local storage chrome.storage.local.set({ lastCookie: cookieText, lastUpdate: Date.now() }, () => { console.log('Cookie saved to storage'); // Push to ytanalyse cookie server if it's running (silently no-ops if not) fetch('http://localhost:7792/cookies', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: cookieText, }).catch(() => {}); // ytanalyse not running — that's fine // Send cookie update to BLAGGINATE page if connected if (blagginateTabId && isBlagginateConnected) { chrome.tabs.sendMessage(blagginateTabId, { type: 'cookieUpdate', cookies: cookieText, timestamp: Date.now() }).catch(error => { console.log('Failed to send cookie to BLAGGINATE page:', error); // Reset connection status if tab is no longer available isBlagginateConnected = false; blagginateTabId = null; }); console.log('Cookie sent to BLAGGINATE page'); } // Resolve with cookie text resolve({ cookieText: cookieText, success: true }); }); }); } catch (error) { reject(error); } }); } // Build Cookie-Editor / EditThisCookie style JSON array — the format // JDownloader 2 (and most cookie-importers) expect when pasting cookies. function getYouTubeCookiesJson() { return new Promise((resolve, reject) => { chrome.cookies.getAll({ domain: ".youtube.com" }, (cookies) => { if (chrome.runtime.lastError) { reject(chrome.runtime.lastError); return; } const out = cookies.map(c => ({ domain: c.domain, expirationDate: c.expirationDate, // omitted for session cookies hostOnly: !!c.hostOnly, httpOnly: !!c.httpOnly, name: c.name, path: c.path, // chrome.cookies sameSite: "no_restriction" | "lax" | "strict" | "unspecified" // Cookie-Editor uses the same vocabulary, so pass through. sameSite: c.sameSite || "unspecified", secure: !!c.secure, session: !!c.session, storeId: c.storeId || null, value: c.value })); resolve(JSON.stringify(out, null, 2)); }); }); } function keepAlive() { setInterval(() => { chrome.runtime.getPlatformInfo(() => { if (chrome.runtime.lastError) { console.log('Keeping service worker alive'); } }); }, 20000); } // BLAGGINATE bridge connection system function connectToBlagginate() { // Check if BLAGGINATE bridge is enabled chrome.storage.local.get(['blagginateBridge'], (result) => { if (result.blagginateBridge === false) { return; // Bridge is disabled, don't attempt connection } const now = Date.now(); // Throttle connection attempts and logging if (now - lastBlagginateConnectionAttempt < 30000) { // 30 seconds return; } lastBlagginateConnectionAttempt = now; blagginateConnectionAttempts++; // Look for BLAGGINATE page tab chrome.tabs.query({}, (tabs) => { const blagginateTab = tabs.find(tab => tab.url && tab.url.includes('wowplatter-admin-blagginate') ); if (blagginateTab) { blagginateTabId = blagginateTab.id; // Test connection by sending a ping chrome.tabs.sendMessage(blagginateTabId, { type: 'ping', timestamp: Date.now() }).then(() => { isBlagginateConnected = true; blagginateConnectionAttempts = 0; // Reset counter on successful connection console.log('Connected to BLAGGINATE page'); // Send initial cookie update updateCookies(); }).catch(error => { if (blagginateConnectionAttempts <= 3) { // Only log first 3 attempts console.log('BLAGGINATE page not ready for communication:', error); } isBlagginateConnected = false; blagginateTabId = null; }); } else { if (blagginateConnectionAttempts <= 3) { // Only log first 3 attempts console.log('BLAGGINATE page not found'); } isBlagginateConnected = false; blagginateTabId = null; } }); }); } // Listen for messages from content scripts (including BLAGGINATE page) chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'toggle_blagginate_bridge') { // Handle BLAGGINATE bridge toggle from popup const enabled = message.enabled; chrome.storage.local.set({ blagginateBridge: enabled }, () => { if (!enabled) { // If disabled, disconnect from BLAGGINATE isBlagginateConnected = false; blagginateTabId = null; console.log('BLAGGINATE bridge disabled'); } else { console.log('BLAGGINATE bridge enabled'); // Try to reconnect if enabled connectToBlagginate(); } }); sendResponse({ success: true }); return; } if (message.type === 'blagginate_ready') { // BLAGGINATE page is ready to receive messages blagginateTabId = sender.tab.id; isBlagginateConnected = true; console.log('BLAGGINATE page connected'); // Send current cookies updateCookies().then(() => { sendResponse({ success: true }); }); return true; // Keep message channel open for async response } if (message.type === 'get_cookies') { // BLAGGINATE page requesting current cookies chrome.storage.local.get(['lastCookie'], (result) => { sendResponse({ cookies: result.lastCookie || null, timestamp: Date.now() }); }); return true; // Keep message channel open for async response } }); // Check for BLAGGINATE connection periodically setInterval(() => { if (!isBlagginateConnected) { connectToBlagginate(); } }, 10000); // Check every 10 seconds // Listen for tab removal to clean up connection chrome.tabs.onRemoved.addListener((tabId) => { if (tabId === blagginateTabId) { console.log('BLAGGINATE tab closed'); isBlagginateConnected = false; blagginateTabId = null; } }); // PriceClogs functionality chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // YouTube Cookie requests if (request.action === "getCookies") { console.log('Received getCookies request from popup'); updateCookies() .then(result => { console.log('UpdateCookies completed:', result); sendResponse(result); }) .catch(error => { console.error('UpdateCookies failed:', error); sendResponse({ error: error.message || 'Failed to update cookies', success: false }); }); return true; } // YouTube cookies in Cookie-Editor JSON format (for JDownloader 2 paste-into-password-field flow) if (request.action === "getCookiesJson") { getYouTubeCookiesJson() .then(json => sendResponse({ success: true, cookieJson: json })) .catch(err => sendResponse({ success: false, error: err.message || String(err) })); return true; } if (request.action === "postGeminiResult") { const result = request.result || {}; console.log('[gemini] postGeminiResult received', { release_id: result.release_id, job_id: result.job_id, prompt_chars: (result.prompt || '').length, output_chars: (result.output_text || '').length, }); chrome.storage.local.get(['monsterSettings'], async (r) => { const cfg = r.monsterSettings || {}; const remoteBase = cfg.serverUrl ? cfg.serverUrl : 'http://100.91.239.7:5002'; const bases = []; if (cfg.preferLocalhost === true) bases.push('http://localhost:5002'); bases.push(remoteBase); const urls = bases .map(b => String(b || '').trim()) .filter(Boolean) .map(b => b.replace(/\/$/, '') + '/plice/gemini'); try { let lastErr = null; for (const url of urls) { try { const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(result), signal: AbortSignal.timeout(8000), }); if (!resp.ok) { const text = await resp.text().catch(() => ''); console.log('[gemini] POST failed', { url, status: resp.status, body: text.slice(0, 500) }); lastErr = `HTTP ${resp.status} ${text}`.trim(); continue; } const data = await resp.json().catch(() => ({})); console.log('[gemini] POST ok', { url, data }); chrome.storage.local.set({ geminiPostLast: { ok: true, url, data, at: new Date().toISOString(), } }); sendResponse({ ok: true, data, url }); return; } catch (e) { const msg = e.message || String(e); console.log('[gemini] POST exception', { url, error: msg }); lastErr = msg; continue; } } chrome.storage.local.set({ geminiPostLast: { ok: false, urls, error: lastErr || 'All POST attempts failed', at: new Date().toISOString(), } }); sendResponse({ ok: false, error: lastErr || 'All POST attempts failed' }); } catch (e) { sendResponse({ ok: false, error: e.message || 'Fetch failed' }); } }); return true; } if (request.action === "closeSenderTab") { const tabId = sender && sender.tab ? sender.tab.id : null; if (!tabId) { sendResponse({ ok: false, error: 'No sender tab' }); return true; } chrome.tabs.remove(tabId, () => { if (chrome.runtime.lastError) { sendResponse({ ok: false, error: chrome.runtime.lastError.message }); } else { sendResponse({ ok: true }); } }); return true; } // PriceClogs file management if (request.action === "checkFileExists") { chrome.downloads.search({ filename: request.filename, exists: true }, async (downloadItems) => { try { if (downloadItems && downloadItems.length > 0) { const response = await fetch(`file://${downloadItems[0].filename}`); if (response.ok) { const data = await response.json(); sendResponse({ exists: true, data: data }); } else { sendResponse({ exists: false }); } } else { sendResponse({ exists: false }); } } catch (error) { console.error('Error checking file:', error); sendResponse({ exists: false }); } }); return true; } if (request.action === "downloadFile") { chrome.downloads.download({ url: request.url, filename: request.filename, conflictAction: 'overwrite', saveAs: false }, (downloadId) => { if (chrome.runtime.lastError) { sendResponse({ success: false, error: chrome.runtime.lastError }); } else { sendResponse({ success: true, downloadId }); } }); return true; } if (request.action === "getBeatportToken") { fetchBeatportToken(sendResponse); return true; } if (request.action === "refreshBeatportToken") { fetchBeatportToken((result) => { if (result && result.success && result.tokenJson) { chrome.storage.local.set({ beatportAuth: { data: result.tokenJson, timestamp: Date.now() } }, () => sendResponse(result)); } else { sendResponse(result); } }); return true; } }); function fetchBeatportToken(sendResponse) { console.log("Fetching Beatport token..."); // Cloudflare blocks service-worker fetches with a bot challenge (cf-mitigated: challenge → 403). // So we run the fetch inside an open beatport.com tab where the page already has CF clearance. // Fall back to a direct SW fetch only if no tab exists (will likely 403, but worth a try). chrome.tabs.query({ url: "*://*.beatport.com/*" }, (tabs) => { const handleResult = (data, errMsg) => { if (errMsg) { console.error("Beatport fetch error:", errMsg); sendResponse({ success: false, error: "fetch_failed", message: "Failed to connect to Beatport. Are you logged in? (Open a beatport.com tab and try again.)" }); return; } if (data && data.token && data.token.accessToken && data.token.accessToken !== 'session') { sendResponse({ success: true, tokenJson: JSON.stringify(data) }); } else { sendResponse({ success: false, error: "token_empty", message: "Beatport returned an empty session. Please log out and back in on beatport.com." }); } }; if (tabs && tabs.length > 0) { const tabId = tabs[0].id; chrome.scripting.executeScript({ target: { tabId }, world: "MAIN", func: async () => { try { const r = await fetch('/api/auth/session', { credentials: 'include' }); if (!r.ok) return { __err: `HTTP ${r.status}` }; return await r.json(); } catch (e) { return { __err: String(e && e.message || e) }; } } }, (results) => { if (chrome.runtime.lastError) { handleResult(null, chrome.runtime.lastError.message); return; } const data = results && results[0] && results[0].result; if (data && data.__err) { handleResult(null, data.__err); } else { handleResult(data, null); } }); return; } // No beatport tab open — try direct fetch as a last resort. fetch('https://www.beatport.com/api/auth/session', { credentials: 'include' }) .then(async (resp) => { if (!resp.ok) throw new Error(`HTTP ${resp.status}`); handleResult(await resp.json(), null); }) .catch((err) => handleResult(null, err && err.message || String(err))); }); } // Add external message handler for YouTube cookies chrome.runtime.onMessageExternal.addListener( function(request, sender, sendResponse) { if (request.action === "refreshCookie") { updateCookies() .then(result => { console.log('Cookies refreshed via external request'); sendResponse({ success: true, timestamp: Date.now() }); }) .catch(error => { console.error('Failed to refresh cookies:', error); sendResponse({ success: false, error: error.message }); }); return true; } } ); function setActionPopup(tabId, url) { chrome.action.setPopup({ tabId: tabId, popup: 'popup.html' }); } chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete' && tab.url) { // console.log('Tab updated:', tab.url); setActionPopup(tabId, tab.url); // Check if we're on a Discogs release page if (tab.url.includes('discogs.com/release/')) { // console.log('Discogs release page detected'); // Check if settings are configured before injecting chrome.storage.local.get(['discogsToken', 'googleSettings', 'wpSettings'], (result) => { // console.log('Checking settings for tab:', tabId); // console.log('Settings found:', { // hasDiscogsToken: !!result.discogsToken, // hasGoogleSettings: !!result.googleSettings, // hasWpSettings: !!result.wpSettings // }); const hasDiscogsToken = result.discogsToken && result.discogsToken.trim() !== ''; if (hasDiscogsToken) { // console.log('Settings configured, injecting content script'); chrome.scripting.executeScript({ target: { tabId: tabId }, files: ['content.js'] }).catch(err => { // console.error('Failed to inject content script:', err); }); } else { // console.log('Settings not configured, skipping content script injection'); } }); } } }); chrome.tabs.onActivated.addListener(activeInfo => { chrome.tabs.get(activeInfo.tabId, (tab) => { if(tab) { setActionPopup(tab.id, tab.url); } }); }); // Set for currently active tab when the extension is loaded chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (tabs[0]) { setActionPopup(tabs[0].id, tabs[0].url); } }); // Initialize YouTube Cookie functionality chrome.runtime.onInstalled.addListener(() => { // console.log('Extension installed'); keepAlive(); }); // Handle messages from content scripts and popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log('Background: Received message:', message.type); if (message.type === 'blagginate_ready') { // BLAGGINATE page is ready for communication if (sender.tab) { blagginateTabId = sender.tab.id; isBlagginateConnected = true; console.log('BLAGGINATE page ready, tab ID:', blagginateTabId); // Send initial cookies updateCookies(); sendResponse({ success: true }); } return true; } if (message.type === 'get_cookies') { // Request for fresh cookies from BLAGGINATE page chrome.storage.local.get(['youtubeCookies'], (result) => { if (result.youtubeCookies) { sendResponse({ success: true, cookies: result.youtubeCookies }); } else { // Get fresh cookies updateCookies().then(() => { chrome.storage.local.get(['youtubeCookies'], (freshResult) => { sendResponse({ success: true, cookies: freshResult.youtubeCookies || null }); }); }); } }); return true; // Keep message channel open for async response } if (message.type === 'force_cookie_update') { // Force update cookies (can be called from popup) updateCookies(); sendResponse({ success: true }); return true; } }); // Handle tab removal - clean up if BLAGGINATE tab is closed chrome.tabs.onRemoved.addListener((tabId) => { if (tabId === blagginateTabId) { console.log('BLAGGINATE tab closed'); isBlagginateConnected = false; blagginateTabId = null; } }); // External message listener for WordPress BLAGGINATE communication chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => { console.log('External message received:', request, 'from:', sender); // Handle WordPress BLAGGINATE cookie requests if (request.action === "getCookies") { console.log('WordPress requesting cookies via external message'); updateCookies() .then(result => { console.log('Cookies updated for WordPress:', result); sendResponse({ success: true, cookieText: result.cookieText, timestamp: Date.now() }); }) .catch(error => { console.error('Failed to get cookies for WordPress:', error); sendResponse({ success: false, error: error.message || 'Failed to get cookies' }); }); return true; // Keep message channel open for async response } // Handle Beatport token extraction requests if (request.action === "getBeatportToken") { fetchBeatportToken(sendResponse); return true; } // Handle WordPress ping/status requests if (request.action === "ping" || request.action === "status") { console.log('WordPress ping received'); sendResponse({ success: true, status: 'connected', timestamp: Date.now() }); return true; } // Get Discogs cookies for the batch researcher node script if (request.action === "getDiscogsCookies") { chrome.cookies.getAll({ url: "https://www.discogs.com" }, (cookies) => { const cookieHeader = cookies.map(c => `${c.name}=${c.value}`).join('; '); sendResponse({ success: true, cookieHeader }); }); return true; } // Get Discogs page HTML via a real Chrome tab (bypasses Cloudflare JS challenge) // Used by the batch researcher node script via the local proxy server if (request.action === "fetchDiscogsPage") { fetchPageViaTab(request.url) .then(html => sendResponse({ success: true, html })) .catch(err => sendResponse({ success: false, error: err.message })); return true; // async } // Handle unknown requests console.log('Unknown external message action:', request.action); sendResponse({ success: false, error: 'Unknown action: ' + request.action }); return true; }); // ─── Batch researcher page proxy ────────────────────────────────────────────── // Polls the node batch script's local proxy server (localhost:7789). // When a Discogs URL is queued, opens it in a real background Chrome tab so // Cloudflare's JS challenge can execute, then returns the HTML to the node script. let batchProxyTabId = null; function ensureBatchProxyTab() { return new Promise((resolve) => { if (batchProxyTabId !== null) { chrome.tabs.get(batchProxyTabId, (tab) => { if (chrome.runtime.lastError || !tab) { batchProxyTabId = null; chrome.tabs.create({ url: 'about:blank', active: false }, (t) => { batchProxyTabId = t.id; resolve(t.id); }); } else { resolve(batchProxyTabId); } }); } else { chrome.tabs.create({ url: 'about:blank', active: false }, (t) => { batchProxyTabId = t.id; resolve(t.id); }); } }); } function fetchPageViaTab(url) { return ensureBatchProxyTab().then(tabId => new Promise((resolve, reject) => { let done = false; const hardTimeout = setTimeout(() => { if (!done) { done = true; reject(new Error('Proxy tab timeout after 60s')); } }, 60000); function extractAfterLoad() { if (done) return; // Run in the tab: scroll to reviews, click Load More until gone, return outerHTML chrome.scripting.executeScript({ target: { tabId }, func: async () => { // Quick CF check first if (document.title === 'Just a moment...' || document.body?.innerHTML?.includes('__cf_chl_')) { return '__CF_BLOCKED__'; } // Scroll to the reviews section to trigger lazy loading const reviewSection = document.querySelector('#release-reviews'); if (reviewSection) { reviewSection.scrollIntoView({ behavior: 'instant' }); } else { window.scrollTo(0, document.body.scrollHeight); } // Wait for the loading spinner inside #release-reviews to disappear // Discogs uses svg[aria-label="loading"] while fetching reviews for (let i = 0; i < 40; i++) { await new Promise(r => setTimeout(r, 500)); const spinner = document.querySelector('#release-reviews svg[aria-label="loading"]'); if (!spinner) break; // reviews loaded (or section has no reviews) } // Click "Load More Reviews" button until gone for (let clicks = 0; clicks < 30; clicks++) { const btn = document.querySelector('#release-reviews button'); if (!btn) break; btn.click(); await new Promise(r => setTimeout(r, 1500)); // Wait for any new spinner to clear after clicking for (let i = 0; i < 10; i++) { await new Promise(r => setTimeout(r, 500)); if (!document.querySelector('#release-reviews svg[aria-label="loading"]')) break; } } return document.documentElement.outerHTML; } }, (results) => { if (done) return; const html = results?.[0]?.result || ''; if (html === '__CF_BLOCKED__') { if (!done) { setTimeout(() => extractAfterLoad(), 3000); } return; } const isCF = html.includes('Just a moment') || html.includes('__cf_chl_') || html.length < 500; if (isCF) { setTimeout(() => extractAfterLoad(), 3000); return; } done = true; clearTimeout(hardTimeout); resolve(html); }); } function onUpdated(updTabId, changeInfo) { if (updTabId !== tabId || changeInfo.status !== 'complete') return; chrome.tabs.onUpdated.removeListener(onUpdated); setTimeout(extractAfterLoad, 1500); } chrome.tabs.onUpdated.addListener(onUpdated); chrome.tabs.update(tabId, { url }); })); } /** * Run the Discogs DeferredReleaseData GraphQL query. * Uses a GET request to the correct endpoint with URL-encoded params. * Executed inside a real Discogs tab so cookies are automatically included. */ async function fetchDiscogsGraphQLViaTab(discogsId) { // Prefer an already-open Discogs tab so we don't need to wait for a page load const tabs = await new Promise(resolve => chrome.tabs.query({ url: '*://*.discogs.com/*' }, resolve)); let tabId = tabs && tabs.length > 0 ? tabs[0].id : null; let createdTab = false; if (!tabId) { const t = await new Promise(resolve => chrome.tabs.create({ url: 'https://www.discogs.com/', active: false }, resolve)); tabId = t.id; createdTab = true; await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Tab load timeout')), 20000); function onUpd(id, info) { if (id !== tabId || info.status !== 'complete') return; chrome.tabs.onUpdated.removeListener(onUpd); clearTimeout(timeout); resolve(); } chrome.tabs.onUpdated.addListener(onUpd); }); } try { const results = await new Promise((resolve, reject) => { chrome.scripting.executeScript({ target: { tabId }, func: async (id) => { try { const params = new URLSearchParams({ operationName: 'DeferredReleaseData', variables: JSON.stringify({ discogsId: id }), extensions: JSON.stringify({ persistedQuery: { version: 1, sha256Hash: 'd11719156ba242b7a0c478272025ba913c71a934c8597ef9cb51e30962a17fb4', }, }), }); const url = `https://www.discogs.com/service/catalog/api/graphql?${params}`; const resp = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json, */*', 'apollographql-client-name': 'release-page-client', }, }); return await resp.text(); } catch (e) { return JSON.stringify({ __fetchError: e.message }); } }, args: [discogsId], }, (r) => { if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message)); else resolve(r); }); }); const text = results && results[0] && results[0].result; if (!text) throw new Error('executeScript returned no result'); return text; } finally { if (createdTab) chrome.tabs.remove(tabId); } } async function batchProxyPoll() { try { // ── HTML page fetch (opens a real tab) ──────────────────────────────── const resp = await fetch('http://localhost:7789/pending-fetch', { signal: AbortSignal.timeout(3000) }); if (resp.status === 200) { const { requestId, url } = await resp.json(); let html = null, error = null; try { html = await fetchPageViaTab(url); } catch (e) { error = e.message; } await fetch('http://localhost:7789/fetch-result', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requestId, html, error }), }); } } catch { // Node server not running — normal when batch script isn't active } try { // ── GraphQL fetch (executed inside a Discogs tab — same-origin, cookies automatic) ─ const gresp = await fetch('http://localhost:7789/pending-graphql', { signal: AbortSignal.timeout(3000) }); if (gresp.status === 200) { const { requestId, discogsId } = await gresp.json(); let json = null, error = null; try { json = await fetchDiscogsGraphQLViaTab(Number(discogsId)); } catch (e) { error = e.message; } await fetch('http://localhost:7789/graphql-result', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requestId, json, error }), }); } } catch { // Normal when server not running } } // Poll every 2 seconds. Silently no-ops when the node server isn't running. setInterval(batchProxyPoll, 2000); // ─── Beatport token refresh poll (for beatport_fill.py) ─────────────────────── // Polls the ytanalyse cookie-server to see if Python has requested a fresh token. // When it has, fetches from beatport.com/api/auth/session and delivers it. async function beatportTokenPoll() { try { const resp = await fetch('http://localhost:7792/beatport-pending', { signal: AbortSignal.timeout(2000) }); if (resp.status !== 200) return; const { pending } = await resp.json(); if (!pending) return; console.log('Beatport token requested by Python — attempting live fetch...'); // Always try a live fetch first so we get the freshest token. // Falls back to whatever is in storage only if the live fetch returns 'session' // (meaning the Beatport browser session has expired). fetchBeatportToken((result) => { if (result && result.success && result.tokenJson) { console.log('Beatport: live fetch succeeded, delivering to cookie-server'); fetch('http://localhost:7792/beatport-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: result.tokenJson, }).catch(() => {}); return; } // Live fetch returned 'session' — Beatport browser session likely expired. // Fall back to storage, but only if the stored token is less than 55 minutes old. console.warn('Beatport: live fetch returned session/empty, checking storage...'); chrome.storage.local.get(['beatportAuth'], (stored) => { const auth = stored.beatportAuth; const ageMs = Date.now() - (auth?.timestamp || 0); const tokenJson = auth?.data; if (tokenJson && ageMs < 55 * 60 * 1000) { try { const parsed = JSON.parse(tokenJson); const tokenObj = parsed.token || parsed; const accessToken = tokenObj.accessToken || tokenObj.access_token; if (accessToken && accessToken !== 'session') { console.log('Beatport: delivering stored token (age: ' + Math.round(ageMs/60000) + 'min)'); fetch('http://localhost:7792/beatport-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: tokenJson, }).catch(() => {}); return; } } catch { /* invalid JSON */ } } // Nothing usable — POST an error so Python stops waiting immediately console.error('Beatport: no valid token available — browser session expired?'); fetch('http://localhost:7792/beatport-token-error', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'session_expired' }), }).catch(() => {}); }); }); } catch { // cookie-server not running — normal } } // Poll every 3 seconds. Silently no-ops when cookie-server isn't running. setInterval(beatportTokenPoll, 3000); // ─── MONSTERWIKI passive auto-capture ───────────────────────────────────────── // When "Auto-capture while browsing" is on, every Discogs release page you open // is harvested silently (DOM via content.js + 1 Discogs API call for the // canonical release object + sell-page scrape) and POSTed to the plice server. // No popup needed. const mcRecent = new Map(); // release_id → last-capture epoch ms const MC_DEDUP_MS = 10 * 60 * 1000; // ponytail: re-capture same release after 10min, not every scroll/SPA re-render async function mcGetReleaseApi(id, token) { for (let attempt = 0; attempt < 3; attempt++) { try { const r = await fetch(`https://api.discogs.com/releases/${id}`, { headers: { 'Authorization': `Discogs token=${token}`, 'User-Agent': 'PriceClogs/1.0' } }); if (r.status === 429) { await new Promise(s => setTimeout(s, 1500 * (attempt + 1))); continue; } const data = await r.json(); if (data && data.artists) return data; } catch (_) {} await new Promise(s => setTimeout(s, 1000)); } return null; } async function mcGetPriceSuggestions(id, token) { try { const r = await fetch(`https://api.discogs.com/marketplace/price_suggestions/${id}`, { headers: { 'Authorization': `Discogs token=${token}`, 'User-Agent': 'PriceClogs/1.0' } }); return r.ok ? await r.json() : null; } catch (_) { return null; } } function mcResolveBase(cfg) { if (cfg.preferLocalhost) return 'http://localhost:5002'; return (cfg.serverUrl || 'http://100.91.239.7:5002').replace(/\/$/, ''); } async function monsterAutoCapture(tabId, releaseId) { const cfg = await new Promise(res => chrome.storage.local.get(['monsterSettings'], r => res(r.monsterSettings || {}))); if (!cfg.autoCapture) return; const last = mcRecent.get(releaseId); if (last && (Date.now() - last) < MC_DEDUP_MS) return; mcRecent.set(releaseId, Date.now()); // mark early so concurrent loads don't double-fire const token = await new Promise(res => chrome.storage.local.get('discogsToken', d => res((d.discogsToken || '').trim() || null))); if (!token) { console.warn('[autocapture] no Discogs token — skipping', releaseId); mcRecent.delete(releaseId); return; } try { // DOM scrape (content.js already auto-injected on discogs.com) — gives // market stats, reviews, recommendations, apple id, cover image. const dom = await new Promise(res => { chrome.tabs.sendMessage(tabId, { action: 'getInitialValues' }, resp => { res(chrome.runtime.lastError ? {} : (resp || {})); }); }); const [api, priceSuggestions, histHtml, listHtml] = await Promise.all([ mcGetReleaseApi(releaseId, token), mcGetPriceSuggestions(releaseId, token), fetch(`https://www.discogs.com/sell/history/${releaseId}`, { credentials: 'include' }).then(r => r.ok ? r.text() : null).catch(() => null), fetch(`https://www.discogs.com/sell/list?release_id=${releaseId}`, { credentials: 'include' }).then(r => r.ok ? r.text() : null).catch(() => null), ]); if (!api) { console.warn('[autocapture] API release fetch failed', releaseId); mcRecent.delete(releaseId); return; } const rd = combineReleaseData(api, dom, priceSuggestions); const payload = { ...buildResearchJson(rd), sales_history: histHtml ? parseSalesHistoryHtml(histHtml) : null, current_listings: listHtml ? parseListingsHtml(listHtml) : null, }; const base = mcResolveBase(cfg); const resp = await fetch(`${base}/plice`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: AbortSignal.timeout(12000), }); if (resp.ok) console.log('[autocapture] ✓ release', releaseId, '→', base); else { console.warn('[autocapture] POST failed', resp.status, releaseId); mcRecent.delete(releaseId); } } catch (e) { console.warn('[autocapture] error', releaseId, e.message); mcRecent.delete(releaseId); // allow retry on next navigation } } chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status !== 'complete' || !tab.url) return; // Release pages only: /release/ or //release/, not master/sell. const m = tab.url.match(/discogs\.com\/(?:[^/]+\/)?release\/(\d+)/); if (m) monsterAutoCapture(tabId, m[1]); });