// 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]);
});