Skip to content

Free setup page: install addons and move a Nuvio account into ARVIO - #774

Merged
ProdigyV21 merged 5 commits into
ProdigyV21:mainfrom
test01203:feature/web-nuvio-migration
Oct 9, 2026
Merged

ProdigyV21 merged 5 commits into
ProdigyV21:mainfrom
test01203:feature/web-nuvio-migration

Conversation

@test01203

@test01203 test01203 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Not run against real accounts. This has not been run against a real Nuvio account or a real ARVIO account — I have access to neither. Everything below was verified against stubs, including the real cloud write path. It is submitted for the maintainers to review and run.

A user moving from Nuvio has no way to carry their setup across: the web app sits behind the subscription, and the Android/TV apps — which are free — have no importer. This adds /migrate, a standalone page outside the subscribed shell, so someone who only uses the free apps can still set their account up.

The page does three things:

  1. Read a Nuvio account — profiles, addons, collections. No ARVIO account needed for this step, and the result can be downloaded as JSON.
  2. Sign in to ARVIO — only required to write anything.
  3. Install addons directly by manifest URL, per profile.

Everything written goes into the ARVIO cloud payload, which the free Android and TV apps already sync, so the result shows up on a TV without a web subscription. Nothing in Nuvio is modified; it is only read.

Review what moves Done
Review Finished

Connecting to Nuvio

The public Nuvio cloud publishes its client settings at https://api.nuvio.tv/.well-known/nuvio, so the page asks only for the user's Nuvio email and password — no server address, and no Nuvio key is embedded in this repo: the publishable key is read at connect time, which also means a rotated key keeps working. Self-hosters can name their own server behind an "I host Nuvio myself" link. The password is used once for the token exchange and never stored; the access token lives in memory for the length of the import.

Data is read from the documented tables — profiles, addons, plugins, collections, home_catalog_settings. Because row-level security can leave a direct table read empty, 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.

How it lands in ARVIO

  • Profiles — matched to an ARVIO profile by name, otherwise created, so a multi-profile account arrives whole and a repeated import does not duplicate anything. Colour and avatar carry over; a Nuvio PIN hash is not portable, so a copied profile starts unlocked rather than locked out.
  • Addons — each manifest URL is resolved into a real addon entry. Ones already on the profile are left alone; an unreachable addon is reported in the summary instead of aborting the run.
  • Collections — handed to the existing parseCustomCollections, the same path the Nuvio collections import already uses.
  • Home rows — Nuvio's order and hidden rows are applied where they line up with ARVIO's catalog ids.
  • Plugins — counted and shown as staying behind, since the web app has no plugins.

Interface is English by default, with a language picker saved per device that reuses the app's existing dictionaries.

Test plan

  • npx tsc --noEmit
  • tests/nuvio-migration.test.cjs — 15 tests: address forms, discovery parsing, sign-in error surfacing, grouping by profile, a missing table, the RPC fallback, colour/avatar conversion, name matching, addon de-duplication, row order.
  • tests/nuvio-import-runner.test.cjs — 7 tests with the cloud layer stubbed: profile creation, skipped profiles writing nothing, no double-install, refusing to run without a session, a failed read skipping that profile, the write being limited to catalogs, and a disabled addon staying disabled.
  • tests/nuvio-import-safety.test.cjs — 3 tests against the real cloud helpers with a stubbed backend: an import leaves every settings field it does not own untouched (theme, AI subtitle key, IPTV playlists), a failed read writes nothing at all, and an account that cannot be read is never written to.
  • npm test — 779 passing; only provider JSON/XML/playlist text survives incorrect octet-stream MIME types fails, and it fails the same way on a clean main (pre-existing, unrelated).
  • Manual run of the whole flow in a browser against a stub Nuvio server: 2 profiles found, one matched by name and one created, addons installed, collections imported, summary as in the screenshots.
  • https://api.nuvio.tv/.well-known/nuvio verified to return backend_url and publishable_key, which is what removes the server field.
  • A full browser run with the ARVIO backend stubbed at the network layer, so the real cloud.ts read/modify/write path executed: an account that started with an accent colour, AI subtitles with a saved key, an IPTV playlist and one catalog came out of the import with all of those unchanged, the imported collection rails added, and an addon that was off in Nuvio installed off.
  • Not run against a real Nuvio account or a real ARVIO account. That is the remaining gap.

Notes for review

  • The page is deliberately not linked from the subscribed app; say the word and I will add an entry in Settings.
  • The screenshots were taken against a stub Nuvio server with the ARVIO backend stubbed in the browser, so the account data in them is fake. They predate the server field being removed from the connect step and the review fixes below, so the summary lines in them are missing the newer counts.

Review fixes (790db45)

All five findings from the review are addressed: the write is limited to catalogs and carries the account's own settings as its baseline; a failed cloud read stops that profile and writes nothing instead of passing for an empty profile; the profile plan is recalculated once ARVIO profiles load, keeping manual picks; profile ids are recovered from every table and RPC that answered, and a failed read is distinguished from an empty one and surfaced; and an addon disabled in Nuvio is installed disabled.

@github-actions github-actions Bot added documentation Improvements or additions to documentation area: web Changes to web or Netlify sites labels Oct 2, 2026
@ProdigyV21

Copy link
Copy Markdown
Owner

This needs testing before I can merge. Also it is questionable if it is really needed.

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.
@test01203
test01203 force-pushed the feature/web-nuvio-migration branch from e4de28f to d34d443 Compare October 2, 2026 11:11
@test01203

Copy link
Copy Markdown
Contributor Author

Fair on both points — let me take the testing one first, because it is the blocker.

Testing. You are right that this is unverified, and I have said so at the top of the description rather than letting it look finished. What I could test, I did: 15 unit tests (the Nuvio read, the RPC fallback, profile matching, addon de-duplication) and a full manual run of the flow in a browser against a stub Nuvio server, which is what the screenshots show. What I could not test is the only part that matters for merging: the real api.nuvio.tv read and the cloud writes in nuvioImportRunner.ts, because I have neither a Nuvio account nor an ARVIO account to write into. If you can point a test Nuvio account at it — or run it once yourself against a throwaway ARVIO account — that closes the gap quickly. The writes all go through saveCloudAddons / saveCloudSettings / saveCloudProfiles, so the blast radius is one account's payload.

Whether it is needed. The case is about who it is for, not about the web app:

  1. The web app is behind the subscription; the APK is not. A free Android/TV user currently has no way to manage their account from a browser at all — addons have to be typed in on a TV remote. This page is a free browser-side setup surface for an account the apps already sync: install an addon by URL, import collections, create profiles. The Nuvio import is one feature on it, not the whole point.
  2. Switching from Nuvio is manual today. Someone with a handful of addons and a few collections across 3-4 profiles has to re-enter all of it by hand, on a TV. That is the step where people give up and stay where they are. This turns it into email, password, pick a target per profile.
  3. It costs the project very little. No new sync surface — it writes the existing cloud payload. No Nuvio key is embedded: api.nuvio.tv publishes /.well-known/nuvio, so the publishable key is read at connect time and a rotation keeps working. Nothing in Nuvio is modified; it is read-only there, with the user's own credentials, used once for the token exchange and never stored.

If the judgement is still that the migration half is not wanted, the addon/collection management half stands on its own and I am happy to split the PR so you can take only that. And if you would rather it lived somewhere other than /migrate, or want it linked from Settings, say where and I will move it.

@ProdigyV21

Copy link
Copy Markdown
Owner

Thanks for the contribution and the explanation. I can see the value in easier browser setup and moving from Nuvio, especially for TV users. I would like to keep exploring this, but a few things need fixing before merging:

  1. Creating a new imported profile passes defaultSettings into saveCloudSettings with no baseline. That writes unrelated global settings too, resetting things like the theme, AI subtitle toggle and saved AI key. Please limit the import to the intended fields and preserve existing account settings.
  2. A failed cloud read is treated as an empty profile and the import continues. I reproduced this replacing existing catalogs and clearing IPTV playlists. Please stop that profile's import on a read failure rather than writing defaults.
  3. Connecting Nuvio before signing into ARVIO leaves the profile choices set to create new profiles, even after matching ARVIO profiles load. Recalculate the initial matches after sign-in without overwriting choices the user has already made.
  4. When Nuvio's profile table is unavailable or empty, the fallback reads data for profiles 1-6 but only builds Profile 1. Please recover the other profile IDs from the returned data so their addons/collections are not silently skipped, and distinguish failed reads from genuinely empty results.
  5. Disabled Nuvio addons are installed as enabled. Please preserve their enabled state, or make enabling them an explicit choice.

TypeScript checks and all 770 web tests passed, but I reproduced the first two problems using the actual cloud helpers with a mocked backend. After fixing these, please add regression tests and verify a real disposable Nuvio-to-ARVIO import, including that existing account settings/IPTV remain intact and the imported content appears on Android/TV. The idea is useful; protecting existing account data is the main blocker here.

@test01203

Copy link
Copy Markdown
Contributor Author

Thanks for actually running it — these are real, and 1 and 2 are the kind of bug that would have cost someone their setup. Protecting existing account data being the blocker is the right call. Taking them in order:

1. defaultSettings into saveCloudSettings. Agreed, that is the worst one: it writes a whole settings object for fields the import has no business touching, so theme, the AI subtitle toggle and the saved key get reset. Fix: build the write from the profile's existing cloud settings, change only catalogs, and pass those same existing settings as the baseline argument so untouched fields are never asserted. For a brand-new profile there is nothing to preserve, so it writes only the catalog list and leaves the rest unset rather than seeding defaults.

2. A failed read treated as an empty profile. My pullCloudPayload(...).catch(() => null) cannot tell "this profile has nothing yet" from "the request failed", and then the merge writes defaults over real data. Fix: let the failure propagate, stop that profile's import, and record it in the summary as failed instead of writing anything. Other profiles continue; nothing is written for the one that could not be read.

3. Choices not recalculated after ARVIO sign-in. Right — connecting Nuvio first (which is the order I moved it to) means defaultChoices runs against an empty profile list, so everything says "create new" even once matching profiles load. Fix: recompute matches when the profile list arrives, and merge rather than replace — a choice the user has already changed by hand stays as they set it.

4. The 1-6 fallback only building Profile 1. Correct, and it silently drops the other profiles' content. Fix: derive the profile ids from what the RPC calls actually returned (and from the addon/plugin rows) and build a profile for each, instead of assuming one. I will also separate the two cases you are pointing at: a read that failed is not an account that is empty, and only the genuinely empty case should fall back.

5. Disabled addons installed as enabled. Fix: carry Nuvio's enabled flag onto the installed addon. An addon the user had switched off in Nuvio arrives switched off.

Regression tests for all five, including one that asserts an unrelated settings field survives an import and one that asserts a failed read writes nothing at all.

On verification: I can cover the above with tests against the real cloud helpers the way you did, but a true end-to-end run needs a disposable ARVIO account to write into, which I do not have — if you can point me at one (or run it once yourself after the fixes land), I will confirm settings and IPTV stay intact and that the imported rows show up on Android/TV before you look again.

Review findings on ProdigyV21#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 <noreply@anthropic.com>
@test01203

Copy link
Copy Markdown
Contributor Author

All five are fixed and pushed (790db45). What changed, in the same order:

1. Unrelated global settings. The write no longer starts from the page's defaults. The import reads the profile's current cloud settings, changes catalogs and nothing else, and passes those same settings back as the baseline argument — so saveCloudSettings sees that this session changed only the catalog list and never asserts or timestamps the rest. That applies to a newly created profile too: the global fields behind it (theme, AI subtitle settings, saved key) belong to the account, not the profile, so they are read and handed back the same way rather than seeded with defaults.

2. Failed read. pullCloudPayload(...).catch(() => null) is gone. A read failure stops that profile's import before anything is written, and the summary shows "Not imported — . Nothing was changed on this profile." The other profiles continue. If the account cannot be read at all, the import stops without a single write.

3. Choices after sign-in. reconcileChoices recomputes the plan whenever the ARVIO profile list changes, and merges instead of replacing: a target the user picked by hand is kept, and the profile they claimed is excluded from name matching so it cannot be handed to a second Nuvio profile.

4. The 1-6 fallback. Profile ids are now recovered from every table that carries profile_id — the addon and plugin rows and whatever the two sync_pull_* RPCs answered for — and a profile is built for each. Reads also distinguish a request that failed from a table that answered with nothing: a failed profiles read falls back and says so in the UI ("Nuvio's profile list could not be read, so profiles were recovered from their content"), a failed addons/plugins read is reported rather than passing for "you have none", and an empty table is just empty.

5. Disabled addons. The enabled flag travels with the addon, so one switched off in Nuvio arrives switched off (enabled and isEnabled both false). It is also shown before the import ("1 switched off in Nuvio") and after ("1 left switched off").

Tests

tests/nuvio-import-safety.test.cjs is new and runs against the real cloud helpers with a stubbed backend, because that is the layer both data-loss bugs lived in — the import code looked fine and the damage happened inside saveCloudSettings:

  • an import leaves every settings field it does not own untouched — theme, OLED black, AI subtitles, the saved AI key and the profile's IPTV playlists and favourites all come out of the push byte-identical, while the account's existing catalog survives alongside the imported collection;
  • a failed read writes nothing at all — the import's own read of the profile fails while every other request succeeds, and the pushed account still has its catalogs, its IPTV and its settings exactly as before;
  • an account that cannot be read at all is never written to — zero pushes.

Both of the first two fail against the previous code and pass against this one; I checked by reverting each fix in turn.

Also added: profile recovery from a failed profiles read, recovery from the RPCs alone, profileIdsFromRows, the recalculation after sign-in including the manual-pick case, and the enabled flag surviving an install. npx tsc --noEmit clean; npm test is 779 passing with the one pre-existing provider JSON/XML/playlist text survives incorrect octet-stream MIME types failure that also fails on a clean main.

End-to-end run

I ran the whole flow in a browser against a stub Nuvio server, with the ARVIO backend stubbed at the network layer so the real cloud.ts read/modify/write path executed against an account that started with an accent colour, AI subtitles on with a saved key, an IPTV playlist and one catalog. Connecting Nuvio first showed both profiles as "create new"; signing in afterwards flipped them to the matching Profile 1 and Kids without me touching them. After the import the pushed account had:

  • accentColor, subtitleAiEnabled and subtitleAiApiKey unchanged;
  • the IPTV playlist unchanged;
  • trending_movies still there, with the imported collection rails added next to it;
  • OpenSubtitles v3 installed with enabled: false, as it was in Nuvio.

That is as far as I can take it without an account. A real disposable Nuvio-to-ARVIO run against api.nuvio.tv and a throwaway ARVIO account is still the piece I cannot do — if you can hand me one, or run it once yourself now the fixes are in, I will confirm the Android/TV side picks the rows up.

@ProdigyV21
ProdigyV21 merged commit fe01c35 into ProdigyV21:main Oct 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: web Changes to web or Netlify sites documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants