mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-27 17:01:04 +01:00
207 lines
10 KiB
JavaScript
207 lines
10 KiB
JavaScript
// ../pub-game-store/native/state/catalog.js
|
|
// reconstructed from the esbuild bundle in eboot.bin
|
|
|
|
import { features, games, twins } from '../data/catalog.js';
|
|
import { formatDate2, formatNumber2, t } from '../i18n/core.js';
|
|
import { createPageCache } from './page-cache.js';
|
|
import { createReleaseCache } from './release-cache.js';
|
|
import { FileSystem, Image } from '@ps5-react/core';
|
|
|
|
var COVER_WIDTH = 440;
|
|
var coverSize = (url) => url.startsWith("https://image.api.playstation.com/") ? url.replace(/([?&]w=)\d+/, `$1${COVER_WIDTH}`) : url;
|
|
var releaseCache = createReleaseCache((path) => FileSystem.readFile(`/app0/${path}`));
|
|
var pageCache = createPageCache((path) => FileSystem.readFile(`/app0/${path}`));
|
|
var NO_PAGE = { about: [], shots: [], summary: "" };
|
|
var SYNONYMS = {
|
|
"Role Playing Game (RPG)": "RPG",
|
|
"Role Playing Games": "RPG",
|
|
"Role-playing": "RPG",
|
|
Simulator: "Simulation",
|
|
"Driving/Racing": "Racing"
|
|
};
|
|
var dataFile = (dir2, id) => `data/${dir2}/${encodeURIComponent(id)}.json`;
|
|
var pageOf = (game) => {
|
|
try {
|
|
return pageCache(dataFile("pages", game.id));
|
|
} catch {
|
|
return NO_PAGE;
|
|
}
|
|
};
|
|
var Entry = {
|
|
get releases() {
|
|
try {
|
|
return releaseCache(dataFile("releases", this.id));
|
|
} catch {
|
|
return [];
|
|
}
|
|
},
|
|
get about() {
|
|
return pageOf(this).about;
|
|
},
|
|
get shots() {
|
|
return pageOf(this).shots;
|
|
},
|
|
get shortDescription() {
|
|
return pageOf(this).summary;
|
|
},
|
|
get titleId() {
|
|
return this.id;
|
|
}
|
|
};
|
|
var entries = [...games, ...twins];
|
|
var _a;
|
|
for (const game of entries) {
|
|
Object.setPrototypeOf(game, Entry);
|
|
game.genres = [...new Set(game.genres.flatMap((genre) => genre.split(" / ")).map((genre) => SYNONYMS[genre] ?? genre))];
|
|
(_a = game.art).cover && (_a.cover = coverSize(game.art.cover));
|
|
if (game.art.coverFallbacks) game.art.coverFallbacks = game.art.coverFallbacks.map(coverSize);
|
|
}
|
|
var editionsByWork = /* @__PURE__ */ new Map();
|
|
for (const game of entries) if (game.work) editionsByWork.set(game.work, [...editionsByWork.get(game.work) ?? [], game]);
|
|
var listed = new Set(games);
|
|
for (const game of entries) game.platforms = [...new Set(editionsOf(game).map((edition2) => edition2.platform))].sort();
|
|
function editionsOf(game) {
|
|
return [...editionsByWork.get(game.work) ?? [game]].sort((a, b) => b.platform.localeCompare(a.platform) || Number(listed.has(b)) - Number(listed.has(a)) || a.titleId.localeCompare(b.titleId));
|
|
}
|
|
var genres = [...new Set(games.flatMap((game) => game.genres))].sort();
|
|
var byId = new Map(entries.map((game) => [game.id, game]));
|
|
var getGame = (id) => byId.get(id);
|
|
function listedId(id) {
|
|
const game = byId.get(id);
|
|
if (!game || listed.has(game)) return id;
|
|
return editionsOf(game).find((edition2) => listed.has(edition2) && edition2.platform === game.platform)?.id ?? id;
|
|
}
|
|
function warmCovers() {
|
|
Image.warm(sortGames(games, "relevance").filter((game) => game.art.cover).map((game) => game.art.cover));
|
|
}
|
|
var SORTS = [
|
|
// Most popular puts the games Archive.org serves first: the app downloads them fastest.
|
|
{ id: "relevance", compare: (a, b) => fromArchive(b) - fromArchive(a) || b.recommendations - a.recommendations },
|
|
{ id: "newest", compare: (a, b) => b.releaseDate.localeCompare(a.releaseDate) },
|
|
{ id: "rating", compare: (a, b) => (b.metacritic ?? 0) - (a.metacritic ?? 0) },
|
|
{ id: "size", compare: (a, b) => (b.sizeGb ?? 0) - (a.sizeGb ?? 0) },
|
|
{ id: "title", compare: (a, b) => a.title.localeCompare(b.title) }
|
|
];
|
|
var sortLabel = (id) => t(`sort.${id}`);
|
|
var sortGames = (list, id) => [...list].sort(SORTS.find((sort) => sort.id === id).compare);
|
|
var fromArchive = (game) => Boolean(game.providers?.includes("archive.org"));
|
|
function showcase(list, count) {
|
|
const first = list.filter(fromArchive);
|
|
return first.length >= count ? first.slice(0, count) : [...first, ...list.filter((game) => !fromArchive(game))].slice(0, count);
|
|
}
|
|
var HOST_NAMES = {
|
|
"overdrive cdn": "Overdrive CDN",
|
|
vikingfile: "VikingFile",
|
|
"archive.org": "Archive.org",
|
|
"1fichier": "1fichier",
|
|
"pippo26442999.github": "Pippo mirror",
|
|
datanodes: "DataNodes",
|
|
mediafire: "MediaFire",
|
|
filekeeper: "FileKeeper",
|
|
datavaults: "DataVaults",
|
|
spectrum: "Spectrum",
|
|
buzzheavier: "Buzzheavier",
|
|
gofile: "Gofile",
|
|
rapidgator: "Rapidgator",
|
|
filecrypt: "Filecrypt",
|
|
fileditchfiles: "FileDitch",
|
|
ranoz: "Ranoz",
|
|
transfer: "Transfer.it",
|
|
akirabox: "AkiraBox",
|
|
"link-vault.org": "LinkVault",
|
|
"rootz.so": "Rootz"
|
|
};
|
|
var MIN_HOST_GAMES = 20;
|
|
var hostCounts = /* @__PURE__ */ new Map();
|
|
for (const game of games) for (const host2 of game.providers ?? []) hostCounts.set(host2, (hostCounts.get(host2) ?? 0) + 1);
|
|
function slug(value) {
|
|
const words = value.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
return words.map((word, i) => i ? word[0].toUpperCase() + word.slice(1) : word).join("");
|
|
}
|
|
var CDN_HOST = "overdrive cdn";
|
|
var hosts = [...hostCounts].filter(([host2, count]) => count >= MIN_HOST_GAMES || host2 === CDN_HOST).sort((a, b) => (b[0] === CDN_HOST) - (a[0] === CDN_HOST) || b[1] - a[1]).map(([host2]) => ({ value: host2, name: HOST_NAMES[host2] ?? host2 }));
|
|
var onCdn = (game) => Boolean(game.providers?.includes(CDN_HOST));
|
|
var SIZES = [
|
|
{ value: "small", label: "filters.size.small", test: (gb) => gb < 10 },
|
|
{ value: "medium", label: "filters.size.medium", test: (gb) => gb >= 10 && gb <= 50 },
|
|
{ value: "large", label: "filters.size.large", test: (gb) => gb > 50 }
|
|
];
|
|
var named = (values, namespace) => values.map((value) => ({ value, label: `${namespace}.${slug(value)}`, name: value }));
|
|
var byGames = (values, has) => {
|
|
const counts = new Map(values.map((value) => [value, games.filter((game) => has(game, value)).length]));
|
|
return [...values].sort((a, b) => counts.get(b) - counts.get(a) || a.localeCompare(b));
|
|
};
|
|
var RELEASES = [
|
|
{ value: "recent", label: "filters.release.recent", test: (year) => year >= 2024 },
|
|
{ value: "current", label: "filters.release.current", test: (year) => year >= 2020 && year <= 2023 },
|
|
{ value: "older", label: "filters.release.older", test: (year) => year < 2020 }
|
|
];
|
|
var FACETS = [
|
|
{
|
|
key: "platform",
|
|
any: true,
|
|
options: [{ value: "ps5", name: "PS5" }, { value: "ps4", name: "PS4" }],
|
|
has: (game, value) => game.platform === value
|
|
},
|
|
// A source the app downloads by itself, with no CAPTCHA page to pass in a browser.
|
|
{ key: "access", options: [{ value: "direct", label: "filters.access.direct" }], has: (game) => Boolean(game.direct) },
|
|
{
|
|
key: "genre",
|
|
options: named(byGames(genres, (game, genre) => game.genres.includes(genre)), "genre"),
|
|
has: (game, value) => game.genres.includes(value)
|
|
},
|
|
{ key: "feature", options: named(features, "feature"), has: (game, value) => game.features.includes(value) },
|
|
{
|
|
key: "release",
|
|
any: true,
|
|
options: RELEASES,
|
|
has: (game, value) => game.year != null && RELEASES.find((release) => release.value === value).test(game.year)
|
|
},
|
|
{
|
|
key: "size",
|
|
any: true,
|
|
options: SIZES,
|
|
has: (game, value) => game.sizeGb != null && SIZES.find((size) => size.value === value).test(game.sizeGb)
|
|
},
|
|
{ key: "provider", any: true, options: hosts, has: (game, value) => game.providers?.includes(value) }
|
|
];
|
|
var facetTitle = (key) => t(`filters.facet.${key}`);
|
|
function optionLabel(option) {
|
|
if (!option.label) return option.name;
|
|
const text = t(option.label);
|
|
return text === option.label ? option.name : text;
|
|
}
|
|
var facetLabel = (key, value) => {
|
|
const option = FACETS.find((facet) => facet.key === key).options.find((candidate) => candidate.value === value);
|
|
return option ? optionLabel(option) : value;
|
|
};
|
|
var genreLabel = (genre) => optionLabel({ label: `genre.${slug(genre)}`, name: genre });
|
|
var featureLabel = (feature) => optionLabel({ label: `feature.${slug(feature)}`, name: feature });
|
|
var NO_FILTERS = { ...Object.fromEntries(FACETS.map((facet) => [facet.key, []])), wishlist: false };
|
|
var activeCount = (filters) => FACETS.reduce((sum, facet) => sum + filters[facet.key].length, Number(filters.wishlist));
|
|
var toggleValue = (list, value) => list.includes(value) ? list.filter((item) => item !== value) : [...list, value];
|
|
var passes = (game, facet, picked) => !picked.length || (facet.any ? picked.some((value) => facet.has(game, value)) : picked.every((value) => facet.has(game, value)));
|
|
function matching({ filters, query, wishlist }, skip) {
|
|
const q = query.trim().toLowerCase();
|
|
return games.filter((game) => (!q || `${game.title} ${game.developer} ${game.publisher}`.toLowerCase().includes(q)) && FACETS.every((facet) => facet === skip || passes(game, facet, filters[facet.key])) && (!filters.wishlist || wishlist.includes(game.id)));
|
|
}
|
|
var filterGames = (state) => matching(state);
|
|
function facetCounts(state, facet) {
|
|
const pool = matching(state, facet.any ? facet : void 0);
|
|
return Object.fromEntries(facet.options.map(({ value }) => [value, pool.filter((game) => facet.has(game, value)).length]));
|
|
}
|
|
var summaryOf = (game) => game.about.find((text) => text.length > 80 && text !== text.toUpperCase()) ?? game.shortDescription;
|
|
function relatedGames(game, limit = 6) {
|
|
const shared = (other) => other.genres.filter((genre) => game.genres.includes(genre)).length;
|
|
return games.filter((other) => other.id !== game.id && shared(other) > 0).sort((a, b) => shared(b) - shared(a) || b.recommendations - a.recommendations).slice(0, limit);
|
|
}
|
|
var formatDate3 = (iso) => iso ? formatDate2(iso) : "\u2014";
|
|
function formatCount(n) {
|
|
const short = (value) => formatNumber2(value, { maximumFractionDigits: 1 });
|
|
if (n >= 1e6) return t("units.millions", { n: short(n / 1e6) });
|
|
if (n >= 1e3) return t("units.thousands", { n: short(n / 1e3) });
|
|
return formatNumber2(n);
|
|
}
|
|
|
|
export { CDN_HOST, FACETS, NO_FILTERS, SORTS, activeCount, editionsOf, facetCounts, facetLabel, facetTitle, featureLabel, filterGames, formatCount, formatDate3, fromArchive, genreLabel, genres, getGame, listedId, onCdn, optionLabel, relatedGames, showcase, sortGames, sortLabel, summaryOf, toggleValue, warmCovers };
|