Snapshot of the working tree exactly as it stood, no edits. This is the predecessor
PRICEGOD was rewritten from ("kept intact, untouched" per pricegod/README.md), and it
is still the only place the DYMO scale is actually implemented —
rfid-daemon/index.js:1068-1265: HID discovery, parseScaleReport, one-shot read, and a
streaming /weight + /scale/start|stop session whose JSON shape PRICEGOD's daemon.js
already speaks.
Preserved verbatim on purpose (hence -og), including the known bug: DYMO_PIDS at
index.js:1082 is [0x8003, 0x8004], so it cannot see the bench M25 (0x8009). Fix that
in whatever daemon inherits the scale, not here.
node_modules stays ignored (22M of the 24M tree). No credentials in the import: the
two PEM markers in utils.js/sheets.js only strip headers off a key read from settings,
and mrpadmin / johnking are an SSH and a Postgres username, both key/trust auth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1099 lines
43 KiB
JavaScript
1099 lines
43 KiB
JavaScript
// 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/<id> or /<artist>/release/<id>, not master/sell.
|
|
const m = tab.url.match(/discogs\.com\/(?:[^/]+\/)?release\/(\d+)/);
|
|
if (m) monsterAutoCapture(tabId, m[1]);
|
|
});
|