Rocky_Mountain_Vending/lib/server/manual-parts-data.ts

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)
}