game page refinements
This commit is contained in:
@@ -16,11 +16,20 @@ export interface Platform {
|
||||
|
||||
export interface DetailedGame extends Game {
|
||||
summary?: string;
|
||||
developer?: string;
|
||||
publisher?: string;
|
||||
developers?: string[];
|
||||
publishers?: string[];
|
||||
genres?: string[];
|
||||
franchises?: string[]; // New field
|
||||
releaseDate?: string;
|
||||
screenshots?: string[];
|
||||
fsName?: string;
|
||||
regions?: string[];
|
||||
players?: string;
|
||||
rating?: number; // New field
|
||||
esrbRating?: string;
|
||||
ratingIconUrl?: string; // Icon for Age Rating
|
||||
sha1?: string;
|
||||
collections?: string[];
|
||||
}
|
||||
|
||||
export interface RommCollection {
|
||||
@@ -79,6 +88,14 @@ const formatBytes = (bytes: number) => {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatDate = (unixSeconds: any) => {
|
||||
if (!unixSeconds) return undefined;
|
||||
const val = Number(unixSeconds);
|
||||
if (isNaN(val) || val < 1000000) return String(unixSeconds);
|
||||
const date = new Date(val * 1000);
|
||||
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(date);
|
||||
};
|
||||
|
||||
export const rommApiClient = {
|
||||
get apiBase() {
|
||||
const cleanUrl = getBaseUrl();
|
||||
@@ -173,14 +190,127 @@ export const rommApiClient = {
|
||||
if (!res.ok) throw new Error('Failed to fetch game details.');
|
||||
const json = await res.json();
|
||||
const game = mapRomToGame(json);
|
||||
|
||||
// Extract metadata with fallbacks across all providers
|
||||
const providers = [
|
||||
json.latest_igdb_metadata, json.igdb_metadata,
|
||||
json.latest_moby_metadata, json.moby_metadata,
|
||||
json.latest_screenscraper_metadata, json.screenscraper_metadata,
|
||||
json.latest_launchbox_metadata, json.launchbox_metadata,
|
||||
json.latest_steamgrid_metadata, json.steamgrid_metadata
|
||||
].filter(Boolean);
|
||||
|
||||
const getFirst = (key: string) => {
|
||||
for (const p of providers) {
|
||||
if (p[key] !== undefined && p[key] !== null) return p[key];
|
||||
}
|
||||
return json[key];
|
||||
};
|
||||
|
||||
const getAll = (key: string) => {
|
||||
const all: any[] = [];
|
||||
if (Array.isArray(json[key])) all.push(...json[key]);
|
||||
for (const p of providers) {
|
||||
if (Array.isArray(p[key])) all.push(...p[key]);
|
||||
}
|
||||
return Array.from(new Set(all)).filter(Boolean);
|
||||
};
|
||||
|
||||
// Extract Age Ratings from all providers (array format for IGDB/ScreenScraper)
|
||||
const ageRatings = new Set<string>();
|
||||
for (const p of providers) {
|
||||
if (Array.isArray(p.age_ratings)) {
|
||||
p.age_ratings.forEach((ar: any) => {
|
||||
if (ar.rating) ageRatings.add(String(ar.rating));
|
||||
});
|
||||
}
|
||||
const pRating = p.esrb_rating || p.esrb || p.pegi_rating || p.pegi;
|
||||
if (pRating && typeof pRating === 'string') ageRatings.add(pRating);
|
||||
}
|
||||
|
||||
// Filter out placeholders like "Not Rated" or "Unknown" if we have valid ratings
|
||||
let finalRatings = Array.from(ageRatings).filter(Boolean);
|
||||
const placeholders = ['not rated', 'unknown', 'pending', 'nr', 'rp', 'rating pending'];
|
||||
if (finalRatings.some(r => !placeholders.includes(r.toLowerCase()))) {
|
||||
finalRatings = finalRatings.filter(r => !placeholders.includes(r.toLowerCase()));
|
||||
}
|
||||
|
||||
const ageRatingStr = finalRatings.join(' / ') || (json.esrb_rating || json.esrb);
|
||||
|
||||
// Defensive mapping for regions
|
||||
const regionsRaw = [
|
||||
...(json.rom_regions || []),
|
||||
...(json.region ? [json.region] : []),
|
||||
...(getFirst('regions') || [])
|
||||
];
|
||||
|
||||
const regions = Array.from(new Set(regionsRaw.map((r: any) =>
|
||||
typeof r === 'string' ? r : (r.display_name || r.name || String(r))
|
||||
))).filter(Boolean);
|
||||
|
||||
const extractCompanies = (type: 'developer' | 'publisher') => {
|
||||
const companies = new Set<string>();
|
||||
|
||||
// 1. Root lists
|
||||
const rootList = type === 'developer' ? (json.developers || json.companies) : (json.publishers || json.companies);
|
||||
if (Array.isArray(rootList)) {
|
||||
rootList.forEach(c => companies.add(typeof c === 'string' ? c : (c.name || c.display_name)));
|
||||
}
|
||||
|
||||
// 2. Provider metadata
|
||||
for (const p of providers) {
|
||||
// IGDB involved_companies
|
||||
if (Array.isArray(p.involved_companies)) {
|
||||
p.involved_companies.forEach((ic: any) => {
|
||||
if (ic[type] && ic.company?.name) companies.add(ic.company.name);
|
||||
});
|
||||
}
|
||||
|
||||
// General companies/developers/publishers lists
|
||||
const pList = p.companies || (type === 'developer' ? (p.developers || p.developer_companies) : (p.publishers || p.publisher_companies));
|
||||
if (Array.isArray(pList)) {
|
||||
pList.forEach((c: any) => companies.add(typeof c === 'string' ? c : (c.name || c.display_name)));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Root singulars
|
||||
const rootSingular = type === 'developer' ? (json.developer || json.developer_name) : (json.publisher || json.publisher_name);
|
||||
if (rootSingular && typeof rootSingular === 'string') companies.add(rootSingular);
|
||||
|
||||
return Array.from(companies).filter(Boolean);
|
||||
};
|
||||
|
||||
const devList = extractCompanies('developer');
|
||||
const pubList = extractCompanies('publisher');
|
||||
|
||||
// Rating extraction with ScreenScraper/Launchbox fallbacks
|
||||
let ratingVal = getFirst('rating') || getFirst('total_rating') || getFirst('aggregated_rating') || getFirst('ss_score') || getFirst('community_rating');
|
||||
if (ratingVal && String(ratingVal).includes('.')) {
|
||||
const parsed = parseFloat(String(ratingVal));
|
||||
// Normalize 0-10 or 0-5 scales to 100%
|
||||
if (parsed <= 10) ratingVal = parsed * 10;
|
||||
else if (parsed <= 5) ratingVal = parsed * 20;
|
||||
}
|
||||
|
||||
return {
|
||||
...game,
|
||||
summary: json.summary,
|
||||
developer: json.developer,
|
||||
publisher: json.publisher,
|
||||
genres: json.genres,
|
||||
releaseDate: json.release_date,
|
||||
screenshots: (json.screenshots || []).map((s: any) => getFullImageUrl(s.url) || '')
|
||||
summary: json.summary || getFirst('summary') || getFirst('description'),
|
||||
developers: devList,
|
||||
publishers: pubList,
|
||||
genres: getAll('genres').map(g => typeof g === 'string' ? g : (g.name || g.display_name)),
|
||||
franchises: getAll('franchises').map(f => typeof f === 'string' ? f : (f.name || f.display_name)),
|
||||
releaseDate: formatDate(json.release_date || getFirst('first_release_date') || getFirst('release_date')),
|
||||
screenshots: (json.screenshots || []).map((s: any) => s.url ? getFullImageUrl(s.url) || '' : ''),
|
||||
fsName: json.fs_name,
|
||||
regions: regions.length > 0 ? regions : ['Global'],
|
||||
players: json.players || getFirst('total_players') || getFirst('players'),
|
||||
rating: ratingVal ? Math.round(Number(ratingVal)) : undefined,
|
||||
esrbRating: ageRatingStr,
|
||||
sha1: json.hashes?.sha1,
|
||||
collections: Array.from(new Set([
|
||||
...(json.rom_collections || []).map((c: any) => c.name),
|
||||
...(getFirst('collections') || [])
|
||||
])).filter(Boolean),
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user