pliceclogs-og/content.js
type-two e5b8504bc6 Import pliceclogs as-is — the original Discogs seller extension
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>
2026-07-24 13:47:09 +10:00

776 lines
35 KiB
JavaScript

// Only run on Discogs pages to avoid interfering with other sites
if (window.location.hostname.includes('discogs.com') && !window.hasRun) {
// console.log('PriceClogs: Initializing on Discogs page');
window.hasRun = true;
const SELECTORS = {
releaseTitle: 'h1.title_Brnd1',
collectionBox: 'div.box_PFmyl.collection_DQxgF',
statsBox: '#release-stats',
appleMusic: '#audio-iframe',
marketStats: {
artistElement: 'h1.title_Brnd1 a',
labelElement: ".info_LD8Ql a[href*='/label/']",
imageElement: '.thumbnail_cgf1w img'
}
};
async function waitForElement(selector, timeout = 7000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
return resolve(element);
}
const observer = new MutationObserver((mutations, obs) => {
const element = document.querySelector(selector);
if (element) {
obs.disconnect();
resolve(element);
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
reject(new Error(`Timeout waiting for element: ${selector}`));
}, timeout);
});
}
function extractReleaseId() {
try {
const href = window.location.href;
const match = href.match(/\/release\/(\d+)/);
if (match) return match[1];
const sellMatch = href.match(/\/sell\/release\/(\d+)/);
if (sellMatch) return sellMatch[1];
const link = document.querySelector('a[href*="/release/"]');
if (link) {
const lm = link.href.match(/\/release\/(\d+)/);
if (lm) return lm[1];
}
const pathNum = (window.location.pathname.match(/\d{5,}/) || [])[0];
if (pathNum) return pathNum;
} catch (e) {}
return null;
}
async function expandAllReviews() {
console.log('[DEBUG] Starting to expand all reviews...');
// First, scroll to bottom to trigger review loading
console.log('[DEBUG] Scrolling to bottom to load reviews...');
const originalScroll = window.scrollY;
window.scrollTo(0, document.body.scrollHeight);
await new Promise(resolve => setTimeout(resolve, 1000));
// Click "Load More" buttons until no more exist
while (true) {
const loadMoreButton = document.querySelector('#release-reviews > div > button');
if (!loadMoreButton) {
console.log('[DEBUG] No more "Load More" buttons found');
break;
}
try {
loadMoreButton.click();
console.log('[DEBUG] Clicked "Load More Reviews"');
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error('[DEBUG] Error clicking load more:', error);
break;
}
}
// Then expand all "See Replies" buttons
let keepChecking = true;
while (keepChecking) {
const seeRepliesButtons = document.querySelectorAll('button.button_PgYDF.link_ijVx7.button_YXKHZ');
keepChecking = false;
for (const button of seeRepliesButtons) {
if (button.textContent.includes('See')) {
try {
button.click();
console.log('[DEBUG] Clicked "See Replies"');
keepChecking = true;
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
console.error('[DEBUG] Error clicking see replies:', error);
}
}
}
if (keepChecking) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Restore original scroll position
window.scrollTo(0, originalScroll);
console.log('[DEBUG] Finished expanding all reviews');
}
function extractHelpfulCount(reviewEl) {
const helpfulButton = Array.from(reviewEl.querySelectorAll('.button_PgYDF.link_ijVx7.button_YXKHZ'))
.find(button => button.textContent.includes('Helpful'));
const helpfulText = helpfulButton?.textContent?.trim() || '';
return helpfulText === 'Helpful' ? 0 : parseInt(helpfulText) || 0;
}
function extractReviews() {
console.log('[DEBUG] Starting review extraction...');
const reviews = [];
// Get all top-level review elements (those not in replies containers)
const reviewElements = document.querySelectorAll('.review_luKwE:not(.replies_r6FWL .review_luKwE)');
console.log(`[DEBUG] Found ${reviewElements.length} top-level review elements`);
reviewElements.forEach(reviewEl => {
// Extract the main review data
const review = {
id: reviewEl.id?.replace('#', '') || '',
username: reviewEl.querySelector('.username_N7O6q')?.innerText?.trim() || '',
profileUrl: reviewEl.querySelector('.username_N7O6q')?.href || '',
avatarUrl: reviewEl.querySelector('.pic_Z6wyD img')?.src || '',
date: reviewEl.querySelector('time.created_EKCAa')?.getAttribute('datetime') || '',
rating: extractRating(reviewEl),
text: reviewEl.querySelector('.markup_Cngxi')?.innerText?.trim() || '',
helpfulCount: extractHelpfulCount(reviewEl),
replies: []
};
// Find replies container that follows this review
const repliesContainer = reviewEl.nextElementSibling;
if (repliesContainer?.classList.contains('replies_r6FWL')) {
// Get all reply elements within this container
const replyElements = repliesContainer.querySelectorAll('.review_luKwE');
replyElements.forEach(replyEl => {
const reply = {
id: replyEl.id?.replace('#', '') || '',
username: replyEl.querySelector('.username_N7O6q')?.innerText?.trim() || '',
profileUrl: replyEl.querySelector('.username_N7O6q')?.href || '',
avatarUrl: replyEl.querySelector('.pic_Z6wyD img')?.src || '',
date: replyEl.querySelector('time.created_EKCAa')?.getAttribute('datetime') || '',
rating: extractRating(replyEl),
text: replyEl.querySelector('.markup_Cngxi')?.innerText?.trim() || '',
helpfulCount: extractHelpfulCount(replyEl),
parentId: review.id
};
review.replies.push(reply);
});
}
reviews.push(review);
});
// Sort top-level reviews by date (newest first)
reviews.sort((a, b) => new Date(b.date) - new Date(a.date));
// Debug logging
console.log(`[DEBUG] Extracted reviews: ${reviews.length}`);
reviews.forEach(review => {
console.log(`[DEBUG] Review ${review.id} has ${review.replies.length} replies`);
});
return reviews;
}
function extractRating(reviewEl) {
const ratingLabel = reviewEl.querySelector('.rating_pea6E')?.getAttribute('aria-label') || '';
const match = ratingLabel.match(/rated this release (\d+) star/);
return match ? parseInt(match[1]) : 0;
}
function extractRecommendations() {
console.log('[DEBUG] Starting recommendation extraction...');
const recommendations = [];
const items = document.querySelectorAll("#release-recommendations > div > div > ul > li");
console.log(`[DEBUG] Found ${items.length} recommendation elements`);
items.forEach(item => {
const recommendation = {
title: item.querySelector(".title_XMp5_")?.innerText?.trim() || '',
artist: item.querySelector("._artistName_1xmn1_9")?.innerText?.trim() || '',
releaseLink: item.querySelector("._link_bcmpa_1._hideUnderLine_bcmpa_25")?.href || '',
coverImage: item.querySelector(".thumbnailContainer_pYnh1 img")?.src || '',
releaseDetails: item.querySelector(".dateAndCountry_Yllip")?.innerText?.trim() || '',
format: {
primary: item.querySelector(".primaryFormat_Fy_7v")?.innerText?.trim() || '',
details: item.querySelector("._concatenatedText_14c0h_1")?.innerText?.trim() || ''
}
};
if (recommendation.title || recommendation.artist) {
recommendations.push(recommendation);
}
});
console.log(`[DEBUG] Extracted ${recommendations.length} recommendations`);
return recommendations;
}
async function extractMarketData() {
console.log('[DEBUG] extractMarketData called.');
const data = { artistId: 'N/A', labelId: 'N/A', imageUrl: 'N/A', lastSold: 'N/A', lowPrice: 'N/A', medianPrice: 'N/A', highPrice: 'N/A', have: 'N/A', want: 'N/A', sellerPriceRange: '' };
try {
const statsContainer = await waitForElement(SELECTORS.statsBox);
console.log('[DEBUG] #release-stats container found.');
const statItems = statsContainer.querySelectorAll('.items_PQSxS li');
statItems.forEach(item => {
const nameEl = item.querySelector('.name_qjn4_');
if (!nameEl) return;
const name = nameEl.textContent.replace(/<!--.*-->|:/g, '').trim();
const valueEl = item.querySelector('a, span:not(.name_qjn4_)');
if (!valueEl) return;
const valueText = valueEl.textContent.trim();
if (name.includes('Have')) data.have = valueText;
else if (name.includes('Want')) data.want = valueText;
else if (name.includes('Last Sold')) data.lastSold = valueEl.querySelector('time')?.getAttribute('datetime') || 'N/A';
else if (name.includes('Low')) data.lowPrice = valueText.replace(/^[A-Z$£€¥₹]+/, '');
else if (name.includes('Median')) data.medianPrice = valueText.replace(/^[A-Z$£€¥₹]+/, '');
else if (name.includes('High')) data.highPrice = valueText.replace(/^[A-Z$£€¥₹]+/, '');
});
const artistElement = document.querySelector(SELECTORS.marketStats.artistElement);
if (artistElement) data.artistId = artistElement.getAttribute('href').match(/\/artist\/(\d+)/)?.[1] || 'N/A';
const labelElement = document.querySelector(SELECTORS.marketStats.labelElement);
if (labelElement) data.labelId = labelElement.getAttribute('href').match(/\/label\/(\d+)/)?.[1] || 'N/A';
const imageElement = document.querySelector(SELECTORS.marketStats.imageElement);
if (imageElement) data.imageUrl = imageElement.src;
// Extract seller price range from shopping box
const shoppingPriceEl = document.querySelector('.shopping-box-price .shopping-box-copy');
if (shoppingPriceEl) {
const rangeMatch = shoppingPriceEl.textContent.trim().match(/From\s+[A-Z]*\$?([\d,.]+)\s+to\s+[A-Z]*\$?([\d,.]+)/i);
if (rangeMatch) {
data.sellerPriceRange = `$${rangeMatch[1]}-$${rangeMatch[2]}`;
}
}
} catch (error) {
console.error('[DEBUG] CRITICAL ERROR in extractMarketData:', error);
}
console.log('[DEBUG] Extracted market data object:', data);
return data;
}
function getCollectionData(collectionBoxElement) {
console.log('[DEBUG] getCollectionData called.');
const collectionData = { mediaCondition: '', sleeveCondition: '', price: 'N/A', comment: 'N/A', collectionFolder: 'N/A' };
if (!collectionBoxElement) {
console.log('[DEBUG] Collection box element was NOT found.');
return collectionData;
}
console.log('[DEBUG] Collection box element found. Extracting data...');
const fields = collectionBoxElement.querySelectorAll('.field_nm6Jt');
fields.forEach(field => {
const labelEl = field.querySelector('label.label_KxNWU');
if (!labelEl) return;
const label = labelEl.textContent.trim();
let value = field.querySelector('.dvalue_fevTQ, .markup_Cngxi')?.textContent.trim() || '';
if (label.includes('Media Condition')) collectionData.mediaCondition = value;
else if (label.includes('Sleeve Condition')) collectionData.sleeveCondition = value;
else if (label.includes('comment')) collectionData.comment = value || 'N/A';
else if (label.includes('price')) collectionData.price = value || 'N/A';
else if (label.includes('Folder')) collectionData.collectionFolder = value || 'N/A';
else if (label === 'SKU') collectionData.collectionSku = value || '';
});
console.log('[DEBUG] Extracted collection data:', collectionData);
return collectionData;
}
async function getInitialValues() {
console.log('[DEBUG] getInitialValues called.');
// Check if we should run extraction based on settings
const shouldExtract = await checkIfShouldExtract();
if (!shouldExtract) {
console.log('[DEBUG] Settings not configured, returning minimal data');
return {
mediaCondition: 'Very Good Plus (VG+)',
sleeveCondition: 'Very Good Plus (VG+)',
price: '',
comment: '',
collectionFolder: '',
reviews: [],
recommendations: []
};
}
try {
await waitForElement(SELECTORS.releaseTitle);
console.log('[DEBUG] Sanity check passed: Release title found.');
} catch (error) {
console.error('[DEBUG] SANITY CHECK FAILED: Could not find release title. Aborting.', error);
return {};
}
let collectionBoxElement;
try {
// Wait for at least one collection box to appear
await waitForElement(SELECTORS.collectionBox, 3000);
// Find all collection boxes and select the last one
const collectionBoxes = document.querySelectorAll(SELECTORS.collectionBox);
if (collectionBoxes.length > 0) {
collectionBoxElement = collectionBoxes[collectionBoxes.length - 1];
console.log(`[DEBUG] Found ${collectionBoxes.length} collection box(es), using the last one.`);
} else {
collectionBoxElement = null;
}
} catch (error) {
console.log('[DEBUG] "In Collection" box not found on this page.');
collectionBoxElement = null;
}
const values = getCollectionData(collectionBoxElement);
// Expose multi-box info so the popup can render a copy selector
const allBoxes = document.querySelectorAll(SELECTORS.collectionBox);
values.collectionBoxCount = allBoxes.length;
values.collectionBoxSkus = Array.from(allBoxes).map(b => getCollectionData(b).collectionSku || '');
values.collectionBoxIndex = allBoxes.length - 1;
const marketData = await extractMarketData();
Object.assign(values, marketData);
// Extract reviews with nested structure
await expandAllReviews();
values.reviews = extractReviews();
// Extract recommendations
values.recommendations = extractRecommendations();
const iframe = document.querySelector(SELECTORS.appleMusic);
if (iframe && iframe.src) {
// URL shape: https://embed.music.apple.com/{country}/album/{slug?}/{id}
const match = iframe.src.match(/\/([a-z]{2})\/album\/(?:[^/?]+\/)?(\d+)/);
values.appleId = match ? match[2] : null;
values.appleCountry = match ? match[1] : null;
}
console.log('[DEBUG] FINAL EXTRACTED DATA with nested reviews and recommendations:', values);
return values;
}
// Function to check if extraction should run based on settings
async function checkIfShouldExtract() {
return new Promise((resolve) => {
chrome.storage.local.get(['discogsToken', 'googleSettings', 'wpSettings'], (result) => {
const hasDiscogsToken = result.discogsToken && result.discogsToken.trim() !== '';
const hasGoogleSettings = result.googleSettings &&
result.googleSettings.spreadsheetId &&
result.googleSettings.clientEmail &&
result.googleSettings.privateKey;
const hasWpSettings = result.wpSettings &&
result.wpSettings.url &&
result.wpSettings.username &&
result.wpSettings.password;
// At least one service should be configured
const hasValidSettings = hasDiscogsToken && (hasGoogleSettings || hasWpSettings);
console.log('[DEBUG] Content script settings check:', { hasDiscogsToken, hasGoogleSettings, hasWpSettings, hasValidSettings });
resolve(hasValidSettings);
});
});
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
try {
if (request.action === 'getInitialValues') {
getInitialValues().then(sendResponse).catch(error => sendResponse({ error: error.message }));
return true;
}
if (request.action === 'getMarketData') {
extractMarketData().then(sendResponse).catch(error => sendResponse({ error: error.message }));
return true;
}
if (request.action === 'selectCollectionBox') {
const boxes = document.querySelectorAll(SELECTORS.collectionBox);
const box = boxes[request.index];
if (box) sendResponse({ ok: true, ...getCollectionData(box) });
else sendResponse({ ok: false, error: 'Box not found' });
return true;
}
if (request.action === 'getReviews') {
expandAllReviews().then(() => {
const reviews = extractReviews();
sendResponse({ reviews });
}).catch(error => {
// console.error('Error extracting reviews:', error);
sendResponse({ error: error.message });
});
return true;
}
} catch (error) {
// console.error('Error processing message:', error);
sendResponse({ error: error.message });
}
return true;
});
} else {
// console.log('PriceClogs: Skipping initialization - not on Discogs page');
}
// Global, synchronous helpers used by the bottom message listener
function extractReleaseId() {
try {
const href = window.location.href;
const match = href.match(/\/release\/(\d+)/);
if (match) return match[1];
const sellMatch = href.match(/\/sell\/release\/(\d+)/);
if (sellMatch) return sellMatch[1];
const link = document.querySelector('a[href*="/release/"]');
if (link) {
const lm = link.href.match(/\/release\/(\d+)/);
if (lm) return lm[1];
}
const pathNum = (window.location.pathname.match(/\d{5,}/) || [])[0];
if (pathNum) return pathNum;
} catch (e) {}
return null;
}
function extractInitialValues() {
const values = {
mediaCondition: '',
sleeveCondition: '',
price: '',
comment: '',
collectionFolder: '',
artistId: 'N/A',
labelId: 'N/A',
imageUrl: 'N/A',
lastSold: 'N/A',
lowPrice: 'N/A',
medianPrice: 'N/A',
highPrice: 'N/A',
have: 'N/A',
want: 'N/A',
sellerPriceRange: '',
appleId: null,
reviews: [],
recommendations: []
};
try {
// Collection box values (last one if multiple)
const collectionBoxes = document.querySelectorAll('div.box_PFmyl.collection_DQxgF');
const collectionBoxElement = collectionBoxes.length ? collectionBoxes[collectionBoxes.length - 1] : null;
if (collectionBoxElement) {
const fields = collectionBoxElement.querySelectorAll('.field_nm6Jt');
fields.forEach(field => {
const labelEl = field.querySelector('label.label_KxNWU');
if (!labelEl) return;
const label = labelEl.textContent.trim();
let value = field.querySelector('.dvalue_fevTQ, .markup_Cngxi')?.textContent.trim() || '';
if (label.includes('Media Condition')) values.mediaCondition = value;
else if (label.includes('Sleeve Condition')) values.sleeveCondition = value;
else if (label.toLowerCase().includes('comment')) values.comment = value;
else if (label.toLowerCase().includes('price')) values.price = value;
else if (label.includes('Folder')) values.collectionFolder = value;
else if (label === 'SKU') values.collectionSku = value || '';
});
}
// Market stats and related IDs
const statsContainer = document.querySelector('#release-stats');
if (statsContainer) {
const statItems = statsContainer.querySelectorAll('.items_PQSxS li');
statItems.forEach(item => {
const nameEl = item.querySelector('.name_qjn4_');
if (!nameEl) return;
const name = nameEl.textContent.replace(/<!--.*-->|:/g, '').trim();
const valueEl = item.querySelector('a, span:not(.name_qjn4_)');
if (!valueEl) return;
const valueText = valueEl.textContent.trim();
if (name.includes('Have')) values.have = valueText;
else if (name.includes('Want')) values.want = valueText;
else if (name.includes('Last Sold')) values.lastSold = valueEl.querySelector('time')?.getAttribute('datetime') || 'N/A';
else if (name.includes('Low')) values.lowPrice = valueText.replace(/^[A-Z$£€¥₹]+/, '');
else if (name.includes('Median')) values.medianPrice = valueText.replace(/^[A-Z$£€¥₹]+/, '');
else if (name.includes('High')) values.highPrice = valueText.replace(/^[A-Z$£€¥₹]+/, '');
});
const artistEl = document.querySelector('h1.title_Brnd1 a');
if (artistEl) values.artistId = artistEl.getAttribute('href').match(/\/artist\/(\d+)/)?.[1] || 'N/A';
const labelEl = document.querySelector(".info_LD8Ql a[href*='/label/']");
if (labelEl) values.labelId = labelEl.getAttribute('href').match(/\/label\/(\d+)/)?.[1] || 'N/A';
const imageEl = document.querySelector('.thumbnail_cgf1w img');
if (imageEl) values.imageUrl = imageEl.src;
}
// Seller price range from shopping box
const shoppingPriceEl = document.querySelector('.shopping-box-price .shopping-box-copy');
if (shoppingPriceEl) {
const rangeMatch = shoppingPriceEl.textContent.trim().match(/From\s+[A-Z]*\$?([\d,.]+)\s+to\s+[A-Z]*\$?([\d,.]+)/i);
if (rangeMatch) {
values.sellerPriceRange = `$${rangeMatch[1]}-$${rangeMatch[2]}`;
}
}
// Apple Music album id
const iframe = document.querySelector('#audio-iframe');
if (iframe && iframe.src) {
const match = iframe.src.match(/album\/(\d+)/);
values.appleId = match ? match[1] : null;
}
} catch (e) {
// fail silently; return whatever we collected
}
return values;
}
// Function to update the Discogs price field on the page
async function updateDiscogsPrice(priceValue) {
console.log('[DEBUG] updateDiscogsPrice called with:', priceValue);
// Find ALL price labels on the page and take the last one (= newest collection box)
const allPriceLabels = Array.from(document.querySelectorAll('.label_KxNWU'))
.filter(el => el.textContent.toLowerCase().trim() === 'price');
console.log('[DEBUG] Found', allPriceLabels.length, 'price label(s) on page');
if (!allPriceLabels.length) {
console.error('[DEBUG] Price label not found');
return { success: false, error: 'Price label not found' };
}
const priceLabel = allPriceLabels[allPriceLabels.length - 1]; // last = newest collection box
console.log('[DEBUG] Found price label (last one)');
// Get the parent field container
const priceField = priceLabel.closest('.field_nm6Jt');
if (!priceField) {
console.error('[DEBUG] Price field container not found');
return { success: false, error: 'Price field container not found' };
}
console.log('[DEBUG] Found price field container, innerHTML:', priceField.innerHTML.substring(0, 200));
// Check if there's already an input/textarea visible (edit mode already active)
let input = priceField.querySelector('textarea.textarea_pO4kR, textarea, input[type="text"]');
console.log('[DEBUG] Initial input search result:', input);
if (!input) {
// Click the button to enter edit mode - try multiple approaches
const editButton = priceField.querySelector('button.wrapper_F6O4e, button[aria-label="Edit Notes"]');
if (!editButton) {
console.error('[DEBUG] Edit button not found');
return { success: false, error: 'Edit button not found' };
}
console.log('[DEBUG] Found edit button, clicking...');
// Try multiple click methods
editButton.focus();
editButton.click();
// Also try dispatching mouse events
editButton.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
editButton.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
editButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
console.log('[DEBUG] Clicked edit button, waiting for textarea...');
// Wait for the textarea to appear with polling
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
input = priceField.querySelector('textarea.textarea_pO4kR, textarea, input[type="text"]');
if (input) {
console.log('[DEBUG] Found input after', (i + 1) * 100, 'ms');
break;
}
}
}
if (!input) {
console.error('[DEBUG] Input/textarea not found after clicking edit. Current field HTML:', priceField.innerHTML);
return { success: false, error: 'Input field not found after clicking edit' };
}
console.log('[DEBUG] Found input field:', input.tagName, input.className);
// Focus the input first
input.focus();
await new Promise(resolve => setTimeout(resolve, 50));
// Clear and set the value using native setter for React compatibility
const isTextarea = input.tagName.toLowerCase() === 'textarea';
const nativeSetter = Object.getOwnPropertyDescriptor(
isTextarea ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
'value'
)?.set;
// Clear first
if (nativeSetter) {
nativeSetter.call(input, '');
}
input.dispatchEvent(new Event('input', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 50));
// Now set the value
if (nativeSetter) {
nativeSetter.call(input, priceValue);
} else {
input.value = priceValue;
}
// Dispatch events to ensure React picks up the change
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
// Simulate typing by dispatching input event with data
input.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: priceValue
}));
console.log('[DEBUG] Set price value to:', priceValue, 'Current input value:', input.value);
// Wait a moment for React to process the input
await new Promise(resolve => setTimeout(resolve, 200));
// Find and click the save button
const saveButton = priceField.querySelector('button.save_q8lO5, button.green_DL05T, button:has(.save), button[class*="save"]');
if (saveButton) {
console.log('[DEBUG] Found save button, clicking...');
saveButton.click();
saveButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
console.log('[DEBUG] Clicked save button');
} else {
console.log('[DEBUG] Save button not found, trying blur instead');
input.blur();
document.body.click();
}
return { success: true };
}
// Function to update the Discogs SKU field on the page
async function updateDiscogsSkuField(skuValue, boxIndex) {
console.log('[DEBUG] updateDiscogsSkuField called with:', skuValue, 'boxIndex:', boxIndex);
// Target the specific collection box (by index) or fall back to the last one
const allBoxes = document.querySelectorAll('div.box_PFmyl.collection_DQxgF');
const targetBox = (boxIndex != null && allBoxes[boxIndex]) ? allBoxes[boxIndex] : allBoxes[allBoxes.length - 1];
const allSkuLabels = targetBox
? Array.from(targetBox.querySelectorAll('.label_KxNWU')).filter(el => el.textContent.trim() === 'SKU')
: Array.from(document.querySelectorAll('.label_KxNWU')).filter(el => el.textContent.trim() === 'SKU');
console.log('[DEBUG] Found', allSkuLabels.length, 'SKU label(s) in target box');
if (!allSkuLabels.length) {
return { success: false, error: 'SKU label not found' };
}
const skuLabel = allSkuLabels[0]; // first (only) SKU field in this box
const skuField = skuLabel.closest('.field_nm6Jt');
if (!skuField) {
return { success: false, error: 'SKU field container not found' };
}
let input = skuField.querySelector('textarea.textarea_pO4kR, textarea, input[type="text"]');
if (!input) {
const editButton = skuField.querySelector('button.wrapper_F6O4e, button[aria-label="Edit Notes"]');
if (!editButton) {
return { success: false, error: 'Edit button not found' };
}
editButton.focus();
editButton.click();
editButton.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
editButton.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window }));
editButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
input = skuField.querySelector('textarea.textarea_pO4kR, textarea, input[type="text"]');
if (input) break;
}
}
if (!input) {
return { success: false, error: 'Input field not found after clicking edit' };
}
input.focus();
await new Promise(resolve => setTimeout(resolve, 50));
const isTextarea = input.tagName.toLowerCase() === 'textarea';
const nativeSetter = Object.getOwnPropertyDescriptor(
isTextarea ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype,
'value'
)?.set;
if (nativeSetter) {
nativeSetter.call(input, '');
}
input.dispatchEvent(new Event('input', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 50));
if (nativeSetter) {
nativeSetter.call(input, String(skuValue));
} else {
input.value = String(skuValue);
}
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: String(skuValue)
}));
await new Promise(resolve => setTimeout(resolve, 200));
const saveButton = skuField.querySelector('button.save_q8lO5, button.green_DL05T, button:has(.save), button[class*="save"]');
if (saveButton) {
saveButton.click();
saveButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
} else {
input.blur();
document.body.click();
}
return { success: true };
}
// Guard: only register this listener once per page context
// (executeScript re-injects the file each time the popup opens,
// without the guard we'd accumulate duplicate listeners)
if (!window._discogsTagListenerAdded) {
window._discogsTagListenerAdded = true;
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// console.log('CONTENT: Received message:', request);
// NOTE: getInitialValues is handled by the async listener registered earlier
// (inside the window.hasRun block) which properly calls expandAllReviews()
// and extractReviews(). Do NOT handle it here — this sync handler would
// respond first and return empty reviews, stomping the async result.
if (request.action === 'updateDiscogsPrice') {
updateDiscogsPrice(request.price).then(result => {
console.log('[DEBUG] updateDiscogsPrice result:', result);
sendResponse(result);
}).catch(error => {
console.error('[DEBUG] Error in updateDiscogsPrice:', error);
sendResponse({ success: false, error: error.message });
});
return true; // Will respond asynchronously
}
if (request.action === 'updateDiscogsSku') {
updateDiscogsSkuField(request.sku, request.boxIndex).then(result => {
console.log('[DEBUG] updateDiscogsSkuField result:', result);
sendResponse(result);
}).catch(error => {
console.error('[DEBUG] Error in updateDiscogsSkuField:', error);
sendResponse({ success: false, error: error.message });
});
return true;
}
return true; // Keep the message channel open for async response
}); // end onMessage listener
} // end _discogsTagListenerAdded guard