From d34d44381bf8f7adec69649dc1d6488a782b5b95 Mon Sep 17 00:00:00 2001 From: koby455 Date: Fri, 2 Oct 2026 14:11:00 +0300 Subject: [PATCH 1/4] Add a free setup page that moves a Nuvio account into ARVIO The web app is behind a subscription, but the Android and TV apps are not, so a user coming from Nuvio had no way to carry their setup over without paying for an interface they did not want. /migrate is a standalone page outside the subscribed shell. It reads a Nuvio account, installs addons by manifest URL, and copies profiles, addons and collections into an ARVIO account, which the free apps then sync. Nuvio keeps its data; nothing there is changed. - The public Nuvio cloud publishes /.well-known/nuvio, so the page needs no server address and no embedded key: it reads the publishable key at connect time, which also survives a key rotation. Self-hosters can name their own server behind a link. - Reading Nuvio needs no ARVIO account, so the connect step comes first and the result can be downloaded as JSON. Signing in to ARVIO is only required to write. - Row-level security can leave a direct table read empty, so collections and home catalog settings fall back to the sync_pull_* RPCs, and an account that exposes no profiles table still migrates as profile 1. - Profiles are matched by name and created when missing, so a multi-profile account arrives whole and a repeated import does not duplicate anything. A Nuvio PIN hash is not portable, so a copied profile starts unlocked. - Addon manifests are resolved one by one; an unreachable addon is reported in the summary instead of aborting the run. - Collections reuse the existing Nuvio collections parser. Plugins are counted and reported as staying behind, since the web app has no plugins. - The password is used once for the token exchange and never stored. - English by default, with a language picker saved per device. --- .../screenshots/02-review.png | Bin 0 -> 84260 bytes .../screenshots/03-finished.png | Bin 0 -> 65818 bytes web/app/migrate/page.tsx | 16 + web/app/migrate/setup.css | 215 ++++++++++ web/components/migrate/MigrateApp.tsx | 324 ++++++++++++++ web/lib/nuvioImportRunner.ts | 101 +++++ web/lib/nuvioMigration.ts | 403 ++++++++++++++++++ web/tests/nuvio-import-runner.test.cjs | 114 +++++ web/tests/nuvio-migration.test.cjs | 207 +++++++++ 9 files changed, 1380 insertions(+) create mode 100644 docs/web-nuvio-migration/screenshots/02-review.png create mode 100644 docs/web-nuvio-migration/screenshots/03-finished.png create mode 100644 web/app/migrate/page.tsx create mode 100644 web/app/migrate/setup.css create mode 100644 web/components/migrate/MigrateApp.tsx create mode 100644 web/lib/nuvioImportRunner.ts create mode 100644 web/lib/nuvioMigration.ts create mode 100644 web/tests/nuvio-import-runner.test.cjs create mode 100644 web/tests/nuvio-migration.test.cjs diff --git a/docs/web-nuvio-migration/screenshots/02-review.png b/docs/web-nuvio-migration/screenshots/02-review.png new file mode 100644 index 0000000000000000000000000000000000000000..d8a1cc4cb8ee2db738ca1054295eacd8f2f15312 GIT binary patch literal 84260 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 Doex_1 literal 0 HcmV?d00001 diff --git a/docs/web-nuvio-migration/screenshots/03-finished.png b/docs/web-nuvio-migration/screenshots/03-finished.png new file mode 100644 index 0000000000000000000000000000000000000000..ccd5a9937a82e930c8652f41a8248ee1433b0091 GIT binary patch literal 65818 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
9K8J;IE{kt8;A` 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 +} diff --git a/web/app/migrate/setup.css b/web/app/migrate/setup.css new file mode 100644 index 000000000..c882530de --- /dev/null +++ b/web/app/migrate/setup.css @@ -0,0 +1,215 @@ +/* Standalone setup page. No app shell, so it carries its own layout, but every + colour, radius and control shape comes from the app's tokens in globals.css + so it reads as the same product. */ + +.setup { + min-height: 100vh; + background: + radial-gradient(1100px 520px at 50% -8%, rgba(0, 213, 136, 0.1), transparent 70%), + radial-gradient(760px 420px at 12% 8%, rgba(255, 205, 60, 0.05), transparent 65%), + var(--bg); +} + +.setup__inner { + max-width: 760px; + margin: 0 auto; + padding: 56px 24px 96px; + display: flex; + flex-direction: column; + gap: 18px; +} + +.setup__brand { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; } + +.setup__brand img { width: 40px; height: 40px; border-radius: 10px; } + +.setup__brand span { + font-size: 22px; + font-weight: 700; + letter-spacing: 0.28em; + text-transform: uppercase; +} + +.setup__header-row { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 18px; + flex-wrap: wrap; +} + +.setup__header h1 { + margin: 0 0 10px; + font-size: clamp(30px, 4vw, 40px); + line-height: 1.1; + font-weight: 700; +} + +.setup__header p { + margin: 0; + color: var(--muted); + line-height: 1.6; + max-width: 62ch; +} + +.setup__language { display: grid; gap: 6px; min-width: 190px; } + +.setup__language span { color: var(--soft); font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; } + +/* Same card geometry as .settings-panel-card in the app. */ +.setup__card { + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.04); + border-radius: 18px; + padding: 24px; + display: flex; + flex-direction: column; + gap: 16px; + box-shadow: var(--shadow); +} + +.setup__card h2 { margin: 0; font-size: 22px; font-weight: 650; } + +.setup label { display: grid; gap: 7px; font-size: 14px; } + +.setup label > span { font-weight: 600; color: var(--text); } + +.setup input, .setup select { + background: var(--bg-card); + border: 1px solid var(--line); + border-radius: 12px; + color: var(--text); + padding: 12px 14px; + font-size: 15px; + min-height: 48px; + width: 100%; +} + +.setup input::placeholder { color: var(--soft); } + +.setup input:focus-visible, .setup select:focus-visible, .setup button:focus-visible { + outline: none; + border-color: var(--green); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 30%, transparent); +} + +.setup small { + display: flex; + align-items: center; + gap: 6px; + color: var(--soft); + font-size: 12px; + line-height: 1.5; +} + +.setup__row { display: flex; gap: 12px; flex-wrap: wrap; align-items: stretch; } + +.setup__row input { flex: 1 1 260px; } + +.setup__primary, .setup__ghost, .setup__icon { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 12px; + cursor: pointer; + font-size: 15px; + font-weight: 600; + min-height: 48px; + padding: 0 20px; + transition: transform 120ms ease, filter 120ms ease; +} + +.setup__primary { background: var(--text); color: #000; border: 1px solid transparent; } + +.setup__primary:hover:not(:disabled) { transform: translateY(-1px); filter: brightness(1.05); } + +.setup__primary:disabled { opacity: 0.4; cursor: not-allowed; } + +.setup__ghost { background: var(--chip); color: var(--text); border: 1px solid var(--line); } + +.setup__icon { + background: var(--chip); + color: var(--text); + border: 1px solid var(--line); + min-height: 38px; + padding: 0 11px; +} + +.setup__icon:hover { background: var(--chip-focus); } + +.setup__profiles { display: flex; gap: 10px; flex-wrap: wrap; } + +.setup__profile { + background: var(--chip); + border: 1px solid var(--line); + border-radius: 999px; + color: var(--text); + cursor: pointer; + font-size: 14px; + min-height: 42px; + padding: 0 18px; +} + +.setup__profile.is-active { background: var(--text); color: #000; border-color: var(--text); } + +.setup__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; } + +.setup__list li { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; + background: var(--bg-card); + border: 1px solid var(--line); + border-radius: 14px; + padding: 14px 16px; + font-size: 14px; +} + +.setup__list li > span { display: grid; gap: 4px; } + +.setup__list li em { color: var(--soft); font-style: normal; font-size: 13px; } + +.setup__list select { width: auto; min-width: 230px; } + +.setup__results > p:first-child { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; color: var(--green); } + +.setup__muted { color: var(--soft); font-size: 13px; margin: 0; line-height: 1.55; } + +.setup__progress { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 14px; } + +.setup__notice { color: var(--green); font-size: 14px; } + +.setup__error { color: var(--red); font-size: 14px; } + +.setup .spin { animation: setup-spin 1s linear infinite; } + +@keyframes setup-spin { to { transform: rotate(360deg); } } + +@media (prefers-reduced-motion: reduce) { + .setup .spin { animation: none; } + .setup__primary:hover:not(:disabled) { transform: none; } +} + +@media (max-width: 560px) { + .setup__inner { padding: 32px 16px 72px; } + .setup__card { padding: 18px; border-radius: 16px; } + .setup__list li { flex-direction: column; align-items: stretch; } + .setup__list select, .setup__row .setup__primary { width: 100%; } +} + +.setup__link { + align-self: flex-start; + background: none; + border: none; + color: var(--muted); + cursor: pointer; + font-size: 13px; + padding: 4px 0; + text-decoration: underline; + text-underline-offset: 3px; +} + +.setup__link:hover { color: var(--text); } diff --git a/web/components/migrate/MigrateApp.tsx b/web/components/migrate/MigrateApp.tsx new file mode 100644 index 000000000..8d83b9850 --- /dev/null +++ b/web/components/migrate/MigrateApp.tsx @@ -0,0 +1,324 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { ArrowRightLeft, Check, Download, Loader2, LogIn, Plus, ShieldCheck, Trash2 } from "lucide-react"; +import { authClient } from "@/lib/store"; +import { installAddon, normalizeAddons } from "@/lib/addons"; +import { pullCloudPayload, pullCloudProfiles, saveCloudAddons } from "@/lib/cloud"; +import { applyNuvioImport } from "@/lib/nuvioImportRunner"; +import { + defaultChoices, discoverNuvioBackend, fetchNuvioSnapshot, signInToNuvio, summarizePlan, OFFICIAL_NUVIO_URL, + type MigrationChoice, type NuvioSnapshot, type ProfileImportSummary +} from "@/lib/nuvioMigration"; +import { defaultSettings } from "@/lib/store"; +import { LanguageProvider, useTranslation } from "@/lib/i18n"; +import { CONTENT_LANGUAGE_OPTIONS } from "@/lib/i18n/languageOptions"; +import { loadStored, saveStored } from "@/lib/storage"; +import type { InstalledAddon, Profile } from "@/lib/types"; + +const LANGUAGE_KEY = "arvio.web.setupLanguage"; + +/** + * Free setup page for ARVIO accounts, outside the web app's subscription: sign + * in, install addons, and bring a Nuvio account over. Everything is written to + * the ARVIO cloud account, which the Android and TV apps sync for free. + */ +export function MigrateApp() { + // English by default; the choice is remembered on this device. + const [language, setLanguage] = useState("en-US"); + // The language list is built with Intl.DisplayNames, which resolves names + // from the host's locale data — the server's and the browser's disagree, so + // the picker is rendered after mount instead of being hydrated. + const [mounted, setMounted] = useState(false); + useEffect(() => { + setLanguage(loadStored(LANGUAGE_KEY, "en-US")); + setMounted(true); + }, []); + const chooseLanguage = (next: string) => { setLanguage(next); saveStored(LANGUAGE_KEY, next); }; + return + + ; +} + +function SetupTools({ language, onLanguageChange, showLanguagePicker }: { + language: string; + onLanguageChange: (value: string) => void; + showLanguagePicker: boolean; +}) { + // Named `text`, not `translateUi`, so this standalone page reuses the shared + // dictionaries without enrolling its own wording in the translated corpus. + const text = useTranslation(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [signedIn, setSignedIn] = useState(false); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [notice, setNotice] = useState(null); + + const [profiles, setProfiles] = useState([]); + const [profileId, setProfileId] = useState(null); + const [addons, setAddons] = useState([]); + const [addonUrl, setAddonUrl] = useState(""); + + const [nuvioServer, setNuvioServer] = useState(""); + const [selfHosted, setSelfHosted] = useState(false); + const [nuvioEmail, setNuvioEmail] = useState(""); + const [nuvioPassword, setNuvioPassword] = useState(""); + const [snapshot, setSnapshot] = useState(null); + const [choices, setChoices] = useState([]); + const [results, setResults] = useState(null); + const [progress, setProgress] = useState(""); + + const loadAccount = useCallback(async (preferredProfileId?: string | null) => { + const cloud = await pullCloudProfiles(authClient); + setProfiles(cloud.profiles); + const nextId = preferredProfileId ?? cloud.activeProfileId ?? cloud.profiles[0]?.id ?? null; + setProfileId(nextId); + if (nextId) { + const payload = await pullCloudPayload(authClient, nextId).catch(() => null); + setAddons(normalizeAddons(payload?.addons ?? [])); + } + }, []); + + useEffect(() => { + if (!authClient.session) return; + setSignedIn(true); + setEmail(authClient.session.email); + void loadAccount().catch(() => setError("Could not read your ARVIO account")); + }, [loadAccount]); + + const run = async (work: () => Promise) => { + setBusy(true); + setError(null); + try { + await work(); + } catch (failure) { + setError(failure instanceof Error ? failure.message : "Something went wrong"); + } finally { + setBusy(false); + setProgress(""); + } + }; + + const signIn = () => run(async () => { + await authClient.signIn(email.trim(), password); + setPassword(""); + setSignedIn(true); + await loadAccount(); + }); + + const selectProfile = (id: string) => run(async () => { + setProfileId(id); + const payload = await pullCloudPayload(authClient, id).catch(() => null); + setAddons(normalizeAddons(payload?.addons ?? [])); + }); + + const addAddon = () => run(async () => { + if (!profileId) throw new Error("Choose a profile first"); + const installed = await installAddon(addonUrl.trim()); + const next = normalizeAddons([...addons, installed]); + await saveCloudAddons(authClient, next, profileId, { changes: undefined }); + setAddons(next); + setAddonUrl(""); + setNotice(`Installed ${installed.name}`); + }); + + const removeAddon = (addon: InstalledAddon) => run(async () => { + if (!profileId) return; + const next = addons.filter(entry => entry.id !== addon.id); + await saveCloudAddons(authClient, next, profileId, { removedIds: [addon.id] }); + setAddons(next); + }); + + /** Keeps a copy of what Nuvio returned, for people without an ARVIO account. */ + const downloadSnapshot = () => { + if (!snapshot) return; + const blob = new Blob([JSON.stringify(snapshot, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "nuvio-setup.json"; + link.click(); + URL.revokeObjectURL(url); + }; + + const connectNuvio = () => run(async () => { + // The normal case is the public Nuvio cloud, which publishes its own + // settings; only a self-hoster has to name a server. + const discovery = await discoverNuvioBackend(selfHosted ? nuvioServer : OFFICIAL_NUVIO_URL); + const session = await signInToNuvio(discovery, nuvioEmail, nuvioPassword); + const pulled = await fetchNuvioSnapshot(discovery, session); + setNuvioPassword(""); + setSnapshot(pulled); + setChoices(defaultChoices(pulled, profiles)); + setResults(null); + }); + + const setTarget = (nuvioProfileId: number, value: string) => { + setChoices(current => current.map(choice => choice.nuvioProfileId !== nuvioProfileId ? choice : { + ...choice, + target: value === "__skip" ? { kind: "skip" } : value === "__create" ? { kind: "create" } : { kind: "existing", profileId: value } + })); + }; + + const runImport = () => run(async () => { + if (!snapshot) return; + const outcome = await applyNuvioImport({ + auth: authClient, profiles, snapshot, choices, + baseSettings: defaultSettings, onProgress: setProgress + }); + setResults(outcome.summaries); + await loadAccount(profileId); + }); + + const planned = snapshot ? summarizePlan(snapshot, choices) : []; + + return ( +
+
+
+ + ARVIO +
+
+
+

{text("ARVIO setup")}

+ {showLanguagePicker && } +
+

Install addons and bring your Nuvio setup over. Everything is saved to your ARVIO account, + so the free Android and TV apps pick it up on their next sync — no web subscription needed.

+
+ + {/* Nuvio comes first: reading a Nuvio account needs no ARVIO account, so + someone who has not signed up yet can still pull their setup out. */} +
+

Move from Nuvio

+ {!snapshot && <> +

Sign in with your Nuvio account to read its profiles, addons and collections. + Nothing in Nuvio is changed, and you do not need an ARVIO account for this step.

+ + + {selfHosted + ? + : } + + } + + {snapshot && !results && <> +

Found {snapshot.profiles.length} profiles in {snapshot.email}.

+
    + {snapshot.profiles.map(profile => { + const choice = choices.find(entry => entry.nuvioProfileId === profile.profileId); + const value = choice?.target.kind === "existing" ? choice.target.profileId + : choice?.target.kind === "skip" ? "__skip" : "__create"; + const collections = Array.isArray(profile.collectionsJson) ? profile.collectionsJson.length : 0; + return
  • + + {profile.name} + {profile.addons.length} addons · {collections} collections + {profile.plugins.length > 0 && ` · ${profile.plugins.length} plugins stay in Nuvio`} + + +
  • ; + })} +
+
+ + +
+ {!signedIn &&

Sign in to ARVIO below to copy this into an account, or download it and keep it for later.

} + } + + {results &&
+

+
    + {results.map(result =>
  • + {result.profileName} + {result.addons} addons · {result.collections} collections{result.created ? " · new profile" : ""} + {result.addonsFailed > 0 && ` · ${result.addonsFailed} could not be reached`} +
  • )} +
+

Open ARVIO on your TV or phone and sign in with this account to see them.

+
} +
+ + {!signedIn &&
+

Sign in to ARVIO

+

Needed to save addons, or to copy a Nuvio account into ARVIO.

+ + + +
} + + {signedIn && <> +
+

Profile

+ {profiles.length === 0 &&

This account has no profiles yet. Importing from Nuvio will create them.

} +
+ {profiles.map(profile => )} +
+
+ +
+

Addons

+

Paste a Stremio addon manifest URL to install it for this profile.

+
+ setAddonUrl(event.target.value)} + placeholder="https://example.com/manifest.json" spellCheck={false} /> + +
+
    + {addons.map(addon =>
  • + {addon.name} + +
  • )} + {addons.length === 0 &&
  • No addons on this profile yet.
  • } +
+
+ + } + + {busy && progress &&

} + {error &&

{error}

} + {notice && !error &&

{notice}

} +
+
+ ); +} diff --git a/web/lib/nuvioImportRunner.ts b/web/lib/nuvioImportRunner.ts new file mode 100644 index 000000000..19117f7e2 --- /dev/null +++ b/web/lib/nuvioImportRunner.ts @@ -0,0 +1,101 @@ +import { installAddon, normalizeAddons } from "./addons"; +import type { AuthClient } from "./auth"; +import { defaultCatalogs } from "./catalogs"; +import { pullCloudPayload, saveCloudAddons, saveCloudProfiles, saveCloudSettings } from "./cloud"; +import { mergeImportedCollections, parseCustomCollections } from "./customCollections"; +import { + applyHomeCatalogSettings, mergeAddonUrls, newProfileFrom, + type MigrationChoice, type NuvioSnapshot, type ProfileImportSummary +} from "./nuvioMigration"; +import { profileColors } from "./profiles"; +import type { AppSettings, InstalledAddon, Profile } from "./types"; + +/** + * Writes a Nuvio snapshot into an ARVIO account. + * + * Everything goes through the cloud payload, which is what the free Android and + * TV apps sync from — so a user who never pays for the web interface still ends + * up with their addons, collections and profiles on their devices. + */ +export async function applyNuvioImport(options: { + auth: AuthClient; + profiles: Profile[]; + snapshot: NuvioSnapshot; + choices: MigrationChoice[]; + baseSettings: AppSettings; + onProgress?: (message: string) => void; +}): Promise<{ summaries: ProfileImportSummary[]; profiles: Profile[] }> { + const { auth, snapshot, choices, baseSettings, onProgress } = options; + if (!auth.session) throw new Error("Sign in to your ARVIO account first"); + + let profiles = [...options.profiles]; + const targets = new Map(); + + // Profiles first: a Nuvio profile with no ARVIO counterpart is created, so a + // multi-profile Nuvio account arrives complete instead of collapsing into one. + for (const profile of snapshot.profiles) { + const choice = choices.find(entry => entry.nuvioProfileId === profile.profileId); + if (!choice || choice.target.kind === "skip") continue; + if (choice.target.kind === "existing") { + targets.set(profile.profileId, { id: choice.target.profileId, created: false }); + continue; + } + onProgress?.(`Creating profile ${profile.name}`); + const id = globalThis.crypto?.randomUUID?.() ?? `p_${Date.now()}_${Math.floor(Math.random() * 1e6)}`; + const color = profileColors[profiles.length % profileColors.length]; + profiles = [...profiles, newProfileFrom(profile, id, color)]; + targets.set(profile.profileId, { id, created: true }); + } + if (!targets.size) return { summaries: [], profiles }; + await saveCloudProfiles(auth, profiles, profiles[0]?.id ?? null); + + const summaries: ProfileImportSummary[] = []; + for (const profile of snapshot.profiles) { + const target = targets.get(profile.profileId); + if (!target) continue; + const summary: ProfileImportSummary = { + profileName: profile.name, created: target.created, addons: 0, addonsFailed: 0, + plugins: profile.plugins.length, collections: 0, catalogSettings: false + }; + + onProgress?.(`${profile.name}: reading what is already there`); + const existing = target.created ? null : await pullCloudPayload(auth, target.id).catch(() => null); + const currentAddons = normalizeAddons(existing?.addons ?? []); + const currentCatalogs = existing?.settings?.catalogs?.length ? existing.settings.catalogs : defaultCatalogs; + + // Addons are stored as resolved manifests, so each new URL is fetched once. + const resolved: InstalledAddon[] = []; + for (const url of mergeAddonUrls(currentAddons.map(addon => addon.manifestUrl ?? addon.id), profile.addons)) { + onProgress?.(`${profile.name}: installing ${url}`); + try { + resolved.push(await installAddon(url)); + summary.addons += 1; + } catch { + // An addon whose server is down must not abort the rest of the import. + summary.addonsFailed += 1; + } + } + const nextAddons = normalizeAddons([...currentAddons, ...resolved]); + + let nextCatalogs = currentCatalogs; + if (Array.isArray(profile.collectionsJson) && profile.collectionsJson.length) { + onProgress?.(`${profile.name}: importing collections`); + try { + const imported = await parseCustomCollections(JSON.stringify(profile.collectionsJson)); + nextCatalogs = mergeImportedCollections(nextCatalogs, imported); + summary.collections = profile.collectionsJson.length; + } catch { + summary.collections = 0; + } + } + const orderedCatalogs = applyHomeCatalogSettings(nextCatalogs, profile.homeCatalogSettings); + summary.catalogSettings = orderedCatalogs !== nextCatalogs; + + onProgress?.(`${profile.name}: saving to your ARVIO account`); + const settings: AppSettings = { ...baseSettings, ...(existing?.settings ?? {}), catalogs: orderedCatalogs }; + if (resolved.length) await saveCloudAddons(auth, nextAddons, target.id, { changes: undefined }); + await saveCloudSettings(auth, settings, nextAddons, target.id, profiles, null); + summaries.push(summary); + } + return { summaries, profiles }; +} diff --git a/web/lib/nuvioMigration.ts b/web/lib/nuvioMigration.ts new file mode 100644 index 000000000..e536646e0 --- /dev/null +++ b/web/lib/nuvioMigration.ts @@ -0,0 +1,403 @@ +import type { CatalogConfig, Profile } from "./types"; + +/** + * Migration from a Nuvio account into ARVIO. + * + * Nuvio stores a user's setup server side (Supabase): `profiles`, `addons`, + * `plugins`, `collections` and `home_catalog_settings`, all keyed by an integer + * `profile_id` (1-6). Nuvio Web keeps nothing importable in the browser, so the + * only way to carry a setup over is to read it from the user's own account. + * + * Deployments publish their connection settings at `/.well-known/nuvio`, which + * is how a self-hosted server hands clients its publishable key. Nothing here is + * hard-coded: the user names their server and signs in with their own account, + * and the access token lives in memory for the length of the import only. + */ + +export interface NuvioDiscovery { + backendUrl: string; + publishableKey: string; + selfHosted: boolean; + emailPasswordAuth: boolean; +} + +export interface NuvioSession { + accessToken: string; + userId: string; + email: string; +} + +/** One Nuvio profile plus everything that belongs to it. */ +export interface NuvioProfileData { + profileId: number; + name: string; + avatarColorHex: string; + avatarId: string | null; + pinEnabled: boolean; + addons: NuvioAddon[]; + plugins: NuvioPlugin[]; + collectionsJson: unknown; + homeCatalogSettings: unknown; +} + +export interface NuvioAddon { + url: string; + name: string | null; + enabled: boolean; + sortOrder: number; +} + +export interface NuvioPlugin extends NuvioAddon { + repoType: string | null; +} + +export interface NuvioSnapshot { + email: string; + profiles: NuvioProfileData[]; +} + +const trimSlash = (value: string) => value.trim().replace(/\/+$/, ""); + +/** Accepts "example.com", "https://example.com" or a full well-known URL. */ +export function normalizeBackendUrl(raw: string): string { + const input = raw.trim(); + if (!input) throw new Error("Enter your Nuvio server address"); + // A local server is reached over plain HTTP, so a scheme-less "localhost:8000" + // must not be turned into an https URL nothing is listening on. + const isLocal = /^(localhost|127\.0\.0\.1)(:|\/|$)/i.test(input); + const withScheme = /^https?:\/\//i.test(input) ? input : `${isLocal ? "http" : "https"}://${input}`; + const url = new URL(withScheme); + if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") { + throw new Error("The Nuvio server address must use HTTPS"); + } + return trimSlash(url.origin + url.pathname.replace(/\/\.well-known\/nuvio\/?$/, "")); +} + +export function parseDiscovery(raw: unknown, fallbackUrl: string): NuvioDiscovery { + const doc = (raw && typeof raw === "object" ? raw : {}) as Record; + const publishableKey = typeof doc.publishable_key === "string" ? doc.publishable_key.trim() : ""; + if (!publishableKey) throw new Error("That server did not return a Nuvio publishable key"); + const capabilities = (doc.capabilities && typeof doc.capabilities === "object" ? doc.capabilities : {}) as Record; + return { + backendUrl: typeof doc.backend_url === "string" && doc.backend_url.trim() ? trimSlash(doc.backend_url) : fallbackUrl, + publishableKey, + selfHosted: doc.self_hosted === true, + // Older deployments omit the capability block; assume the standard login works. + emailPasswordAuth: capabilities.email_password_auth !== false + }; +} + +/** + * The public Nuvio cloud. It publishes its own `/.well-known/nuvio`, so the + * page only needs the address: the publishable key is read at connect time and + * nothing has to be embedded here (and a rotated key keeps working). + */ +export const OFFICIAL_NUVIO_URL = "https://api.nuvio.tv"; + +type Fetcher = typeof fetch; + +export async function discoverNuvioBackend(rawUrl: string, fetcher: Fetcher = fetch): Promise { + const backendUrl = normalizeBackendUrl(rawUrl); + const response = await fetcher(`${backendUrl}/.well-known/nuvio`, { headers: { accept: "application/json" } }); + if (!response.ok) { + throw new Error(`Could not read the Nuvio server settings (HTTP ${response.status})`); + } + return parseDiscovery(await response.json(), backendUrl); +} + +export async function signInToNuvio( + discovery: NuvioDiscovery, + email: string, + password: string, + fetcher: Fetcher = fetch +): Promise { + if (!discovery.emailPasswordAuth) throw new Error("This Nuvio server has email sign-in disabled"); + const response = await fetcher(`${discovery.backendUrl}/auth/v1/token?grant_type=password`, { + method: "POST", + headers: { "content-type": "application/json", apikey: discovery.publishableKey }, + body: JSON.stringify({ email: email.trim(), password }) + }); + const payload = await response.json().catch(() => ({})) as Record; + if (!response.ok) { + const message = typeof payload.error_description === "string" ? payload.error_description + : typeof payload.msg === "string" ? payload.msg + : "Nuvio rejected that email or password"; + throw new Error(message); + } + const accessToken = typeof payload.access_token === "string" ? payload.access_token : ""; + const user = (payload.user && typeof payload.user === "object" ? payload.user : {}) as Record; + if (!accessToken) throw new Error("Nuvio did not return an access token"); + return { + accessToken, + userId: typeof user.id === "string" ? user.id : "", + email: typeof user.email === "string" ? user.email : email.trim() + }; +} + +async function selectRows( + discovery: NuvioDiscovery, + session: NuvioSession, + table: string, + query: string, + fetcher: Fetcher +): Promise[]> { + const response = await fetcher(`${discovery.backendUrl}/rest/v1/${table}?${query}`, { + headers: { + apikey: discovery.publishableKey, + authorization: `Bearer ${session.accessToken}`, + accept: "application/json" + } + }); + // A table a deployment does not have (or does not expose) must not fail the + // whole import — the user still gets everything else. + if (!response.ok) return []; + const rows: unknown = await response.json().catch(() => []); + return Array.isArray(rows) ? rows.filter((row): row is Record => Boolean(row) && typeof row === "object") : []; +} + +/** + * Row-level security on a Nuvio deployment can leave a plain table read empty + * even though the data is there; the sync RPCs are the supported way in. They + * take one profile at a time, so they are only used to fill a gap. + */ +async function callSyncRpc( + discovery: NuvioDiscovery, + session: NuvioSession, + name: string, + profileIds: number[], + fetcher: Fetcher +): Promise[]> { + const rows: Record[] = []; + for (const profileId of profileIds) { + const response = await fetcher(`${discovery.backendUrl}/rest/v1/rpc/${name}`, { + method: "POST", + headers: { + apikey: discovery.publishableKey, + authorization: `Bearer ${session.accessToken}`, + "content-type": "application/json", + accept: "application/json" + }, + body: JSON.stringify({ p_profile_id: profileId }) + }).catch(() => null); + if (!response?.ok) continue; + const payload: unknown = await response.json().catch(() => null); + const list = Array.isArray(payload) ? payload : payload ? [payload] : []; + for (const row of list) { + if (row && typeof row === "object") { + rows.push({ profile_id: profileId, ...(row as Record) }); + } + } + } + return rows; +} + +const text = (value: unknown): string => typeof value === "string" ? value.trim() : ""; +const int = (value: unknown, fallback: number): number => { + const parsed = typeof value === "number" ? value : Number.parseInt(text(value), 10); + return Number.isFinite(parsed) ? parsed : fallback; +}; + +export function mapAddonRows(rows: Record[]): NuvioAddon[] { + return rows + .map(row => ({ + url: text(row.url), + name: text(row.name) || null, + enabled: row.enabled !== false, + sortOrder: int(row.sort_order, 0) + })) + .filter(addon => /^https?:\/\//i.test(addon.url)) + .sort((a, b) => a.sortOrder - b.sortOrder); +} + +export function mapPluginRows(rows: Record[]): NuvioPlugin[] { + return mapAddonRows(rows).map((addon, index) => ({ + ...addon, + repoType: text(rows[index]?.repo_type) || null + })); +} + +/** Groups every table by Nuvio's integer profile id. */ +export function buildSnapshot( + email: string, + profileRows: Record[], + addonRows: Record[], + pluginRows: Record[], + collectionRows: Record[], + catalogSettingRows: Record[] +): NuvioSnapshot { + const byProfile = >(rows: T[], profileId: number) => + rows.filter(row => int(row.profile_id, 1) === profileId); + const profiles = profileRows + .map(row => { + const profileId = int(row.profile_id, int(row.profile_index, 1)); + const addons = byProfile(addonRows, profileId); + const plugins = byProfile(pluginRows, profileId); + return { + profileId, + name: text(row.name) || `Profile ${profileId}`, + avatarColorHex: text(row.avatar_color_hex) || "#1E88E5", + avatarId: text(row.avatar_id) || null, + pinEnabled: row.pin_enabled === true, + addons: mapAddonRows(addons), + plugins: mapPluginRows(plugins), + collectionsJson: byProfile(collectionRows, profileId)[0]?.collections_json ?? null, + homeCatalogSettings: byProfile(catalogSettingRows, profileId)[0]?.settings_json ?? null + }; + }) + .sort((a, b) => a.profileId - b.profileId); + return { email, profiles }; +} + +export async function fetchNuvioSnapshot( + discovery: NuvioDiscovery, + session: NuvioSession, + fetcher: Fetcher = fetch +): Promise { + const [profiles, addons, plugins, collections, catalogSettings] = await Promise.all([ + selectRows(discovery, session, "profiles", "select=*&order=profile_id.asc", fetcher), + selectRows(discovery, session, "addons", "select=*&order=sort_order.asc", fetcher), + selectRows(discovery, session, "plugins", "select=*&order=sort_order.asc", fetcher), + selectRows(discovery, session, "collections", "select=*", fetcher), + selectRows(discovery, session, "home_catalog_settings", "select=*", fetcher) + ]); + + // Nuvio accounts always have profile 1; a direct read of `profiles` that comes + // back empty means the table is not exposed, not that the account is empty. + const profileIds = profiles.length + ? profiles.map(row => int(row.profile_id, int(row.profile_index, 1))) + : [1, 2, 3, 4, 5, 6]; + const effectiveProfiles = profiles.length ? profiles : [{ profile_id: 1, name: "Profile 1" }]; + const effectiveCollections = collections.length ? collections + : await callSyncRpc(discovery, session, "sync_pull_collections", profileIds, fetcher); + const effectiveCatalogSettings = catalogSettings.length ? catalogSettings + : await callSyncRpc(discovery, session, "sync_pull_home_catalog_settings", profileIds, fetcher); + + const snapshot = buildSnapshot( + session.email, effectiveProfiles, addons, plugins, effectiveCollections, effectiveCatalogSettings + ); + if (!snapshot.profiles.length) throw new Error("That Nuvio account has no profiles to import"); + return snapshot; +} + +// ── Mapping Nuvio profiles onto ARVIO profiles ────────────────────────────── + +/** Nuvio stores "#RRGGBB"; ARVIO stores the Android ARGB long. */ +export function hexToArgb(hex: string, fallback: number): number { + const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim()); + if (!match) return fallback; + return (0xff000000 | Number.parseInt(match[1], 16)) >>> 0; +} + +/** Nuvio avatar ids are free text; ARVIO uses 0 (initial) or an avatar 1-84. */ +export function avatarIdToArvio(avatarId: string | null): number { + const digits = /(\d{1,3})/.exec(avatarId ?? ""); + if (!digits) return 0; + const parsed = Number.parseInt(digits[1], 10); + return parsed >= 1 && parsed <= 84 ? parsed : 0; +} + +export type ProfileTarget = + | { kind: "existing"; profileId: string } + | { kind: "create" } + | { kind: "skip" }; + +export interface MigrationChoice { + nuvioProfileId: number; + target: ProfileTarget; +} + +const normalizedName = (value: string) => value.trim().toLocaleLowerCase("en"); + +/** + * Default plan: a Nuvio profile lands on the ARVIO profile with the same name, + * otherwise it is created. That keeps a repeated import idempotent instead of + * growing a second copy of every profile. + */ +export function defaultChoices(snapshot: NuvioSnapshot, existing: Pick[]): MigrationChoice[] { + const taken = new Set(); + return snapshot.profiles.map(profile => { + const match = existing.find(candidate => + normalizedName(candidate.name) === normalizedName(profile.name) && !taken.has(candidate.id)); + if (match) taken.add(match.id); + return { nuvioProfileId: profile.profileId, target: match ? { kind: "existing", profileId: match.id } : { kind: "create" } }; + }); +} + +export function newProfileFrom(profile: NuvioProfileData, id: string, fallbackColor: number): Profile { + return { + id, + name: profile.name.slice(0, 40), + avatarColor: hexToArgb(profile.avatarColorHex, fallbackColor), + avatarId: avatarIdToArvio(profile.avatarId), + createdAt: Date.now(), + lastUsedAt: 0, + // A Nuvio PIN hash cannot be carried over, so the copy starts unlocked and + // the user sets a new PIN in ARVIO rather than being locked out of it. + isLocked: false, + pin: null + }; +} + +export interface ProfileImportSummary { + profileName: string; + created: boolean; + addons: number; + addonsFailed: number; + plugins: number; + collections: number; + catalogSettings: boolean; +} + +/** Counts what a plan would bring over, for the confirmation step. */ +export function summarizePlan(snapshot: NuvioSnapshot, choices: MigrationChoice[]): ProfileImportSummary[] { + return snapshot.profiles.flatMap(profile => { + const choice = choices.find(entry => entry.nuvioProfileId === profile.profileId); + if (!choice || choice.target.kind === "skip") return []; + const collections = Array.isArray(profile.collectionsJson) ? profile.collectionsJson.length : 0; + return [{ + profileName: profile.name, + created: choice.target.kind === "create", + addons: profile.addons.length, + addonsFailed: 0, + plugins: profile.plugins.length, + collections, + catalogSettings: Boolean(profile.homeCatalogSettings) + }]; + }); +} + +/** Keeps existing addons, appends the Nuvio ones, newest URL wins. */ +export function mergeAddonUrls(existing: string[], incoming: NuvioAddon[]): string[] { + const seen = new Set(existing.map(url => url.trim().toLowerCase())); + const added: string[] = []; + for (const addon of incoming) { + const key = addon.url.trim().toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + added.push(addon.url); + } + return added; +} + +/** + * Nuvio's home catalog settings describe row order and visibility by catalog id. + * ARVIO keeps the same two ideas, so the parts that line up are applied and the + * rest is ignored rather than guessed at. + */ +export function applyHomeCatalogSettings(catalogs: CatalogConfig[], settings: unknown): CatalogConfig[] { + const doc = (settings && typeof settings === "object" ? settings : {}) as Record; + const order = Array.isArray(doc.order) ? doc.order.map(text).filter(Boolean) + : Array.isArray(doc.catalogOrder) ? doc.catalogOrder.map(text).filter(Boolean) : []; + const hidden = new Set( + (Array.isArray(doc.hidden) ? doc.hidden : Array.isArray(doc.hiddenCatalogIds) ? doc.hiddenCatalogIds : []) + .map(text).filter(Boolean) + ); + if (!order.length && !hidden.size) return catalogs; + const ranked = catalogs.map(catalog => ({ + catalog: hidden.has(catalog.id) ? { ...catalog, enabled: false } : catalog, + rank: order.indexOf(catalog.id) + })); + return ranked + .sort((a, b) => (a.rank < 0 ? Number.MAX_SAFE_INTEGER : a.rank) - (b.rank < 0 ? Number.MAX_SAFE_INTEGER : b.rank)) + .map(entry => entry.catalog); +} diff --git a/web/tests/nuvio-import-runner.test.cjs b/web/tests/nuvio-import-runner.test.cjs new file mode 100644 index 000000000..6e7720ae4 --- /dev/null +++ b/web/tests/nuvio-import-runner.test.cjs @@ -0,0 +1,114 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { load } = require('./load.cjs'); +// Values produced inside the sandbox carry that realm's prototypes. +const same = (actual, expected) => assert.deepEqual(JSON.parse(JSON.stringify(actual)), expected); + +const snapshot = { + email: 'demo@nuvio.example', + profiles: [ + { + profileId: 1, name: 'Main', avatarColorHex: '#E50914', avatarId: 'avatar_3', pinEnabled: false, + addons: [{ url: 'https://a.example/manifest.json', name: 'A', enabled: true, sortOrder: 0 }, + { url: 'https://down.example/manifest.json', name: 'Down', enabled: true, sortOrder: 1 }], + plugins: [{ url: 'https://p.example/repo.json', name: 'P', enabled: true, sortOrder: 0, repoType: 'github' }], + collectionsJson: [{ id: 'studios', title: 'Studios', folders: [ + { id: 'pixar', title: 'Pixar', sources: [{ provider: 'tmdb', tmdbSourceType: 'COMPANY', tmdbId: 3, mediaType: 'MOVIE' }] } + ] }], + homeCatalogSettings: null + }, + { + profileId: 2, name: 'Kids', avatarColorHex: '#1DB954', avatarId: 'avatar_9', pinEnabled: true, + addons: [{ url: 'https://a.example/manifest.json', name: 'A', enabled: true, sortOrder: 0 }], + plugins: [], collectionsJson: null, homeCatalogSettings: null + } + ] +}; + +function setup({ existingProfiles = [], existingAddons = [] } = {}) { + const calls = { addons: [], settings: [], profiles: [], installed: [] }; + const runner = load('lib/nuvioImportRunner.ts', { + './addons': { + installAddon: async (url) => { + calls.installed.push(url); + if (url.includes('down.example')) throw new Error('unreachable'); + return { id: url, name: `Addon ${calls.installed.length}`, manifestUrl: url, enabled: true }; + }, + normalizeAddons: (list) => list.filter(Boolean) + }, + './auth': {}, + './catalogs': { defaultCatalogs: [{ id: 'trending', enabled: true }] }, + './cloud': { + pullCloudPayload: async () => ({ addons: existingAddons, settings: { catalogs: [{ id: 'trending', enabled: true }] } }), + saveCloudAddons: async (_auth, addons, profileId) => calls.addons.push({ profileId, count: addons.length }), + saveCloudProfiles: async (_auth, profiles) => calls.profiles.push(profiles.map(p => p.name)), + saveCloudSettings: async (_auth, settings, _addons, profileId) => + calls.settings.push({ profileId, catalogs: settings.catalogs.length }) + }, + './customCollections': load('lib/customCollections.ts'), + './nuvioMigration': load('lib/nuvioMigration.ts'), + './profiles': { profileColors: [1, 2, 3] }, + './types': {} + }); + return { runner, calls }; +} + +const auth = { session: { userId: 'u1', email: 'me@arvio.example' } }; +const baseSettings = { catalogs: [] }; + +test('creates the missing profiles and writes each one to the account', async () => { + const { runner, calls } = setup({ existingProfiles: [] }); + const result = await runner.applyNuvioImport({ + auth, profiles: [{ id: 'p1', name: 'Main' }], snapshot, + choices: [ + { nuvioProfileId: 1, target: { kind: 'existing', profileId: 'p1' } }, + { nuvioProfileId: 2, target: { kind: 'create' } } + ], + baseSettings + }); + // Kids had no ARVIO profile, so it was created before anything was written. + same(calls.profiles, [['Main', 'Kids']]); + assert.equal(result.profiles.length, 2); + assert.equal(result.summaries.length, 2); + + const main = result.summaries[0]; + assert.equal(main.addons, 1); // one installed + assert.equal(main.addonsFailed, 1); // the unreachable one is reported, not fatal + assert.equal(main.collections, 1); + assert.equal(main.plugins, 1); // counted so the UI can say they stay behind + assert.equal(result.summaries[1].created, true); + + // Settings (catalogs) are written for both profiles, addons only where new ones resolved. + same(calls.settings.map(entry => entry.profileId).sort(), ['p1', result.profiles[1].id].sort()); + assert.equal(calls.addons.length, 2); +}); + +test('skips profiles the user did not choose and never writes them', async () => { + const { runner, calls } = setup(); + const result = await runner.applyNuvioImport({ + auth, profiles: [{ id: 'p1', name: 'Main' }], snapshot, + choices: [{ nuvioProfileId: 1, target: { kind: 'skip' } }, { nuvioProfileId: 2, target: { kind: 'skip' } }], + baseSettings + }); + same(result.summaries, []); + same(calls.settings, []); + same(calls.profiles, []); +}); + +test('an addon already on the profile is not installed twice', async () => { + const { runner, calls } = setup({ existingAddons: [{ id: 'x', name: 'A', manifestUrl: 'https://a.example/manifest.json' }] }); + await runner.applyNuvioImport({ + auth, profiles: [{ id: 'p1', name: 'Main' }], snapshot, + choices: [{ nuvioProfileId: 1, target: { kind: 'existing', profileId: 'p1' } }], + baseSettings + }); + same(calls.installed, ['https://down.example/manifest.json']); +}); + +test('refuses to run without an ARVIO session', async () => { + const { runner } = setup(); + await assert.rejects( + runner.applyNuvioImport({ auth: { session: null }, profiles: [], snapshot, choices: [], baseSettings }), + /Sign in to your ARVIO account/ + ); +}); diff --git a/web/tests/nuvio-migration.test.cjs b/web/tests/nuvio-migration.test.cjs new file mode 100644 index 000000000..2d8025b01 --- /dev/null +++ b/web/tests/nuvio-migration.test.cjs @@ -0,0 +1,207 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const ts = require('typescript'); +const fs = require('node:fs'); +const vm = require('node:vm'); + +const code = ts.transpileModule(fs.readFileSync(require.resolve('../lib/nuvioMigration.ts'), 'utf8'), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } +}).outputText; +const sandbox = { exports: {}, URL, console }; +vm.runInNewContext(code, sandbox); +const { + normalizeBackendUrl, parseDiscovery, discoverNuvioBackend, signInToNuvio, fetchNuvioSnapshot, + buildSnapshot, mapAddonRows, hexToArgb, avatarIdToArvio, defaultChoices, newProfileFrom, + summarizePlan, mergeAddonUrls, applyHomeCatalogSettings +} = sandbox.exports; + +const jsonResponse = (body, ok = true, status = 200) => ({ ok, status, json: async () => body }); +// Values built inside the VM sandbox carry that realm's prototypes, so compare +// them structurally rather than by reference. +const plain = (value) => JSON.parse(JSON.stringify(value)); +const same = (actual, expected) => assert.deepEqual(plain(actual), expected); + +test('accepts the server address in the forms a user types', () => { + assert.equal(normalizeBackendUrl('nuvio.example.com'), 'https://nuvio.example.com'); + assert.equal(normalizeBackendUrl('https://nuvio.example.com/'), 'https://nuvio.example.com'); + assert.equal(normalizeBackendUrl('https://nuvio.example.com/.well-known/nuvio'), 'https://nuvio.example.com'); + assert.equal(normalizeBackendUrl('http://localhost:8000'), 'http://localhost:8000'); + // A local server speaks plain HTTP, so a scheme-less local address keeps it. + assert.equal(normalizeBackendUrl('localhost:4599'), 'http://localhost:4599'); + assert.equal(normalizeBackendUrl('127.0.0.1:8000'), 'http://127.0.0.1:8000'); + // Plaintext to a remote host would leak the password. + assert.throws(() => normalizeBackendUrl('http://nuvio.example.com'), /HTTPS/); + assert.throws(() => normalizeBackendUrl(' '), /server address/); +}); + +test('reads the publishable key from the discovery document', () => { + const discovery = parseDiscovery({ + backend_url: 'https://api.example.com', publishable_key: 'pk_test', self_hosted: true, + capabilities: { email_password_auth: true } + }, 'https://nuvio.example.com'); + assert.equal(discovery.backendUrl, 'https://api.example.com'); + assert.equal(discovery.publishableKey, 'pk_test'); + assert.equal(discovery.selfHosted, true); + assert.equal(discovery.emailPasswordAuth, true); + // A server that answers without a key cannot be signed in to. + assert.throws(() => parseDiscovery({ backend_url: 'https://api.example.com' }, 'https://x.example'), /publishable key/); + // Older deployments omit capabilities entirely. + assert.equal(parseDiscovery({ publishable_key: 'pk' }, 'https://x.example').emailPasswordAuth, true); +}); + +test('discovery falls back to the typed address when the document omits backend_url', async () => { + const discovery = await discoverNuvioBackend('nuvio.example.com', async (url) => { + assert.equal(url, 'https://nuvio.example.com/.well-known/nuvio'); + return jsonResponse({ publishable_key: 'pk_test' }); + }); + assert.equal(discovery.backendUrl, 'https://nuvio.example.com'); +}); + +test('sign-in surfaces the server message instead of a generic failure', async () => { + const discovery = { backendUrl: 'https://api.example.com', publishableKey: 'pk', emailPasswordAuth: true }; + const session = await signInToNuvio(discovery, ' user@example.com ', 'secret', async (url, init) => { + assert.equal(url, 'https://api.example.com/auth/v1/token?grant_type=password'); + assert.equal(init.headers.apikey, 'pk'); + same(JSON.parse(init.body), { email: 'user@example.com', password: 'secret' }); + return jsonResponse({ access_token: 'token', user: { id: 'u1', email: 'user@example.com' } }); + }); + assert.equal(session.accessToken, 'token'); + assert.equal(session.email, 'user@example.com'); + + await assert.rejects( + signInToNuvio(discovery, 'user@example.com', 'wrong', async () => + jsonResponse({ error_description: 'Invalid login credentials' }, false, 400)), + /Invalid login credentials/ + ); + await assert.rejects( + signInToNuvio({ ...discovery, emailPasswordAuth: false }, 'a@b.c', 'x', async () => jsonResponse({})), + /email sign-in disabled/ + ); +}); + +test('groups addons, plugins and collections by their Nuvio profile', () => { + const snapshot = buildSnapshot( + 'user@example.com', + [ + { profile_id: 2, name: 'Kids', avatar_color_hex: '#1DB954', avatar_id: 'avatar_7', pin_enabled: true }, + { profile_id: 1, name: 'Main', avatar_color_hex: '#E50914' } + ], + [ + { profile_id: 1, url: 'https://a.example/manifest.json', name: 'A', sort_order: 2 }, + { profile_id: 1, url: 'https://b.example/manifest.json', name: 'B', sort_order: 1, enabled: false }, + { profile_id: 2, url: 'https://kids.example/manifest.json' }, + { profile_id: 1, url: 'not-a-url' } + ], + [{ profile_id: 1, url: 'https://plug.example/repo.json', repo_type: 'github' }], + [{ profile_id: 1, collections_json: [{ title: 'Studios', folders: [] }] }], + [{ profile_id: 1, settings_json: { order: ['b', 'a'] } }] + ); + same(snapshot.profiles.map(p => p.profileId), [1, 2]); + const main = snapshot.profiles[0]; + // sort_order decides the order, and a malformed URL is dropped. + same(main.addons.map(a => a.name), ['B', 'A']); + assert.equal(main.addons[0].enabled, false); + assert.equal(main.plugins[0].repoType, 'github'); + assert.equal(main.collectionsJson.length, 1); + same(main.homeCatalogSettings, { order: ['b', 'a'] }); + assert.equal(snapshot.profiles[1].addons.length, 1); + assert.equal(snapshot.profiles[1].pinEnabled, true); +}); + +test('a missing table does not fail the whole import', async () => { + const discovery = { backendUrl: 'https://api.example.com', publishableKey: 'pk' }; + const snapshot = await fetchNuvioSnapshot(discovery, { accessToken: 't', userId: 'u', email: 'u@e.c' }, async (url) => { + if (url.includes('/profiles')) return jsonResponse([{ profile_id: 1, name: 'Main' }]); + if (url.includes('/plugins')) return jsonResponse({ message: 'relation does not exist' }, false, 404); + return jsonResponse([]); + }); + assert.equal(snapshot.profiles.length, 1); + same(snapshot.profiles[0].plugins, []); + + // A deployment that exposes nothing directly still yields the default profile, + // so the sync RPCs below have something to fill. + const bare = await fetchNuvioSnapshot(discovery, { accessToken: 't', userId: 'u', email: 'u@e.c' }, async () => jsonResponse([])); + assert.equal(bare.profiles.length, 1); + assert.equal(bare.profiles[0].profileId, 1); +}); + +test('falls back to the sync RPCs when a direct table read is empty', async () => { + const discovery = { backendUrl: 'https://api.example.com', publishableKey: 'pk' }; + const called = []; + const snapshot = await fetchNuvioSnapshot(discovery, { accessToken: 't', userId: 'u', email: 'u@e.c' }, async (url, init) => { + if (url.includes('/rpc/')) { + called.push({ url, profileId: JSON.parse(init.body).p_profile_id }); + return url.includes('sync_pull_collections') + ? jsonResponse([{ collections_json: [{ title: 'Studios', folders: [] }] }]) + : jsonResponse([{ settings_json: { order: ['a'] } }]); + } + if (url.includes('/profiles')) return jsonResponse([{ profile_id: 1, name: 'Main' }]); + return jsonResponse([]); + }); + // Row-level security can hide the tables while the RPCs still answer. + assert.equal(snapshot.profiles[0].collectionsJson.length, 1); + same(snapshot.profiles[0].homeCatalogSettings, { order: ['a'] }); + assert.deepEqual(called.map(entry => entry.profileId), [1, 1]); +}); + +test('converts Nuvio profile looks into ARVIO ones', () => { + assert.equal(hexToArgb('#E50914', 0), 0xffe50914); + assert.equal(hexToArgb('E50914', 0), 0xffe50914); + assert.equal(hexToArgb('nonsense', 0xff123456), 0xff123456); + assert.equal(avatarIdToArvio('avatar_12'), 12); + assert.equal(avatarIdToArvio('85'), 0); + assert.equal(avatarIdToArvio(null), 0); + + const profile = newProfileFrom( + { name: 'Kids', avatarColorHex: '#1DB954', avatarId: 'avatar_7', pinEnabled: true }, + 'new-id', 0xff000000 + ); + assert.equal(profile.avatarColor, 0xff1db954); + assert.equal(profile.avatarId, 7); + // A Nuvio PIN hash is not portable, so the copy must not arrive locked. + assert.equal(profile.isLocked, false); + assert.equal(profile.pin, null); +}); + +test('matches profiles by name and creates the missing ones', () => { + const snapshot = { + email: 'u@e.c', + profiles: [ + { profileId: 1, name: 'Main', addons: [], plugins: [], collectionsJson: null, homeCatalogSettings: null }, + { profileId: 2, name: 'Kids', addons: [{ url: 'https://a.example/manifest.json' }], plugins: [], collectionsJson: [{}, {}], homeCatalogSettings: { order: [] } } + ] + }; + const choices = defaultChoices(snapshot, [{ id: 'p1', name: ' main ' }]); + same(choices[0].target, { kind: 'existing', profileId: 'p1' }); + same(choices[1].target, { kind: 'create' }); + + const summary = summarizePlan(snapshot, choices); + assert.equal(summary.length, 2); + assert.equal(summary[1].created, true); + assert.equal(summary[1].addons, 1); + assert.equal(summary[1].collections, 2); + assert.equal(summary[1].catalogSettings, true); + + // An explicit skip, or a profile the user never chose a target for, is left out. + assert.equal(summarizePlan(snapshot, [{ nuvioProfileId: 1, target: { kind: 'skip' } }]).length, 0); + assert.equal(summarizePlan(snapshot, [{ nuvioProfileId: 2, target: { kind: 'create' } }]).length, 1); +}); + +test('only adds addons that are not installed yet', () => { + const added = mergeAddonUrls( + ['https://a.example/manifest.json'], + [{ url: 'https://A.example/manifest.json' }, { url: 'https://b.example/manifest.json' }, { url: 'https://b.example/manifest.json' }] + ); + same(added, ['https://b.example/manifest.json']); +}); + +test('applies Nuvio row order and hidden rows onto ARVIO catalogs', () => { + const catalogs = [ + { id: 'a', enabled: true }, { id: 'b', enabled: true }, { id: 'c', enabled: true } + ]; + const result = applyHomeCatalogSettings(catalogs, { order: ['c', 'a'], hidden: ['b'] }); + same(result.map(c => c.id), ['c', 'a', 'b']); + assert.equal(result.find(c => c.id === 'b').enabled, false); + // Nothing recognisable means the user's existing order is left alone. + same(applyHomeCatalogSettings(catalogs, { unrelated: true }), catalogs); +}); From 790db45b7b1ac6644ca298f8ddc3d3a6e908b1d0 Mon Sep 17 00:00:00 2001 From: koby455 Date: Tue, 6 Oct 2026 19:10:01 +0300 Subject: [PATCH 2/4] Protect existing account data during a Nuvio import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on #774, all five: 1. The import wrote a whole settings object with no baseline, so the theme, the AI subtitle toggle and the saved AI key were reset to the page's defaults. It now reads the profile's current cloud settings, changes only `catalogs`, and passes those same settings as the `baseline`, so saveCloudSettings asserts nothing else. 2. A failed cloud read was swallowed and treated as an empty profile, which then wrote defaults over real catalogs and IPTV playlists. The failure now stops that profile's import, writes nothing for it, and is reported in the summary. Other profiles continue. 3. Connecting Nuvio before signing in to ARVIO left every profile set to "create new", because the plan was drawn up against an empty profile list. The plan is recalculated when profiles arrive, keeping any target the user already picked by hand. 4. When the profile table is unavailable the profile ids are recovered from whatever the other tables and the sync RPCs answered for, instead of building Profile 1 alone and dropping the rest. A read that failed is also distinguished from a table that is simply empty, and a failed read is surfaced as a warning. 5. An addon switched off in Nuvio is installed switched off. Tests: the two data-safety regressions run against the real cloud helpers with a stubbed backend, since that is the layer the damage happened in — one asserts unrelated settings and IPTV survive an import, one asserts a failed read writes nothing at all. Co-Authored-By: Claude Opus 5 --- web/app/migrate/setup.css | 10 ++ web/components/migrate/MigrateApp.tsx | 30 ++++- web/lib/nuvioImportRunner.ts | 50 ++++++-- web/lib/nuvioMigration.ts | 140 ++++++++++++++++++---- web/tests/nuvio-import-runner.test.cjs | 92 ++++++++++++-- web/tests/nuvio-import-safety.test.cjs | 158 +++++++++++++++++++++++++ web/tests/nuvio-migration.test.cjs | 96 ++++++++++++++- 7 files changed, 521 insertions(+), 55 deletions(-) create mode 100644 web/tests/nuvio-import-safety.test.cjs diff --git a/web/app/migrate/setup.css b/web/app/migrate/setup.css index c882530de..1f5610534 100644 --- a/web/app/migrate/setup.css +++ b/web/app/migrate/setup.css @@ -184,6 +184,16 @@ .setup__error { color: var(--red); font-size: 14px; } +.setup__warning { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + color: var(--yellow, #ffcd3c); + font-size: 13px; + line-height: 1.5; +} + .setup .spin { animation: setup-spin 1s linear infinite; } @keyframes setup-spin { to { transform: rotate(360deg); } } diff --git a/web/components/migrate/MigrateApp.tsx b/web/components/migrate/MigrateApp.tsx index 8d83b9850..bd3cb0f42 100644 --- a/web/components/migrate/MigrateApp.tsx +++ b/web/components/migrate/MigrateApp.tsx @@ -1,13 +1,13 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { ArrowRightLeft, Check, Download, Loader2, LogIn, Plus, ShieldCheck, Trash2 } from "lucide-react"; +import { AlertTriangle, ArrowRightLeft, Check, Download, Loader2, LogIn, Plus, ShieldCheck, Trash2 } from "lucide-react"; import { authClient } from "@/lib/store"; import { installAddon, normalizeAddons } from "@/lib/addons"; import { pullCloudPayload, pullCloudProfiles, saveCloudAddons } from "@/lib/cloud"; import { applyNuvioImport } from "@/lib/nuvioImportRunner"; import { - defaultChoices, discoverNuvioBackend, fetchNuvioSnapshot, signInToNuvio, summarizePlan, OFFICIAL_NUVIO_URL, + defaultChoices, discoverNuvioBackend, fetchNuvioSnapshot, reconcileChoices, signInToNuvio, summarizePlan, OFFICIAL_NUVIO_URL, type MigrationChoice, type NuvioSnapshot, type ProfileImportSummary } from "@/lib/nuvioMigration"; import { defaultSettings } from "@/lib/store"; @@ -154,9 +154,18 @@ function SetupTools({ language, onLanguageChange, showLanguagePicker }: { setResults(null); }); + // The Nuvio step runs before the ARVIO sign-in, so the first plan is drawn up + // against an empty profile list and proposes creating everything. Redo it the + // moment real profiles arrive — without touching a choice already made by hand. + useEffect(() => { + if (!snapshot) return; + setChoices(current => reconcileChoices(snapshot, profiles, current)); + }, [snapshot, profiles]); + const setTarget = (nuvioProfileId: number, value: string) => { setChoices(current => current.map(choice => choice.nuvioProfileId !== nuvioProfileId ? choice : { ...choice, + userSet: true, target: value === "__skip" ? { kind: "skip" } : value === "__create" ? { kind: "create" } : { kind: "existing", profileId: value } })); }; @@ -224,16 +233,21 @@ function SetupTools({ language, onLanguageChange, showLanguagePicker }: { {snapshot && !results && <>

Found {snapshot.profiles.length} profiles in {snapshot.email}.

+ {snapshot.warnings.map(warning =>

+

)}
    {snapshot.profiles.map(profile => { const choice = choices.find(entry => entry.nuvioProfileId === profile.profileId); const value = choice?.target.kind === "existing" ? choice.target.profileId : choice?.target.kind === "skip" ? "__skip" : "__create"; const collections = Array.isArray(profile.collectionsJson) ? profile.collectionsJson.length : 0; + const disabled = profile.addons.filter(addon => !addon.enabled).length; return
  • {profile.name} {profile.addons.length} addons · {collections} collections + {disabled > 0 && ` · ${disabled} switched off in Nuvio`} {profile.plugins.length > 0 && ` · ${profile.plugins.length} plugins stay in Nuvio`} setAddonUrl(event.target.value)} placeholder="https://example.com/manifest.json" spellCheck={false} /> - diff --git a/web/lib/cloud.ts b/web/lib/cloud.ts index 5487f6a7a..17caee3c4 100644 --- a/web/lib/cloud.ts +++ b/web/lib/cloud.ts @@ -1454,6 +1454,29 @@ export async function saveCloudProfiles(auth: AuthClient, profiles: Profile[], a }); } +/** Add imported profiles without asserting a stale copy of the account's list. */ +export async function addCloudProfiles(auth: AuthClient, additions: Profile[]): Promise { + if (!auth.session) throw new Error("Sign in to your ARVIO account first"); + let profiles: Profile[] = []; + await mutateCloudPayload(auth, (root) => { + if (root.profiles != null && !Array.isArray(root.profiles)) { + throw new Error("Could not read your ARVIO profiles"); + } + profiles = [...arrayValue(root.profiles)]; + const ids = new Set(profiles.map(profile => profile.id)); + for (const profile of additions) { + if (ids.has(profile.id)) continue; + profiles.push(profile); + ids.add(profile.id); + } + root.profiles = profiles; + if (!profiles.some(profile => profile.id === root.activeProfileId)) { + root.activeProfileId = profiles[0]?.id ?? null; + } + }); + return profiles; +} + const liveAddonIdMarkers = [ "livetv", "live_tv", diff --git a/web/lib/nuvioImportRunner.ts b/web/lib/nuvioImportRunner.ts index bb2782228..5c57359ff 100644 --- a/web/lib/nuvioImportRunner.ts +++ b/web/lib/nuvioImportRunner.ts @@ -1,7 +1,7 @@ import { installAddon, normalizeAddon, normalizeAddons } from "./addons"; import type { AuthClient } from "./auth"; import { defaultCatalogs } from "./catalogs"; -import { pullCloudPayload, saveCloudAddons, saveCloudProfiles, saveCloudSettings } from "./cloud"; +import { addCloudProfiles, invalidateRawPayloadCache, pullCloudPayload, pullCloudProfiles, saveCloudAddons, saveCloudSettings } from "./cloud"; import { mergeImportedCollections, parseCustomCollections } from "./customCollections"; import { addonsToInstall, applyHomeCatalogSettings, newProfileFrom, @@ -35,7 +35,10 @@ export async function applyNuvioImport(options: { const { auth, snapshot, choices, baseSettings, onProgress } = options; if (!auth.session) throw new Error("Sign in to your ARVIO account first"); - let profiles = [...options.profiles]; + if (!choices.some(choice => choice.target.kind !== "skip")) return { summaries: [], profiles: options.profiles }; + invalidateRawPayloadCache(); + let profiles = [...(await pullCloudProfiles(auth)).profiles]; + const additions: Profile[] = []; const targets = new Map(); // Profiles first: a Nuvio profile with no ARVIO counterpart is created, so a @@ -44,17 +47,23 @@ export async function applyNuvioImport(options: { const choice = choices.find(entry => entry.nuvioProfileId === profile.profileId); if (!choice || choice.target.kind === "skip") continue; if (choice.target.kind === "existing") { - targets.set(profile.profileId, { id: choice.target.profileId, created: false }); + const profileId = choice.target.profileId; + if (!profiles.some(entry => entry.id === profileId)) { + throw new Error("An ARVIO profile is no longer available. Reload your account and choose again."); + } + targets.set(profile.profileId, { id: profileId, created: false }); continue; } onProgress?.(`Creating profile ${profile.name}`); const id = globalThis.crypto?.randomUUID?.() ?? `p_${Date.now()}_${Math.floor(Math.random() * 1e6)}`; const color = profileColors[profiles.length % profileColors.length]; - profiles = [...profiles, newProfileFrom(profile, id, color)]; + const created = newProfileFrom(profile, id, color); + profiles = [...profiles, created]; + additions.push(created); targets.set(profile.profileId, { id, created: true }); } if (!targets.size) return { summaries: [], profiles }; - await saveCloudProfiles(auth, profiles, profiles[0]?.id ?? null); + if (additions.length) profiles = await addCloudProfiles(auth, additions); const summaries: ProfileImportSummary[] = []; for (const profile of snapshot.profiles) { @@ -72,6 +81,7 @@ export async function applyNuvioImport(options: { // (theme, AI key, IPTV) are shared and must survive the same way. let existing; try { + invalidateRawPayloadCache(); existing = await pullCloudPayload(auth, target.id); } catch (failure) { const message = (failure as { message?: unknown } | null)?.message; @@ -122,7 +132,9 @@ export async function applyNuvioImport(options: { onProgress?.(`${profile.name}: saving to your ARVIO account`); const settings: AppSettings = { ...base, catalogs: orderedCatalogs }; if (resolved.length) await saveCloudAddons(auth, nextAddons, target.id, { changes: undefined }); - await saveCloudSettings(auth, settings, nextAddons, target.id, profiles, base); + // Profile creation has its own additive write; settings must not reassert + // the list if another device creates or edits a profile during the import. + await saveCloudSettings(auth, settings, nextAddons, target.id, [], base); summaries.push(summary); } return { summaries, profiles }; diff --git a/web/lib/nuvioMigration.ts b/web/lib/nuvioMigration.ts index ba56057fb..bf45ea55b 100644 --- a/web/lib/nuvioMigration.ts +++ b/web/lib/nuvioMigration.ts @@ -169,7 +169,7 @@ export async function selectRows( // reported instead of passing for an empty table. if (!response?.ok) return { table, ok: false, rows: [] }; const rows: unknown = await response.json().catch(() => null); - if (rows === null) return { table, ok: false, rows: [] }; + if (!Array.isArray(rows)) return { table, ok: false, rows: [] }; return { table, ok: true, rows: objectRows(rows) }; } @@ -184,8 +184,9 @@ async function callSyncRpc( name: string, profileIds: number[], fetcher: Fetcher -): Promise[]> { +): Promise<{ rows: Record[]; failedProfileIds: number[] }> { const rows: Record[] = []; + const failedProfileIds: number[] = []; for (const profileId of profileIds) { const response = await fetcher(`${discovery.backendUrl}/rest/v1/rpc/${name}`, { method: "POST", @@ -197,8 +198,15 @@ async function callSyncRpc( }, body: JSON.stringify({ p_profile_id: profileId }) }).catch(() => null); - if (!response?.ok) continue; - const payload: unknown = await response.json().catch(() => null); + if (!response?.ok) { + failedProfileIds.push(profileId); + continue; + } + const payload: unknown = await response.json().catch(() => undefined); + if (payload === undefined || (payload !== null && typeof payload !== "object")) { + failedProfileIds.push(profileId); + continue; + } const list = Array.isArray(payload) ? payload : payload ? [payload] : []; for (const row of list) { if (row && typeof row === "object") { @@ -206,7 +214,7 @@ async function callSyncRpc( } } } - return rows; + return { rows, failedProfileIds }; } const text = (value: unknown): string => typeof value === "string" ? value.trim() : ""; @@ -314,10 +322,16 @@ export async function fetchNuvioSnapshot( : profileIdsFromRows(addons.rows, plugins.rows, collections.rows, catalogSettings.rows); const probeIds = profiles.ok && profiles.rows.length ? knownIds : PROBE_PROFILE_IDS; - const effectiveCollections = collections.ok && collections.rows.length ? collections.rows - : await callSyncRpc(discovery, session, "sync_pull_collections", probeIds, fetcher); - const effectiveCatalogSettings = catalogSettings.ok && catalogSettings.rows.length ? catalogSettings.rows - : await callSyncRpc(discovery, session, "sync_pull_home_catalog_settings", probeIds, fetcher); + const readWithFallback = async (read: TableRead, rpc: string) => { + if (read.ok && read.rows.length) return read.rows; + const result = await callSyncRpc(discovery, session, rpc, probeIds, fetcher); + if (result.failedProfileIds.length) { + warnings.push(`Nuvio did not return ${read.table} for profiles ${result.failedProfileIds.join(", ")} - this part of their setup could not be imported. Reconnect to Nuvio to retry reading this data.`); + } + return result.rows; + }; + const effectiveCollections = await readWithFallback(collections, "sync_pull_collections"); + const effectiveCatalogSettings = await readWithFallback(catalogSettings, "sync_pull_home_catalog_settings"); let effectiveProfiles = profiles.rows; if (!profiles.ok || !profiles.rows.length) { diff --git a/web/package.json b/web/package.json index 2ec5fa11a..90cdc545c 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "setup:selfhost": "node scripts/selfhost.mjs setup", "check:selfhost": "node scripts/selfhost.mjs check", "test": "node --test tests/*.test.cjs", + "test:nuvio-ui": "node scripts/check-nuvio-ui.cjs", "test:library-ui": "node scripts/check-library-ui.cjs", "test:calendar-ui": "node scripts/check-calendar-ui.cjs", "test:player-sources-ui": "node scripts/check-player-sources-ui.cjs", diff --git a/web/scripts/check-nuvio-ui.cjs b/web/scripts/check-nuvio-ui.cjs new file mode 100644 index 000000000..cfe5480bb --- /dev/null +++ b/web/scripts/check-nuvio-ui.cjs @@ -0,0 +1,84 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const http = require('node:http'); +const assert = require('node:assert/strict'); +const esbuild = require('esbuild'); +const { chromium } = require('@playwright/test'); + +(async () => { + const root = path.resolve(__dirname, '..'); + const output = path.resolve(root, '../artifacts/nuvio-import'); + fs.mkdirSync(output, { recursive: true }); + const stub = path.join(root, 'tests/nuvio-ui/stubs.tsx'); + await esbuild.build({ + entryPoints: [path.join(root, 'tests/nuvio-ui/entry.tsx')], bundle: true, + outfile: path.join(output, 'app.js'), jsx: 'automatic', + define: { 'process.env.NODE_ENV': '"test"' }, + plugins: [{ name: 'offline-account', setup(build) { + build.onResolve({ filter: /^@\/lib\// }, () => ({ path: stub })); + } }] + }); + const server = http.createServer((req, res) => { + if (req.url === '/app.js') { + res.setHeader('content-type', 'text/javascript'); + fs.createReadStream(path.join(output, 'app.js')).pipe(res); + } else if (req.url === '/setup.css') { + res.setHeader('content-type', 'text/css'); + fs.createReadStream(path.join(root, 'app/migrate/setup.css')).pipe(res); + } else if (req.url === '/globals.css') { + res.setHeader('content-type', 'text/css'); + fs.createReadStream(path.join(root, 'app/globals.css')).pipe(res); + } else if (req.url.startsWith('/logos/')) { + const file = path.join(root, 'public/logos', path.basename(req.url)); + if (!fs.existsSync(file)) { res.writeHead(404).end(); return; } + res.setHeader('content-type', file.endsWith('.svg') ? 'image/svg+xml' : 'image/png'); + fs.createReadStream(file).pipe(res); + } else if (req.url === '/arvio-icon-192.png') { + res.setHeader('content-type', 'image/png'); + fs.createReadStream(path.join(root, 'public/arvio-icon-192.png')).pipe(res); + } else { + res.setHeader('content-type', 'text/html'); + res.end('
    '); + } + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + let browser; + try { + browser = await chromium.launch({ channel: 'msedge', headless: true }); + for (const [name, width, height] of [['desktop', 1440, 1000], ['mobile', 390, 844]]) { + const page = await browser.newPage({ viewport: { width, height } }); + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + await page.goto(`http://127.0.0.1:${server.address().port}`); + await page.getByText('Could not read your ARVIO account', { exact: true }).waitFor(); + const nuvio = page.locator('section').filter({ has: page.getByRole('heading', { name: /Nuvio/ }) }); + await nuvio.locator('input[type=email]').fill('demo@nuvio.example'); + await nuvio.locator('input[type=password]').fill('test-password'); + await nuvio.locator('button.setup__primary').first().click(); + const copy = page.getByRole('button', { name: /Copy 1 profiles into ARVIO/ }); + await copy.waitFor(); + assert.equal(await copy.isDisabled(), true, 'failed account load must block importing'); + assert.equal(await page.getByText('This account has no profiles yet.', { exact: false }).count(), 0); + await page.getByRole('button', { name: 'Retry loading account' }).click(); + await page.waitForFunction(() => window.nuvioTest.resolveProfiles !== null); + assert.equal(await copy.isDisabled(), true, 'pending retry must block importing'); + await page.evaluate(() => window.nuvioTest.resolveProfiles()); + await page.waitForFunction(() => ![...document.querySelectorAll('button')].find(button => button.textContent.includes('Copy 1 profiles')).disabled); + await copy.click(); + await page.getByText('Import finished with warnings', { exact: true }).waitFor(); + await page.getByRole('status').filter({ hasText: 'Nuvio did not return collections' }).waitFor(); + assert.equal(await page.evaluate(() => window.nuvioTest.imports), 1); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false); + assert.deepEqual(errors, []); + assert.equal(await page.locator('.setup__brand img').evaluate(image => image.complete && image.naturalWidth > 0), true); + await page.screenshot({ path: path.join(output, `${name}.png`), fullPage: true }); + await page.getByRole('button', { name: 'Reconnect to Nuvio' }).click(); + await page.getByRole('button', { name: 'Connect to Nuvio', exact: true }).waitFor(); + await page.close(); + console.log(`${name}: failed/pending account load blocked; retry enabled import; warnings visible`); + } + } finally { + await browser?.close(); + await new Promise(resolve => server.close(resolve)); + } +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/web/tests/nuvio-import-runner.test.cjs b/web/tests/nuvio-import-runner.test.cjs index 2d8873110..ba6057f02 100644 --- a/web/tests/nuvio-import-runner.test.cjs +++ b/web/tests/nuvio-import-runner.test.cjs @@ -27,7 +27,7 @@ const snapshot = { ] }; -function setup({ existingProfiles = [], existingAddons = [], pull } = {}) { +function setup({ existingProfiles = [{ id: 'p1', name: 'Main' }, { id: 'p2', name: 'Kids' }], existingAddons = [], pull } = {}) { const calls = { addons: [], settings: [], profiles: [], installed: [], saved: [] }; const runner = load('lib/nuvioImportRunner.ts', { './addons': { @@ -42,11 +42,17 @@ function setup({ existingProfiles = [], existingAddons = [], pull } = {}) { './auth': {}, './catalogs': { defaultCatalogs: [{ id: 'trending', enabled: true }] }, './cloud': { + invalidateRawPayloadCache: () => {}, + pullCloudProfiles: async () => ({ profiles: existingProfiles, activeProfileId: existingProfiles[0]?.id ?? null }), pullCloudPayload: pull ?? (async () => ({ addons: existingAddons, settings: { catalogs: [{ id: 'trending', enabled: true }] } })), saveCloudAddons: async (_auth, addons, profileId) => calls.addons.push({ profileId, count: addons.length, addons }), - saveCloudProfiles: async (_auth, profiles) => calls.profiles.push(profiles.map(p => p.name)), + addCloudProfiles: async (_auth, additions) => { + existingProfiles = [...existingProfiles, ...additions]; + calls.profiles.push(existingProfiles.map(p => p.name)); + return existingProfiles; + }, saveCloudSettings: async (_auth, settings, _addons, profileId, _profiles, baseline) => - calls.settings.push({ profileId, catalogs: settings.catalogs.length, settings, baseline }) + calls.settings.push({ profileId, catalogs: settings.catalogs.length, settings, baseline, profiles: _profiles }) }, './customCollections': load('lib/customCollections.ts'), './nuvioMigration': load('lib/nuvioMigration.ts'), @@ -60,7 +66,7 @@ const auth = { session: { userId: 'u1', email: 'me@arvio.example' } }; const baseSettings = { catalogs: [] }; test('creates the missing profiles and writes each one to the account', async () => { - const { runner, calls } = setup({ existingProfiles: [] }); + const { runner, calls } = setup({ existingProfiles: [{ id: 'p1', name: 'Main' }] }); const result = await runner.applyNuvioImport({ auth, profiles: [{ id: 'p1', name: 'Main' }], snapshot, choices: [ @@ -168,6 +174,8 @@ test('the write is limited to catalogs and carries the account as its baseline', same(writtenRest, JSON.parse(JSON.stringify(baselineRest))); same(write.baseline.catalogs, cloudSettings.catalogs); assert.ok(write.settings.catalogs.length > cloudSettings.catalogs.length); + same(write.profiles, []); + same(calls.profiles, []); }); test('an addon switched off in Nuvio arrives switched off', async () => { diff --git a/web/tests/nuvio-import-safety.test.cjs b/web/tests/nuvio-import-safety.test.cjs index 948309ff6..66a0a4e97 100644 --- a/web/tests/nuvio-import-safety.test.cjs +++ b/web/tests/nuvio-import-safety.test.cjs @@ -20,6 +20,7 @@ const account = () => ({ subtitleAiApiKey: 'the-users-saved-key', subtitleAiModel: 'groq', profiles: [{ id: 'p1', name: 'Main' }], + activeProfileId: 'p1', catalogsByProfile: { p1: [{ id: 'trending_movies', name: 'Trending in Movies', enabled: true, isPreinstalled: true }] }, iptvByProfile: { p1: { playlists: [{ id: 'a', name: 'Home', m3uUrl: 'https://example.invalid/a.m3u' }], favoriteChannels: ['a:1'] } }, addonsByProfile: { p1: [] }, @@ -55,8 +56,8 @@ const defaults = { }; // `failRead` picks which account reads fail, counted in order: 1 is the read -// behind the profile-list write, 2 is the import's own read of the profile. -function setup({ failRead = () => false } = {}) { +// of the profile list, 2 is the import's own read of the profile. +function setup({ failRead = () => false, beforeRead = () => {} } = {}) { let stored = account(); let reads = 0; let writes = 0; @@ -70,6 +71,7 @@ function setup({ failRead = () => false } = {}) { if (options?.method === 'POST') { writes += 1; stored = JSON.parse(options.body).payload; return { accepted: true }; } reads += 1; if (failRead(reads)) throw new Error('backend unreachable'); + beforeRead(stored, reads); return { payload: structuredClone(stored) }; } } @@ -89,7 +91,7 @@ function setup({ failRead = () => false } = {}) { './nuvioMigration': load('lib/nuvioMigration.ts'), './profiles': load('lib/profiles.ts') }); - return { runner, read: () => stored, writes: () => writes }; + return { runner, cloud, read: () => stored, writes: () => writes }; } const auth = { @@ -130,7 +132,7 @@ test('a failed read writes nothing at all', async () => { // One read fails — the import's own read of the profile — while everything // else keeps working. That is the exact shape of the bug: the failure passed // for an empty profile and the write went ahead with the page's defaults. - const { runner, read } = setup({ failRead: (n) => n === 2 }); + const { runner, read, writes } = setup({ failRead: (n) => n === 2 }); const before = account(); const result = await runner.applyNuvioImport({ auth, profiles: [{ id: 'p1', name: 'Main' }], snapshot, choices, baseSettings: defaults @@ -146,6 +148,7 @@ test('a failed read writes nothing at all', async () => { same(after.iptvByProfile, before.iptvByProfile); same(after.addonsByProfile, before.addonsByProfile); assert.equal(after.settings, undefined); + assert.equal(writes(), 0); }); test('an account that cannot be read at all is never written to', async () => { @@ -156,3 +159,64 @@ test('an account that cannot be read at all is never written to', async () => { assert.equal(writes(), 0); same(read(), account()); }); + +test('an unloaded profile list cannot replace existing cloud profiles', async () => { + const { runner, read } = setup(); + await runner.applyNuvioImport({ + auth, profiles: [], snapshot, + choices: [{ nuvioProfileId: 1, target: { kind: 'create' } }], baseSettings: defaults + }); + assert.ok(read().profiles.some(profile => profile.id === 'p1')); + assert.equal(read().profiles.length, 2); + assert.equal(read().activeProfileId, 'p1'); +}); + +test('profiles added by another device survive creation and later settings writes', async () => { + const { runner, read } = setup({ beforeRead: (stored, n) => { + if (n === 2) stored.profiles.push({ id: 'p2', name: 'Kids' }); + if (n === 4) { + stored.profiles.push({ id: 'p3', name: 'Guest' }); + stored.profiles.find(profile => profile.id === 'p1').name = 'Renamed'; + } + } }); + await runner.applyNuvioImport({ + auth, profiles: [{ id: 'p1', name: 'Main' }], snapshot, + choices: [{ nuvioProfileId: 1, target: { kind: 'create' } }], baseSettings: defaults + }); + const profiles = read().profiles; + assert.equal(profiles.length, 4); + assert.equal(profiles.find(profile => profile.id === 'p1').name, 'Renamed'); + assert.ok(profiles.some(profile => profile.id === 'p2')); + assert.ok(profiles.some(profile => profile.id === 'p3')); + assert.equal(read().activeProfileId, 'p1'); +}); + +test('a deleted target profile stops the import before any writes', async () => { + const { runner, read, writes } = setup(); + await assert.rejects(runner.applyNuvioImport({ + auth, profiles: [{ id: 'deleted', name: 'Main' }], snapshot, + choices: [{ nuvioProfileId: 1, target: { kind: 'existing', profileId: 'deleted' } }], baseSettings: defaults + }), /no longer available/); + assert.equal(writes(), 0); + same(read(), account()); +}); + +test('a failed profile creation read never writes a partial list', async () => { + const { runner, read, writes } = setup({ failRead: n => n === 2 }); + await assert.rejects(runner.applyNuvioImport({ + auth, profiles: [], snapshot, + choices: [{ nuvioProfileId: 1, target: { kind: 'create' } }], baseSettings: defaults + })); + assert.equal(writes(), 0); + same(read(), account()); +}); + +test('additive profile writes never replace a matching remote profile', async () => { + const { cloud, read } = setup(); + const profiles = await cloud.addCloudProfiles(auth, [ + { id: 'p1', name: 'Stale name' }, { id: 'p2', name: 'Kids' }, { id: 'p2', name: 'Duplicate' } + ]); + same(profiles, [{ id: 'p1', name: 'Main' }, { id: 'p2', name: 'Kids' }]); + same(read().profiles, [{ id: 'p1', name: 'Main' }, { id: 'p2', name: 'Kids' }]); + assert.equal(read().activeProfileId, 'p1'); +}); diff --git a/web/tests/nuvio-migration.test.cjs b/web/tests/nuvio-migration.test.cjs index afacf91ea..4d757bb0e 100644 --- a/web/tests/nuvio-migration.test.cjs +++ b/web/tests/nuvio-migration.test.cjs @@ -291,3 +291,42 @@ test('matches are recalculated once ARVIO profiles arrive, keeping manual picks' // 'Kids' is taken by the manual choice above, so it cannot be matched again. same(claimed[1].target, { kind: 'create' }); }); + +test('failed collections and home settings reads are reported, not treated as empty', async () => { + const discovery = { backendUrl: 'https://api.example.com', publishableKey: 'pk' }; + const snapshot = await fetchNuvioSnapshot(discovery, { accessToken: 't', userId: 'u', email: 'u@e.c' }, async (url) => { + if (url.includes('/profiles')) return jsonResponse([{ profile_id: 1, name: 'Main' }]); + if (url.includes('/collections') || url.includes('/home_catalog_settings') || url.includes('/rpc/')) { + return jsonResponse({ message: 'unreachable' }, false, 503); + } + return jsonResponse([]); + }); + assert.match(snapshot.warnings.join(' '), /collections for profiles 1/); + assert.match(snapshot.warnings.join(' '), /home_catalog_settings for profiles 1/); +}); + +test('a successful empty fallback is empty without a warning', async () => { + const discovery = { backendUrl: 'https://api.example.com', publishableKey: 'pk' }; + const snapshot = await fetchNuvioSnapshot(discovery, { accessToken: 't', userId: 'u', email: 'u@e.c' }, async (url) => { + if (url.includes('/profiles')) return jsonResponse([{ profile_id: 1, name: 'Main' }]); + if (url.includes('/collections') || url.includes('/home_catalog_settings')) return jsonResponse({}, false, 404); + return jsonResponse([]); + }); + same(snapshot.warnings, []); +}); + +test('a partial fallback retains successful profiles and identifies the failed profile', async () => { + const discovery = { backendUrl: 'https://api.example.com', publishableKey: 'pk' }; + const snapshot = await fetchNuvioSnapshot(discovery, { accessToken: 't', userId: 'u', email: 'u@e.c' }, async (url, init) => { + if (url.includes('/profiles')) return jsonResponse([{ profile_id: 1, name: 'Main' }, { profile_id: 2, name: 'Kids' }]); + if (url.includes('sync_pull_collections')) { + return JSON.parse(init.body).p_profile_id === 1 + ? jsonResponse([{ collections_json: [{ title: 'Studios', folders: [] }] }]) + : { ok: true, json: async () => { throw new Error('bad JSON'); } }; + } + return jsonResponse([]); + }); + assert.equal(snapshot.profiles[0].collectionsJson.length, 1); + assert.match(snapshot.warnings.join(' '), /collections for profiles 2/); + assert.equal(snapshot.warnings.some(warning => warning.includes('home_catalog_settings')), false); +}); diff --git a/web/tests/nuvio-ui/entry.tsx b/web/tests/nuvio-ui/entry.tsx new file mode 100644 index 000000000..e08c2d815 --- /dev/null +++ b/web/tests/nuvio-ui/entry.tsx @@ -0,0 +1,4 @@ +import { createRoot } from "react-dom/client"; +import { MigrateApp } from "../../components/migrate/MigrateApp"; + +createRoot(document.getElementById("root")!).render(); diff --git a/web/tests/nuvio-ui/stubs.tsx b/web/tests/nuvio-ui/stubs.tsx new file mode 100644 index 000000000..a546e5ce6 --- /dev/null +++ b/web/tests/nuvio-ui/stubs.tsx @@ -0,0 +1,46 @@ +import type { ReactNode } from "react"; + +const snapshot = { + email: "demo@nuvio.example", + warnings: ["Nuvio did not return collections for profiles 1 - retry the import."], + profiles: [{ profileId: 1, name: "Main", addons: [], plugins: [], collectionsJson: null, homeCatalogSettings: null }] +}; + +export const authClient = { session: { email: "demo@arvio.example" } }; +export const defaultSettings = {}; +export const CONTENT_LANGUAGE_OPTIONS = [["en-US", "English"]]; +export const LanguageProvider = ({ children }: { children: ReactNode }) => children; +export const useTranslation = () => (value: string) => value; +export const loadStored = (_key: string, fallback: unknown) => fallback; +export const saveStored = () => {}; +export const installAddon = async () => { throw new Error("Unexpected addon installation"); }; +export const normalizeAddons = (value: unknown) => value; +export const saveCloudAddons = async () => { throw new Error("Unexpected addon write"); }; +export const OFFICIAL_NUVIO_URL = "https://nuvio.invalid"; +export const discoverNuvioBackend = async () => ({}); +export const signInToNuvio = async () => ({}); +export const fetchNuvioSnapshot = async () => snapshot; +export const defaultChoices = () => [{ nuvioProfileId: 1, target: { kind: "create" } }]; +export const reconcileChoices = (_snapshot: unknown, _profiles: unknown, choices: unknown) => choices; +export const summarizePlan = () => snapshot.profiles; + +let reads = 0; +const controls = { + imports: 0, + resolveProfiles: null as null | (() => void) +}; +Object.assign(window, { nuvioTest: controls }); +export const pullCloudProfiles = async () => { + reads += 1; + if (reads === 1) throw new Error("Account unavailable"); + if (reads === 2) await new Promise(resolve => { controls.resolveProfiles = resolve; }); + return { profiles: [{ id: "p1", name: "Main" }], activeProfileId: "p1" }; +}; +export const pullCloudPayload = async () => ({ addons: [] }); +export const applyNuvioImport = async () => { + controls.imports += 1; + return { + profiles: [{ id: "p1", name: "Main" }], + summaries: [{ profileName: "Main", created: false, addons: 0, addonsFailed: 0, addonsDisabled: 0, collections: 0, plugins: 0 }] + }; +}; From d7009fa08a3ab16929174416ed3156b7307793d1 Mon Sep 17 00:00:00 2001 From: Arvin Date: Fri, 9 Oct 2026 16:35:30 +0200 Subject: [PATCH 4/4] test(web): keep Nuvio UI check independent of packaged manifests --- web/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/web/package.json b/web/package.json index 90cdc545c..2ec5fa11a 100644 --- a/web/package.json +++ b/web/package.json @@ -13,7 +13,6 @@ "setup:selfhost": "node scripts/selfhost.mjs setup", "check:selfhost": "node scripts/selfhost.mjs check", "test": "node --test tests/*.test.cjs", - "test:nuvio-ui": "node scripts/check-nuvio-ui.cjs", "test:library-ui": "node scripts/check-library-ui.cjs", "test:calendar-ui": "node scripts/check-calendar-ui.cjs", "test:player-sources-ui": "node scripts/check-player-sources-ui.cjs",