feat: enrich tape identifier results with entry details
Show authors, genre, machine type, release year, CRC32, and a prominent "View entry" link. Joins releases, genretypes, machinetypes, and authors in lookupByMd5() for richer context. opus-4-6@McFiver
This commit is contained in:
@@ -88,29 +88,67 @@ export default function TapeIdentifier() {
|
|||||||
|
|
||||||
{state.kind === "results" ? (
|
{state.kind === "results" ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-secondary mb-2">
|
<p className="text-secondary mb-3">
|
||||||
<strong>{state.fileName}</strong> matched {state.matches.length === 1 ? "1 entry" : `${state.matches.length} entries`}:
|
<strong>{state.fileName}</strong> matched {state.matches.length === 1 ? "1 entry" : `${state.matches.length} entries`}:
|
||||||
</p>
|
</p>
|
||||||
<div className="list-group list-group-flush mb-3">
|
{state.matches.map((m) => (
|
||||||
{state.matches.map((m) => (
|
<div key={m.downloadId} className="card border mb-3">
|
||||||
<div key={m.downloadId} className="list-group-item px-0">
|
<div className="card-body">
|
||||||
<div className="d-flex justify-content-between align-items-start">
|
<div className="d-flex justify-content-between align-items-start mb-2">
|
||||||
<div>
|
<h6 className="card-title mb-0">
|
||||||
<Link href={`/zxdb/entries/${m.entryId}`} className="fw-semibold text-decoration-none">
|
<Link href={`/zxdb/entries/${m.entryId}`} className="text-decoration-none">
|
||||||
{m.entryTitle}
|
{m.entryTitle}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="text-secondary small mt-1">
|
</h6>
|
||||||
{m.innerPath}
|
{m.releaseYear && (
|
||||||
</div>
|
<span className="badge text-bg-secondary ms-2">{m.releaseYear}</span>
|
||||||
</div>
|
)}
|
||||||
<div className="text-end text-secondary small text-nowrap ms-3">
|
|
||||||
<div>{formatBytes(m.sizeBytes)}</div>
|
|
||||||
<div className="font-monospace" style={{ fontSize: "0.75rem" }}>{m.md5}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(m.authors.length > 0 || m.genre || m.machinetype) && (
|
||||||
|
<div className="d-flex flex-wrap gap-2 mb-2 small text-secondary">
|
||||||
|
{m.authors.length > 0 && (
|
||||||
|
<span><span className="bi bi-person me-1" aria-hidden />{m.authors.join(", ")}</span>
|
||||||
|
)}
|
||||||
|
{m.genre && (
|
||||||
|
<span><span className="bi bi-tag me-1" aria-hidden />{m.genre}</span>
|
||||||
|
)}
|
||||||
|
{m.machinetype && (
|
||||||
|
<span><span className="bi bi-cpu me-1" aria-hidden />{m.machinetype}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<table className="table table-sm table-borderless mb-2 small" style={{ maxWidth: 500 }}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="text-secondary ps-0" style={{ width: 90 }}>File</td>
|
||||||
|
<td className="font-monospace">{m.innerPath}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="text-secondary ps-0">Size</td>
|
||||||
|
<td>{formatBytes(m.sizeBytes)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="text-secondary ps-0">MD5</td>
|
||||||
|
<td className="font-monospace">{m.md5}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="text-secondary ps-0">CRC32</td>
|
||||||
|
<td className="font-monospace">{m.crc32}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/zxdb/entries/${m.entryId}`}
|
||||||
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
>
|
||||||
|
View entry <span className="bi bi-arrow-right ms-1" aria-hidden />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-secondary mb-3">
|
<p className="text-secondary mb-3">
|
||||||
|
|||||||
@@ -2633,6 +2633,11 @@ export type TapeMatch = {
|
|||||||
md5: string;
|
md5: string;
|
||||||
crc32: string;
|
crc32: string;
|
||||||
sizeBytes: number;
|
sizeBytes: number;
|
||||||
|
machinetype: string | null;
|
||||||
|
genre: string | null;
|
||||||
|
releaseYear: number | null;
|
||||||
|
authors: string[];
|
||||||
|
downloadLink: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function lookupByMd5(md5: string): Promise<TapeMatch[]> {
|
export async function lookupByMd5(md5: string): Promise<TapeMatch[]> {
|
||||||
@@ -2645,12 +2650,46 @@ export async function lookupByMd5(md5: string): Promise<TapeMatch[]> {
|
|||||||
md5: softwareHashes.md5,
|
md5: softwareHashes.md5,
|
||||||
crc32: softwareHashes.crc32,
|
crc32: softwareHashes.crc32,
|
||||||
sizeBytes: softwareHashes.sizeBytes,
|
sizeBytes: softwareHashes.sizeBytes,
|
||||||
|
machinetypeName: machinetypes.name,
|
||||||
|
genreName: genretypes.name,
|
||||||
|
releaseYear: releases.releaseYear,
|
||||||
|
downloadLink: downloads.fileLink,
|
||||||
})
|
})
|
||||||
.from(softwareHashes)
|
.from(softwareHashes)
|
||||||
.innerJoin(downloads, eq(downloads.id, softwareHashes.downloadId))
|
.innerJoin(downloads, eq(downloads.id, softwareHashes.downloadId))
|
||||||
.innerJoin(entries, eq(entries.id, downloads.entryId))
|
.innerJoin(entries, eq(entries.id, downloads.entryId))
|
||||||
|
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||||
|
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||||
|
.leftJoin(
|
||||||
|
releases,
|
||||||
|
and(eq(releases.entryId, downloads.entryId), eq(releases.releaseSeq, downloads.releaseSeq))
|
||||||
|
)
|
||||||
.where(eq(softwareHashes.md5, md5.toLowerCase()));
|
.where(eq(softwareHashes.md5, md5.toLowerCase()));
|
||||||
|
|
||||||
|
// Collect unique entry IDs to fetch authors
|
||||||
|
const entryIds = [...new Set(rows.map((r) => Number(r.entryId)))];
|
||||||
|
const authorMap = new Map<number, string[]>();
|
||||||
|
if (entryIds.length > 0) {
|
||||||
|
try {
|
||||||
|
const authorRows = await db
|
||||||
|
.select({ entryId: authors.entryId, name: labels.name })
|
||||||
|
.from(authors)
|
||||||
|
.innerJoin(labels, eq(labels.id, authors.labelId))
|
||||||
|
.where(
|
||||||
|
entryIds.length === 1
|
||||||
|
? eq(authors.entryId, entryIds[0])
|
||||||
|
: sql`${authors.entryId} in (${sql.join(entryIds.map((id) => sql`${id}`), sql`, `)})`
|
||||||
|
)
|
||||||
|
.orderBy(asc(authors.authorSeq));
|
||||||
|
for (const a of authorRows) {
|
||||||
|
const eid = Number(a.entryId);
|
||||||
|
const existing = authorMap.get(eid);
|
||||||
|
if (existing) existing.push(a.name);
|
||||||
|
else authorMap.set(eid, [a.name]);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
downloadId: Number(r.downloadId),
|
downloadId: Number(r.downloadId),
|
||||||
entryId: Number(r.entryId),
|
entryId: Number(r.entryId),
|
||||||
@@ -2659,5 +2698,10 @@ export async function lookupByMd5(md5: string): Promise<TapeMatch[]> {
|
|||||||
md5: r.md5,
|
md5: r.md5,
|
||||||
crc32: r.crc32,
|
crc32: r.crc32,
|
||||||
sizeBytes: Number(r.sizeBytes),
|
sizeBytes: Number(r.sizeBytes),
|
||||||
|
machinetype: r.machinetypeName ?? null,
|
||||||
|
genre: r.genreName ?? null,
|
||||||
|
releaseYear: r.releaseYear != null ? Number(r.releaseYear) : null,
|
||||||
|
authors: authorMap.get(Number(r.entryId)) ?? [],
|
||||||
|
downloadLink: r.downloadLink,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user