diff --git a/scripts/catalog-sync.sh b/scripts/catalog-sync.sh index 7f2b0cce..ad385852 100755 --- a/scripts/catalog-sync.sh +++ b/scripts/catalog-sync.sh @@ -117,6 +117,21 @@ ACTIONS=(audit) CONFIRM="" # Empty for the audit-only branch on purpose: no repair flags are passed. ACTION_FLAGS=() +# Prints a warning when the chosen source only serves furnidata (no .nitro +# bundles), so the import step would find nothing to download. +warn_compare_only_sources() { + case "$SOURCE_LABEL" in + "Habbo (COM)" | "Habbo (NL)" | "Habbo (DE)" | "Habbo (FR)" | \ + "Habbo (ES)" | "Habbo (FI)" | "Habbo (BR)" | "Habbo (TR)" | \ + "Habbo (IT - GitHub mirror)" | "YabboHotel" | "Wibbo" | "Hubba.cc") + echo + echo "Note: ${SOURCE_LABEL} only serves furnidata — there are no .nitro" + echo "bundles to import, so the import step can only COMPARE." + echo "To actually import furniture, pick a source that serves bundles" + echo "(Habblet City, VirtualCity, SodaStudios, Habboon, Leet, Hubbly) or 'a'." + ;; + esac +} case "$ACTION_CHOICE" in 1) ACTIONS=(audit "[nothing written]") @@ -179,6 +194,9 @@ esac echo printf 'Running: %s against %s\n' "${ACTIONS[1]}" "$SOURCE_LABEL" +case "$ACTION_FLAGS" in + *--import-new*) warn_compare_only_sources ;; +esac if [ -n "$CONFIRM" ]; then read -r -p "$CONFIRM" REPLY if [ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ]; then diff --git a/scripts/catalog-sync.test.ts b/scripts/catalog-sync.test.ts index 827ac8e1..bb5c5ab2 100644 --- a/scripts/catalog-sync.test.ts +++ b/scripts/catalog-sync.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { + canImportBundles, DEFAULT_CATALOG_SYNC_FLAGS, describeActions, parseCatalogSyncArgs, @@ -131,6 +132,36 @@ describe("catalog-sync import selection", () => { }); }); +describe("catalog-sync import feasibility", () => { + it("imports from a source that serves bundles", () => { + expect( + canImportBundles({ + id: "default-habblet-city", + name: "Habblet City", + furnidataUrl: "https://www.habblet.city/gamedata/furnidata.json", + nitroBaseUrl: "https://www.habblet.city/nitro/bundled/furniture", + iconBaseUrl: "", + hotel: "com", + } as never), + ).toBe(true); + }); + + // The Habbo presets have no nitro/icon base URL: their furnidata can be + // compared, but there is nothing to download from. + it("does not import from a compare-only source", () => { + expect( + canImportBundles({ + id: "default-habbo-com", + name: "Habbo (COM)", + furnidataUrl: "https://www.habbo.com/gamedata/furnidata_json/1", + nitroBaseUrl: "", + iconBaseUrl: "", + hotel: "com", + } as never), + ).toBe(false); + }); +}); + describe("catalog-sync step composition", () => { it("--everything turns on every step except the writing ones", () => { const flags = parseCatalogSyncArgs(["--everything"]); diff --git a/scripts/catalog-sync.ts b/scripts/catalog-sync.ts index b3cd9ab5..d040a9f9 100644 --- a/scripts/catalog-sync.ts +++ b/scripts/catalog-sync.ts @@ -83,6 +83,22 @@ export interface ImportOutcome { skipped: number; failed: number; failures: Array<{ classname: string; error: string }>; + /** Sources that were skipped because they cannot serve bundles. */ + skippedSources: string[]; +} + +/** + * Whether a source can actually be imported from. + * + * The clone import downloads `${nitroBaseUrl}/${classname}.nitro` and gives up + * when that is not reachable. Habbo's own entries carry no nitro/icon base URLs + * (their furnidata is served, their assets are not), so they are compare-only: + * perfect for finding out what this hotel lacks, useless for actually fetching + * it. The SWF-based path that works against `images.habbo.com` lives in + * `furni-import.ts`, not in the clone importer this CLI uses. + */ +export function canImportBundles(source: SourceRef): boolean { + return Boolean(source.nitroBaseUrl?.trim()); } export interface CatalogSyncFlags { @@ -420,8 +436,16 @@ async function stepImportNew( itemType: string; }> = []; let lastSource = ""; + const skippedSources: string[] = []; for (const source of sources) { + if (!canImportBundles(source)) { + log( + `! ${source.name} serves no .nitro bundles — it can be compared against, but nothing can be imported from it`, + ); + skippedSources.push(source.name); + continue; + } if (flags.importLimit > 0 && planned >= flags.importLimit) break; const remaining = flags.importLimit ? flags.importLimit - planned @@ -515,6 +539,7 @@ async function stepImportNew( skipped, failed, failures: failures.slice(0, 25), + skippedSources, }; } @@ -736,12 +761,12 @@ function printSummary(report: AuditReport, started: number): void { console.log( ` imported from source ${outcome.imported} new (${outcome.skipped} already present, ${outcome.failed} failed)`, ); + if (outcome.skippedSources.length > 0) + console.log( + ` compared only ${outcome.skippedSources.join(", ")} (no .nitro bundles to import from)`, + ); for (const failure of outcome.failures) console.log(` ${failure.classname}: ${failure.error}`); - if (outcome.failures.length > 0) - console.log( - ` import failures are per-item: a failed download never aborts the run.`, - ); } if (report.missingFromSources.length > 0) {