208 lines
5.8 KiB
TypeScript
208 lines
5.8 KiB
TypeScript
import { readFile } from "node:fs/promises"
|
|
import path from "node:path"
|
|
import {
|
|
rankListingsForQuery,
|
|
sortListingsByFreshness,
|
|
type CachedEbayListing,
|
|
} from "@/lib/ebay-parts-match"
|
|
|
|
export type ManualPartRow = {
|
|
partNumber: string
|
|
description: string
|
|
ebayListings?: CachedEbayListing[]
|
|
}
|
|
|
|
type ManualPartsLookup = Record<string, ManualPartRow[]>
|
|
type ManualPagesPartsLookup = Record<string, Record<string, ManualPartRow[]>>
|
|
|
|
let manualPartsCache: ManualPartsLookup | null = null
|
|
let manualPagesPartsCache: ManualPagesPartsLookup | null = null
|
|
let staticEbayListingsCache: CachedEbayListing[] | null = null
|
|
|
|
async function readJsonFile<T>(filename: string): Promise<T> {
|
|
const filePath = path.join(process.cwd(), "public", filename)
|
|
const contents = await readFile(filePath, "utf8")
|
|
return JSON.parse(contents) as T
|
|
}
|
|
|
|
export async function loadManualPartsLookup(): Promise<ManualPartsLookup> {
|
|
if (!manualPartsCache) {
|
|
manualPartsCache = await readJsonFile<ManualPartsLookup>(
|
|
"manual_parts_lookup.json"
|
|
)
|
|
}
|
|
|
|
return manualPartsCache
|
|
}
|
|
|
|
export async function loadManualPagesPartsLookup(): Promise<ManualPagesPartsLookup> {
|
|
if (!manualPagesPartsCache) {
|
|
manualPagesPartsCache = await readJsonFile<ManualPagesPartsLookup>(
|
|
"manual_pages_parts.json"
|
|
)
|
|
}
|
|
|
|
return manualPagesPartsCache
|
|
}
|
|
|
|
export async function findManualParts(
|
|
manualFilename: string
|
|
): Promise<ManualPartRow[]> {
|
|
const manualParts = await loadManualPartsLookup()
|
|
if (manualFilename in manualParts) {
|
|
return manualParts[manualFilename]
|
|
}
|
|
|
|
const lowerFilename = manualFilename.toLowerCase()
|
|
for (const [filename, parts] of Object.entries(manualParts)) {
|
|
if (filename.toLowerCase() === lowerFilename) {
|
|
return parts
|
|
}
|
|
}
|
|
|
|
const filenameWithoutExt = manualFilename.replace(/\.pdf$/i, "")
|
|
const lowerWithoutExt = filenameWithoutExt.toLowerCase()
|
|
|
|
for (const [filename, parts] of Object.entries(manualParts)) {
|
|
const otherWithoutExt = filename.replace(/\.pdf$/i, "").toLowerCase()
|
|
if (
|
|
otherWithoutExt === lowerWithoutExt ||
|
|
otherWithoutExt.includes(lowerWithoutExt) ||
|
|
lowerWithoutExt.includes(otherWithoutExt)
|
|
) {
|
|
return parts
|
|
}
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
export async function findManualPageParts(
|
|
manualFilename: string,
|
|
pageNumber: number
|
|
): Promise<ManualPartRow[]> {
|
|
const manualPagesParts = await loadManualPagesPartsLookup()
|
|
if (
|
|
manualPagesParts[manualFilename] &&
|
|
manualPagesParts[manualFilename][pageNumber.toString()]
|
|
) {
|
|
return manualPagesParts[manualFilename][pageNumber.toString()]
|
|
}
|
|
|
|
const lowerFilename = manualFilename.toLowerCase()
|
|
for (const [filename, pages] of Object.entries(manualPagesParts)) {
|
|
if (
|
|
filename.toLowerCase() === lowerFilename &&
|
|
pages[pageNumber.toString()]
|
|
) {
|
|
return pages[pageNumber.toString()]
|
|
}
|
|
}
|
|
|
|
const filenameWithoutExt = manualFilename.replace(/\.pdf$/i, "")
|
|
const lowerWithoutExt = filenameWithoutExt.toLowerCase()
|
|
|
|
for (const [filename, pages] of Object.entries(manualPagesParts)) {
|
|
const otherWithoutExt = filename.replace(/\.pdf$/i, "").toLowerCase()
|
|
if (
|
|
otherWithoutExt === lowerWithoutExt ||
|
|
otherWithoutExt.includes(lowerWithoutExt) ||
|
|
lowerWithoutExt.includes(otherWithoutExt)
|
|
) {
|
|
if (pages[pageNumber.toString()]) {
|
|
return pages[pageNumber.toString()]
|
|
}
|
|
}
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
export async function listManualsWithParts(): Promise<Set<string>> {
|
|
const manualParts = await loadManualPartsLookup()
|
|
const manualsWithParts = new Set<string>()
|
|
|
|
for (const [filename, parts] of Object.entries(manualParts)) {
|
|
if (parts.length > 0) {
|
|
manualsWithParts.add(filename)
|
|
manualsWithParts.add(filename.toLowerCase())
|
|
manualsWithParts.add(filename.replace(/\.pdf$/i, ""))
|
|
manualsWithParts.add(filename.replace(/\.pdf$/i, "").toLowerCase())
|
|
}
|
|
}
|
|
|
|
return manualsWithParts
|
|
}
|
|
|
|
function dedupeListings(listings: CachedEbayListing[]): CachedEbayListing[] {
|
|
const byItemId = new Map<string, CachedEbayListing>()
|
|
|
|
for (const listing of listings) {
|
|
const itemId = listing.itemId?.trim()
|
|
if (!itemId) {
|
|
continue
|
|
}
|
|
|
|
const existing = byItemId.get(itemId)
|
|
if (!existing) {
|
|
byItemId.set(itemId, listing)
|
|
continue
|
|
}
|
|
|
|
const existingFreshness = existing.lastSeenAt ?? existing.fetchedAt ?? 0
|
|
const nextFreshness = listing.lastSeenAt ?? listing.fetchedAt ?? 0
|
|
if (nextFreshness >= existingFreshness) {
|
|
byItemId.set(itemId, {
|
|
...existing,
|
|
...listing,
|
|
sourceQueries: Array.from(
|
|
new Set([...(existing.sourceQueries || []), ...(listing.sourceQueries || [])])
|
|
),
|
|
})
|
|
}
|
|
}
|
|
|
|
return sortListingsByFreshness(Array.from(byItemId.values()))
|
|
}
|
|
|
|
export async function loadStaticEbayListings(): Promise<CachedEbayListing[]> {
|
|
if (staticEbayListingsCache) {
|
|
return staticEbayListingsCache
|
|
}
|
|
|
|
const [manualParts, manualPagesParts] = await Promise.all([
|
|
loadManualPartsLookup(),
|
|
loadManualPagesPartsLookup(),
|
|
])
|
|
|
|
const listings: CachedEbayListing[] = []
|
|
|
|
for (const parts of Object.values(manualParts)) {
|
|
for (const part of parts) {
|
|
if (Array.isArray(part.ebayListings)) {
|
|
listings.push(...part.ebayListings)
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const pages of Object.values(manualPagesParts)) {
|
|
for (const parts of Object.values(pages)) {
|
|
for (const part of parts) {
|
|
if (Array.isArray(part.ebayListings)) {
|
|
listings.push(...part.ebayListings)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
staticEbayListingsCache = dedupeListings(listings)
|
|
return staticEbayListingsCache
|
|
}
|
|
|
|
export async function searchStaticEbayListings(
|
|
query: string,
|
|
limit = 6
|
|
): Promise<CachedEbayListing[]> {
|
|
const listings = await loadStaticEbayListings()
|
|
return rankListingsForQuery(query, listings, limit)
|
|
}
|