The match result to flatten.
Flattened result with all fields in a single-level object.
export function flattenMatchResult(
match: MangaMatchResult | FlattenableMatchResult,
): FlattenedMatchResult {
const kenmei = match.kenmeiManga;
// Find the highest confidence match from anilistMatches
const highestConfidenceMatch =
match.anilistMatches && match.anilistMatches.length > 0
? match.anilistMatches.reduce((prev, current) => {
const prevConf = prev.confidence ?? 0;
const currConf = current.confidence ?? 0;
return currConf > prevConf ? current : prev;
}, match.anilistMatches[0])
: null;
// Use selectedMatch or fall back to highest confidence match
const matchForData = match.selectedMatch ?? highestConfidenceMatch;
// Extract confidence from MangaMatch or default to 0
const confidence =
matchForData && "confidence" in matchForData
? (matchForData.confidence ?? 0)
: 0;
// Extract AniListManga data safely - handles shapes without id/title fields
const anilistData = extractAniListData(matchForData);
// Fallback to selectedMatch format/genres when anilistData is unavailable
// Use these fields only when AniList data is not present (no id/title from AniList)
const selectedMatchFormat =
anilistData === undefined ? match.selectedMatch?.format : undefined;
const selectedMatchGenres =
anilistData === undefined ? match.selectedMatch?.genres : undefined;
// Normalize genres array to semicolon-separated string
let genresString = "";
if (Array.isArray(anilistData?.genres)) {
genresString = anilistData.genres.join("; ");
} else if (Array.isArray(selectedMatchGenres)) {
genresString = selectedMatchGenres.join("; ");
}
const kenmeiRecord = kenmei as unknown as Record<string, unknown>;
const getKenmeiNumber = (camel: string, snake: string): number => {
const camelVal = kenmeiRecord[camel];
if (typeof camelVal === "number") return camelVal;
const snakeVal = kenmeiRecord[snake];
if (typeof snakeVal === "number") return snakeVal;
return 0;
};
const getKenmeiString = (camel: string, snake: string): string => {
const camelVal = kenmeiRecord[camel];
if (typeof camelVal === "string") return camelVal;
const snakeVal = kenmeiRecord[snake];
if (typeof snakeVal === "string") return snakeVal;
return "";
};
return {
// Kenmei data
kenmeiId: Number(kenmei.id),
kenmeiTitle: kenmei.title,
kenmeiStatus: kenmei.status || "",
kenmeiScore: kenmei.score ?? null,
chaptersRead: getKenmeiNumber("chaptersRead", "chapters_read"),
volumesRead: getKenmeiNumber("volumesRead", "volumes_read"),
author: "author" in kenmei ? (kenmei.author ?? "") : "",
notes: kenmei.notes || "",
createdAt: getKenmeiString("createdAt", "created_at"),
updatedAt: getKenmeiString("updatedAt", "updated_at"),
lastReadAt: getKenmeiString("lastReadAt", "last_read_at"),
// Match data
matchStatus: match.status,
matchDate: normalizeMatchDate(match.matchDate),
confidence,
// AniList data - explicitly set to empty strings when no AniList data available
// This ensures selectedMatch shapes without id/title don't incorrectly populate these fields
anilistId: anilistData?.id ?? null,
anilistTitleRomaji:
typeof anilistData?.title === "object"
? (anilistData.title.romaji ?? "")
: (anilistData?.title ?? ""),
anilistTitleEnglish:
typeof anilistData?.title === "object"
? (anilistData.title.english ?? "")
: (anilistData?.title ?? ""),
anilistTitleNative:
typeof anilistData?.title === "object"
? (anilistData.title.native ?? "")
: (anilistData?.title ?? ""),
// Prefer AniList data format, fall back to selectedMatch format when no AniList data
format: anilistData?.format ?? selectedMatchFormat ?? "",
totalChapters: anilistData?.chapters ?? null,
totalVolumes: anilistData?.volumes ?? null,
// Prefer AniList data genres, fall back to selectedMatch genres when no AniList data
genres: genresString,
};
}
Flattens a match result into a single-level structure suitable for CSV export. Selects best match data and normalizes dates/genres for tabular output.