import type { ConnectorMeta } from '@rareindex/connectors'; import { CertLookupConnector, type PageStatus, type ParsedCert } from '../_g2-grading-lib/cert-base.js'; import { cardCategoryFromText, clean, parseDate, titleCase, toInt } from '../_g2-grading-lib/text.js'; /** * TAG Grading cert lookup — https://my.taggrading.com/card/ (DIG report; public, robots.txt * explicitly allows /card/, ~3M card URLs listed in the public dig*.xml sitemaps). Client-rendered React * app → Firecrawl markdown (the DOM uses hashed MUI class names, so the markdown line sequence is the * stable surface). Complements connectors/firecrawl/tag-pop (set-level population tables). */ const PARSER_VERSION = '1.0.0'; const PATTERNS = [/^https?:\/\/my\.taggrading\.com\/card\/([A-Z]\d{7})\/?(?:[?#].*)?$/i, /^https?:\/\/tagd\.co\/([A-Z]\d{7})\/?(?:[?#].*)?$/i]; export function certUrl(cert: string): string { return `https://my.taggrading.com/card/${clean(cert).toUpperCase()}`; } export function certFromUrl(url: string): string | null { for (const re of PATTERNS) { const m = url.match(re); if (m) return m[1]!.toUpperCase(); } return null; } export interface TagPage { cert: string | null; name: string; setLine: string; extraLines: string[]; tagScore: number | null; grade: string | null; gradeLabel: string | null; asOf: Date | null; atGrade: number | null; totalGraded: number | null; gradedDate: Date | null; rankByGrade: string | null; rankOverall: string | null; images: string[]; } /** Pure parser over the trimmed markdown. Exported for tests. */ export function parseTagMarkdown(md: string): TagPage | null { const rawLines = md.split('\n').map((l) => clean(l)).filter(Boolean); const lines = rawLines.filter((l) => !/^!\[/.test(l)); const i = lines.findIndex((l) => /^TAG Score$/i.test(l)); if (i < 1) return null; const tagScore = toInt(lines[i - 1]); // Header block = the lines between the nav ("Submit") and the score. let start = i - 1; while (start > 0 && !/^(Submit|HomeAbout|Home)/i.test(lines[start - 1]!) && i - start < 6) start--; const header = lines.slice(start, i - 1); const name = header[0] ?? ''; const setLine = header[1] ?? ''; const extraLines = header.slice(2); if (!name) return null; const gradeTok = lines[i + 1] ?? ''; const grade = /^(10|[1-9](?:\.5)?)$/.test(gradeTok) ? gradeTok : /^auth/i.test(gradeTok) ? 'authentic' : null; const gradeLabel = grade ? (lines[i + 2] ?? null) : gradeTok || null; const cert = md.match(/cert\s*#\s*([A-Z]\d{7})/i)?.[1]?.toUpperCase() ?? null; const p = lines.findIndex((l) => /^population$/i.test(l)); let asOf: Date | null = null; let atGrade: number | null = null; let totalGraded: number | null = null; if (p >= 0) { asOf = parseDate((lines[p + 1] ?? '').replace(/^AS OF\s*/i, '')); for (let k = p + 1; k < Math.min(lines.length, p + 8); k++) { const l = lines[k]!; if (/^total graded$/i.test(l)) totalGraded = toInt(lines[k - 1]); else if (/ graded$/i.test(l) && !/^total/i.test(l)) atGrade = toInt(lines[k - 1]); } } const r = lines.findIndex((l) => /^card rank$/i.test(l)); let rankByGrade: string | null = null; let rankOverall: string | null = null; if (r >= 0) { for (let k = r + 1; k < Math.min(lines.length, r + 8); k++) { const l = lines[k]!; if (/^highest .* \(T\)$|^highest [A-Z ]+$/i.test(l) && !/overall/i.test(l)) rankByGrade = lines[k - 1] ?? null; if (/^highest overall/i.test(l)) rankOverall = lines[k - 1] ?? null; } } const gradedDate = parseDate(md.match(/graded\s+(\d{1,2}\/\d{1,2}\/\d{4})/i)?.[1]); const images = [...new Set([...md.matchAll(/!\[[^\]]*\]\((https?:\/\/[^)\s]+_(?:FRONT|BACK)_MAIN\.jpg)\)/gi)].map((m) => m[1]!))]; return { cert, name, setLine, extraLines, tagScore, grade, gradeLabel, asOf, atGrade, totalGraded, gradedDate, rankByGrade, rankOverall, images }; } /** "2021 BOWMAN CHROME #BCP-238" → year 2021, set "Bowman Chrome", number "BCP-238". */ export function parseSetLine(line: string): { year: number | null; set: string | null; number: string | null } { const t = clean(line); const year = t.match(/^(1[89]\d{2}|20\d{2})(?:-\d{2})?\b/)?.[1]; const number = t.match(/#\s*([A-Za-z0-9/.-]+)\s*$/)?.[1] ?? null; let set = t.replace(/#\s*[A-Za-z0-9/.-]+\s*$/, '').trim(); if (year) set = set.replace(/^(1[89]\d{2}|20\d{2})(?:-\d{2})?\s*/, '').trim(); return { year: year ? Number(year) : null, set: set ? titleCase(set) : null, number }; } export class TagCertConnector extends CertLookupConnector { readonly version = '1.0.0'; readonly parserVersion = PARSER_VERSION; readonly grader = 'tag'; readonly idKey = 'tag_cert'; readonly format = 'markdown' as const; override readonly urlPatterns = PATTERNS; protected override waitForMs = 8000; certUrl(cert: string): string { return certUrl(cert); } certFromUrl(url: string): string | null { return certFromUrl(url); } classify(doc: string): PageStatus { if (/cert\s*#\s*[A-Z]\d{7}/i.test(doc) && /TAG Score/i.test(doc)) return 'found'; if (/not found|no card|does not exist|invalid cert|could not be found|unable to find|no results|something went wrong/i.test(doc)) return 'not_found'; // The portal shell rendered without any card content = TAG has nothing for this cert. if (/Pop Report/i.test(doc) && !/TAG Score/i.test(doc) && !/Surface Defect/i.test(doc)) return 'not_found'; return 'unknown'; } trim(doc: string): string { const lines = doc.split('\n'); const end = lines.findIndex((l) => /TAG grading summary|Surface details|Corner Details/i.test(l)); const cut = end > 0 ? lines.slice(0, end) : lines; return cut .map((l) => l.replace(/ /g, ' ').trimEnd()) .filter((l, idx, arr) => l.trim() || (idx > 0 && arr[idx - 1]!.trim())) .join('\n') .slice(0, 12_000); } parse(snapshot: string, info: { cert: string; url: string }): ParsedCert | null { const page = parseTagMarkdown(snapshot); if (!page) return null; const cert = page.cert ?? info.cert; const s = parseSetLine(page.setLine); const variant = page.extraLines.filter((l) => !/^\d+$/.test(l)).join(' ') || null; const text = `${page.setLine} ${variant ?? ''} ${page.name}`; const cat = cardCategoryFromText(text); const isPokemon = cat.slug === 'pokemon'; return { title: `${page.setLine} ${page.name}${variant ? ` ${variant}` : ''} TAG ${page.grade ?? ''} ${page.gradeLabel ?? ''}`.replace(/\s+/g, ' ').trim(), attributes: { categorySlug: cat.slug, franchise: isPokemon ? 'Pokémon' : null, brand: isPokemon ? 'The Pokémon Company' : null, set: s.set, name: titleCase(page.name), number: s.number, year: s.year, variant, language: /japanese/i.test(text) ? 'Japanese' : null, identifiers: {}, metadata: { tag_cert: cert, tag_score: page.tagScore, graded_date: page.gradedDate?.toISOString().slice(0, 10) ?? null, rank_by_grade: page.rankByGrade, rank_overall: page.rankOverall, population_as_of: page.asOf?.toISOString().slice(0, 10) ?? null, }, }, grade: page.grade, qualifier: null, gradeLabel: page.gradeLabel, images: page.images, population: page.atGrade !== null ? { gradeKey: page.grade ?? page.gradeLabel ?? 'unknown', atGrade: page.atGrade, higher: null, total: page.totalGraded, url: info.url, asOf: page.asOf } : null, confidence: cat.confident ? 0.9 : 0.75, }; } } export default (meta: ConnectorMeta) => new TagCertConnector(meta);