fix(catalog): check source assets and offer reachable import alternatives
CI / check (push) Failing after 19s
CI / deploy (push) Skipped
CI / e2e (push) Skipped

This commit is contained in:
Simo committed 2026-09-05 14:56:04 +02:00
1 parent 8c1efae296
commit 159b1f7d1b
10 files changed
+646 -56

No files matched your search

@@ -0,0 +1,98 @@
import type {
AssetAvailability,
SourceAssetCheck,
SourceAssetItem,
} from "@/lib/furni/source-assets";
import { relatedFurniture } from "@/lib/furni/source-assets";
import type { CloneSource } from "./clone-sources";
import { browserHeaders } from "./import/core/browser-headers";
const cache = new Map<string, { expires: number; state: AssetAvailability }>();
export async function probeAsset(url: string): Promise<AssetAvailability> {
const cached = cache.get(url);
if (cached && cached.expires > Date.now()) return cached.state;
let state: AssetAvailability = "unknown";
try {
const response = await fetch(url, {
method: "HEAD",
signal: AbortSignal.timeout(6000),
headers: browserHeaders(),
});
if (
response.ok &&
!response.headers.get("content-type")?.includes("text/html")
)
state = "available";
else if (response.status === 404 || response.status === 410)
state = "missing";
} catch {
/* A network problem is not proof the asset is absent. */
}
if (cache.size >= 2000) cache.clear();
cache.set(url, { expires: Date.now() + 60000, state });
return state;
}
export function furnitureAssetUrls(
item: SourceAssetItem,
source?: CloneSource | null,
) {
const base = encodeURIComponent(item.classname.split("*")[0]);
const urls: string[] = [];
if (source?.nitroBaseUrl)
urls.push(`${source.nitroBaseUrl.replace(/\/+$/, "")}/${base}.nitro`);
if (source?.sourceSwfBaseUrl) {
const root = source.sourceSwfBaseUrl.replace(/\/+$/, "");
urls.push(
`${root}/hof_furni/${item.revision}/${base}.swf`,
`${root}/hof_furni/${base}.swf`,
);
}
urls.push(
`https://images.habbo.com/dcr/hof_furni/${item.revision}/${base}.swf`,
);
return urls;
}
async function available(
item: SourceAssetItem,
source?: CloneSource | null,
): Promise<AssetAvailability> {
const states = await Promise.all(
furnitureAssetUrls(item, source).map(probeAsset),
);
return states.includes("available")
? "available"
: states.every((state) => state === "missing")
? "missing"
: "unknown";
}
export async function inspectSourceAssets(
item: SourceAssetItem,
official: SourceAssetItem[],
source?: CloneSource | null,
): Promise<SourceAssetCheck> {
const canonical = official.find(
(entry) => entry.classname === item.classname,
);
const effective = {
...item,
revision: item.revision || canonical?.revision || 0,
};
const state = await available(effective, source);
const alternatives: SourceAssetItem[] = [];
if (state === "missing") {
for (const candidate of relatedFurniture(
{ ...effective, name: canonical?.name || item.name },
official,
)) {
if ((await available(candidate)) === "available")
alternatives.push(candidate);
if (alternatives.length === 3) break;
}
}
return {
classname: item.classname,
revision: effective.revision,
state,
alternatives,
};
}