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>
776 lines
35 KiB
JavaScript
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
|