// 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