SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
5.7 KB · 142 lines typescript
Raw Blame History
1import { html as H, type ConnectorMeta } from '@rareindex/connectors';2import { CertLookupConnector, type PageStatus, type ParsedCert } from '../_g2-grading-lib/cert-base.js';3import { clean, money, parseCoinGrade, toInt, yearOf } from '../_g2-grading-lib/text.js';45/**6 * PCGS certificate verification — https://www.pcgs.com/cert/<cert> (public; coins and banknotes).7 * Svelte-rendered page behind a Cloudflare managed challenge → Firecrawl render (Scrapfly fallback).8 * The page carries the coin's PCGS# (deterministic id shared with connectors/firecrawl/pcgs-priceguide),9 * grade, price-guide value and the PCGS population at that grade + higher.10 */11const PARSER_VERSION = '1.0.0';12const PATTERNS = [/^https?:\/\/(?:www\.)?pcgs\.com\/cert\/(\d{6,10})\/?(?:[?#].*)?$/i, /^https?:\/\/(?:www\.)?pcgs\.com\/cert\/verify\?(?:.*&)?certno=(\d{6,10})/i];1314export function certUrl(cert: string): string {15  return `https://www.pcgs.com/cert/${clean(cert).replace(/\D/g, '')}`;16}1718export function certFromUrl(url: string): string | null {19  for (const re of PATTERNS) {20    const m = url.match(re);21    if (m) return m[1]!;22  }23  return null;24}2526export interface PcgsPage {27  title: string;28  cert: string | null;29  rows: Record<string, string>;30  links: Record<string, string>;31  images: string[];32}3334/** Pure parser over the trimmed fragment. Exported for tests. */35export function parsePcgsPage(snapshot: string): PcgsPage | null {36  const $ = H.load(snapshot);37  const title = clean($('.text-display5').first().text());38  const cert = clean($('.text-display5').first().parent().text()).match(/#(\d{6,10})/)?.[1] ?? clean($.root().text()).match(/#(\d{6,10})/)?.[1] ?? null;39  const rows: Record<string, string> = {};40  const links: Record<string, string> = {};41  $('table tr').each((_, tr) => {42    const tds = $(tr).find('td');43    if (tds.length < 2) return;44    const label = clean($(tds[0]).text());45    if (!label) return;46    rows[label] = clean($(tds[1]).text());47    const href = $(tds[1]).find('a[href]').first().attr('href');48    if (href) links[label] = href;49  });50  if (!title || !rows['Grade']) return null;51  const images = [...new Set($('img[alt*="enlarge" i], img[src*="/pcgs/cert/"]').toArray().map((e) => $(e).attr('src') ?? '').filter((u) => /^https?:\/\//.test(u)))];52  return { title, cert, rows, links, images };53}5455export class PcgsCertConnector extends CertLookupConnector {56  readonly version = '1.0.0';57  readonly parserVersion = PARSER_VERSION;58  readonly grader = 'pcgs';59  readonly idKey = 'pcgs_cert';60  readonly format = 'html' as const;61  override readonly urlPatterns = PATTERNS;6263  certUrl(cert: string): string {64    return certUrl(cert);65  }66  certFromUrl(url: string): string | null {67    return certFromUrl(url);68  }6970  classify(doc: string): PageStatus {71    const $ = H.load(doc);72    const hasGradeRow = $('table tr td')73      .toArray()74      .some((td) => /^grade$/i.test(clean($(td).text())));75    if ($('.text-display5').length && hasGradeRow) return 'found';76    const text = clean($('body').text() || $.root().text());77    if (/not found|no record|invalid cert|does not exist|could not be found|unable to (find|locate)|no results|not in our|we were unable|isn.t in the pcgs/i.test(text)) return 'not_found';78    return 'unknown';79  }8081  trim(doc: string): string {82    const $ = H.load(doc);83    $('script, style, noscript, iframe, svg, header, footer, nav, form').remove();84    let node = $('.text-display5').first();85    if (!node.length) {86      const body = $('body');87      return `<div class="g2-trimmed">${clean(body.text()).slice(0, 4000)}</div>`;88    }89    for (let i = 0; i < 10 && node.length; i++) {90      if (node.find('table').length) break;91      node = node.parent();92    }93    return $.html(node.length ? node : $('body'));94  }9596  parse(snapshot: string, info: { cert: string; url: string }): ParsedCert | null {97    const page = parsePcgsPage(snapshot);98    if (!page) return null;99    const r = page.rows;100    const cert = page.cert ?? info.cert;101    const g = parseCoinGrade(r['Grade']);102    const pcgsNo = clean(r['PCGS #'] ?? '').replace(/\D/g, '') || null;103    const region = r['Region'] ?? null;104    const isNote = /note|banknote|currency/i.test(`${page.title} ${r['Denomination'] ?? ''} ${r['Holder Type'] ?? ''}`) && !/coin/i.test(r['Holder Type'] ?? '');105    const identifiers: Record<string, string> = {};106    if (pcgsNo) identifiers.pcgs_number = pcgsNo;107    const population = toInt(r['Population']);108    return {109      title: `${page.title} PCGS ${r['Grade']}`.trim(),110      attributes: {111        categorySlug: isNote ? 'banknotes' : 'coins',112        name: page.title,113        year: yearOf(r['Date, Mintmark'] ?? page.title),114        country: /united states/i.test(region ?? '') ? 'US' : null,115        region,116        variant: r['Variety'] ?? r['Designation'] ?? null,117        productionQuantity: toInt(r['Mintage']),118        identifiers,119        metadata: {120          pcgs_cert: cert,121          date_mintmark: r['Date, Mintmark'] ?? null,122          denomination: r['Denomination'] ?? null,123          price_guide_value_usd: money(r['Price Guide Value']),124          holder_type: r['Holder Type'] ?? null,125          security: r['Security'] ?? null,126          coinfacts_url: page.links['PCGS #'] ?? null,127          population_url: page.links['Population'] ?? null,128          fields: r,129        },130      },131      grade: g.grade,132      qualifier: g.qualifier,133      gradeLabel: g.label || null,134      images: page.images,135      population: population !== null ? { gradeKey: g.grade ?? g.label, atGrade: population, higher: toInt(r['Pop Higher']), url: page.links['Population'] ?? null } : null,136      confidence: pcgsNo ? 0.95 : 0.85,137    };138  }139}140141export default (meta: ConnectorMeta) => new PcgsCertConnector(meta);142