pliceclogs-og/monster_master.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

232 lines
12 KiB
JavaScript

/**
* MONSTERWIKI — master page content script
* Fires on discogs.com/master/* — scrapes reviews, POSTs to ultra.
*/
(async () => {
const { monsterSettings } = await chrome.storage.local.get(['monsterSettings']);
if (!monsterSettings?.enabled) return;
const masterIdMatch = window.location.pathname.match(/^\/master\/(\d+)/);
if (!masterIdMatch) return;
const masterId = parseInt(masterIdMatch[1]);
// Notify popup we're on a master page and starting
chrome.runtime.sendMessage({ action: 'monsterMasterStatus', state: 'syncing', masterId });
// ── 1. Try __NEXT_DATA__ first (fast, structured) ──────────────────────
let reviews = [];
const nextDataEl = document.getElementById('__NEXT_DATA__');
if (nextDataEl) {
try {
const data = JSON.parse(nextDataEl.textContent);
const pp = data?.props?.pageProps;
const raw = pp?.reviews?.items
|| (Array.isArray(pp?.reviews) ? pp.reviews : null)
|| pp?.master?.reviews?.items
|| pp?.initialState?.reviews?.items
|| [];
reviews = raw.map(r => ({
username: r.user?.username || r.username || null,
date: (r.submitted_at || r.submittedAt || r.date || '').split('T')[0] || null,
rating: r.rating ?? null,
text: (r.body || r.text || '').trim() || null,
helpful: r.helpful_count || r.helpfulCount || 0,
version_ref: r.version?.description || r.versionDescription || null,
version_id: r.version?.id || r.versionId || null,
replies: (r.replies || []).map(rep => ({
username: rep.user?.username || rep.username || null,
date: (rep.submitted_at || rep.date || '').split('T')[0] || null,
text: (rep.body || rep.text || '').trim() || null,
})),
})).filter(r => r.text || r.rating);
} catch (e) {
console.warn('[MONSTER] __NEXT_DATA__ parse failed:', e.message);
}
}
async function expandAllMasterReviews() {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const isVisible = (el) => {
if (!el) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const clickAll = async (matcher, maxRounds = 12) => {
for (let round = 0; round < maxRounds; round++) {
const els = [...document.querySelectorAll('button, a, div[role="button"]')].filter(isVisible);
const targets = els.filter(matcher);
if (!targets.length) return;
for (const el of targets) {
try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch {}
try { el.click(); } catch {}
await sleep(250);
}
await sleep(500);
}
};
for (let i = 0; i < 6; i++) {
window.scrollTo(0, document.body.scrollHeight);
await sleep(700);
}
await clickAll((el) => {
const t = (el.textContent || '').trim().toLowerCase();
const label = (el.getAttribute('aria-label') || '').trim().toLowerCase();
if (!t && !label) return false;
const s = `${t} ${label}`;
return s.includes('load more') || s.includes('more reviews') || s.includes('show more');
});
await clickAll((el) => {
const t = (el.textContent || '').trim();
return /^see\s+\d+\s+repl/i.test(t);
});
}
function scrapeDomReviewsWithReplies() {
const out = [];
const extractRating = (el) => {
const ratingLabel = el.querySelector('[class*="rating"], [aria-label*="rated"]')?.getAttribute?.('aria-label') || '';
const match = ratingLabel.match(/rated.*?(\d+)\s*star/i);
if (match) return parseInt(match[1]);
const stars = el.querySelectorAll('[class*="star"][class*="filled"], [class*="starFilled"]');
return stars.length || null;
};
const extractHelpful = (el) => {
const txt = [...el.querySelectorAll('button, a')]
.map(b => b.textContent || '')
.find(t => t.toLowerCase().includes('helpful')) || '';
const n = txt.match(/\d+/)?.[0];
return n ? parseInt(n) : 0;
};
// Discogs release-style markup (often reused)
const reviewEls = document.querySelectorAll('.review_luKwE:not(.replies_r6FWL .review_luKwE)');
if (reviewEls.length) {
reviewEls.forEach(reviewEl => {
const username = reviewEl.querySelector('.username_N7O6q')?.innerText?.trim() || null;
const date = reviewEl.querySelector('time')?.getAttribute('datetime')?.split('T')[0]
|| reviewEl.querySelector('time')?.textContent?.trim()
|| null;
const text = reviewEl.querySelector('.markup_Cngxi')?.innerText?.trim() || null;
const rating = extractRating(reviewEl);
const helpful = extractHelpful(reviewEl);
const replies = [];
const repliesContainer = reviewEl.nextElementSibling;
if (repliesContainer && repliesContainer.classList.contains('replies_r6FWL')) {
const replyEls = repliesContainer.querySelectorAll('.review_luKwE');
replyEls.forEach(replyEl => {
const ru = replyEl.querySelector('.username_N7O6q')?.innerText?.trim() || null;
const rd = replyEl.querySelector('time')?.getAttribute('datetime')?.split('T')[0]
|| replyEl.querySelector('time')?.textContent?.trim()
|| null;
const rt = replyEl.querySelector('.markup_Cngxi')?.innerText?.trim() || null;
if (ru && rt) replies.push({ username: ru, date: rd, text: rt });
});
}
if (username && (text || rating)) out.push({ username, date, rating, text, helpful, version_ref: null, version_id: null, replies });
});
}
return out;
}
// ── 2. DOM scrape fallback (reviews loaded client-side) ────────────────
// Note: expandAllMasterReviews() is NOT called here — it scrolls and clicks the page
// automatically which interferes with normal browsing. It only runs when triggered
// explicitly from the popup.
if (!reviews.length || reviews.every(r => !r.replies || !r.replies.length)) {
const domWithReplies = scrapeDomReviewsWithReplies();
if (domWithReplies.length) {
reviews = domWithReplies;
} else if (!reviews.length) {
document.querySelectorAll('[class*="review_card"], [class*="reviewCard"], .review').forEach(card => {
const username = card.querySelector('[class*="username"], [data-username]')?.textContent?.trim()
|| card.getAttribute('data-username') || null;
const dateEl = card.querySelector('time');
const date = dateEl?.getAttribute('datetime')?.split('T')[0]
|| dateEl?.textContent?.trim() || null;
const text = card.querySelector('[class*="review_body"], [class*="reviewBody"], p')
?.textContent?.trim() || null;
const ratingEl = card.querySelectorAll('[class*="star"][class*="filled"], [class*="starFilled"]');
const rating = ratingEl.length || null;
const versionEl = [...card.querySelectorAll('a, p')]
.find(el => el.textContent.includes('referencing'));
const version_ref = versionEl
? versionEl.textContent.replace(/^.*referencing\s*/i, '').trim() || null
: null;
const helpful = parseInt(card.querySelector('[class*="helpful"]')
?.textContent?.match(/\d+/)?.[0]) || 0;
const replies = [];
const replyBlocks = card.querySelectorAll('[class*="reply"], [class*="Reply"]');
replyBlocks.forEach(rep => {
const ru = rep.querySelector('[class*="username"], [data-username]')?.textContent?.trim() || null;
const rd = rep.querySelector('time')?.getAttribute('datetime')?.split('T')[0]
|| rep.querySelector('time')?.textContent?.trim()
|| null;
const rt = rep.querySelector('p, [class*="body"], [class*="markup"]')?.textContent?.trim() || null;
if (ru && rt) replies.push({ username: ru, date: rd, text: rt });
});
if (username && (text || rating)) {
reviews.push({ username, date, rating, text, helpful, version_ref, version_id: null, replies });
}
});
}
}
// ── Master metadata ────────────────────────────────────────────────────
const titleEl = document.querySelector('h1[class*="title"], h1');
const artistEl = document.querySelector('[class*="artist"] a, h2 a');
const title = titleEl?.textContent?.trim() || null;
const artist = artistEl?.textContent?.trim() || null;
console.log(`[MONSTER] master/${masterId}${reviews.length} reviews found`);
if (!reviews.length) {
chrome.runtime.sendMessage({ action: 'monsterMasterStatus', state: 'no_reviews', masterId });
return;
}
// ── Resolve server URL ─────────────────────────────────────────────────
const base = await (async () => {
try {
const r = await fetch('http://localhost:5002/plice/health',
{ signal: AbortSignal.timeout(400) });
if (r.ok) return 'http://localhost:5002';
} catch (_) {}
return (monsterSettings.serverUrl || 'http://100.91.239.7:5002').replace(/\/$/, '');
})();
// ── POST ───────────────────────────────────────────────────────────────
try {
const resp = await fetch(`${base}/plice/master`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ master_id: masterId, title, artist, url: window.location.pathname, reviews }),
signal: AbortSignal.timeout(10000),
});
const ok = resp.ok;
const d = ok ? await resp.json() : {};
console.log(`[MONSTER] master POST ${ok ? 'ok' : 'failed'}${d.review_rows ?? '?'} rows`);
chrome.runtime.sendMessage({
action: 'monsterMasterStatus',
state: ok ? 'done' : 'error',
masterId, reviewCount: d.review_rows || reviews.length,
});
} catch (e) {
console.warn('[MONSTER] master POST failed:', e.message);
chrome.runtime.sendMessage({ action: 'monsterMasterStatus', state: 'error', masterId });
}
})();