github vatax3/NuvioTVOS v1.0.5
Nuvio for Apple TV 1.0.5

latest releases: v1.0.44, v1.0.43, v1.0.42...
one month ago

Collections are the real feature now, and thirty percent of the settings screen
stopped lying. Sideload over 1.0.3; the About screen prints 1.0.5 (7).

This release also carries 1.0.4, which was built and never published. Nothing
is missing from the list below because of it.

Read this first if you made collections

Collections you built by hand are gone from the app. Not deleted — the old
file is moved aside inside the container rather than removed — but nothing can
read it any more, and there is no import path back.

That is because our collections and the Android app's were never the same
feature. Ours was a named folder holding a list of titles you had bookmarked.
Theirs is a collection containing folders, each folder containing
sources, where a source is a live query: an addon catalog optionally narrowed
by a genre, a TMDB request, a Trakt list. A folder fills itself and keeps filling
itself.

Inventing a fourth "manual list" source type would have put the shared format
straight back into the divergence described below, so the manual kind is gone
rather than bridged.

The part that was quietly destroying data

Collections sync through your Nuvio account as a single collections_json blob,
and the two payloads had nothing in common.

Theirs would not decode here at all — Swift's JSONDecoder is strict, name was
absent, the pull was discarded. Ours parsed there into titleless empty
collections, because Gson is lenient and fills in nulls.

So on one account, whichever app synced last destroyed the other's
collections.
If you have been using both, that is what was happening.

The shape now matches upstream field for field, including the legacy
catalogSources mirror that older Android builds still read. Eleven contract
tests pin it: field survival, per-provider decoding, a provider-less source
defaulting to addon, unknown enum values falling back rather than throwing, the
required fields their importer validates, and the exact upper/lower casing each
enum uses.

Two details that would have broken interoperability in silence. Sources encode
flat, discriminated by provider — the idiomatic Swift encoding produces
{"tmdb":{…}}, which Android reads as an empty addon source. And addonId is
the manifest id, not the base URL, which differs between installations and
would not survive the trip.

Untested, and I would rather say so: an export from the Android app pasted
into the import screen, exported back and compared. That needs the Android app
and an account. The tests are written against the shape in their
CollectionsDataStore.kt, not against a live capture.

What collections do

A collection appears as a rail on Home and in the Library, one card per folder.
Opening a folder shows its sources — as tabs over a grid, as rows, or following
your home layout, whichever the collection is set to.

Build them in Settings → Sources → Collections. Import and export are there
too, in the same JSON the account uses.

Addon-catalog sources need nothing configured and are the place to start. TMDB
and Trakt sources need their keys in Integrations. A folder whose sources are all
unreachable says which key is missing instead of rendering as an empty rail.

The TMDB source editor exposes all eighteen discover filters — genres, dates,
ratings, language, origin, keywords, studios, networks, watch providers.

Not drawn on Apple TV: focusGifUrl and heroVideoUrl. An animated cover per
card needs a video layer per tile. Both are parsed, kept and written back
unchanged, so a round trip through this app does not strip them from the others.

Thirty percent of the settings screen did nothing

An audit of the previous build asked the tree a mechanical question: which
settings can you change that change nothing? 243 settings were declared and 73
had no reader.
Eleven of those had a live control — a switch you could throw
and watch nothing happen.

Eight are now wired.

Forced subtitles turned out to mean something narrower than its label, and it
is worth knowing which: when the audio is already in the language you read
subtitles in, a full subtitle track just repeats dialogue you can hear — so only
a forced track comes on, and where there is none, nothing does. When the audio
is in another language the rule inverts, and forced tracks are the ones excluded.
Addons do not flag forced tracks, so the word is looked for in the id, the URL
and the addon name, the same way upstream does it.

Watch progress from Trakt now works. The client method for it had existed
since the port began, with a comment saying what it was for and no caller at all.
A source pointing at an account you never signed into falls back to this device
rather than showing you an empty screen, and the picker only offers accounts you
have actually connected.

More like this honours its source — addon catalog, TMDB, or Trakt's related
titles. All three fall back to the catalog rather than leaving the row empty; an
empty row reads as "nothing is like this" rather than "that account is not
connected".

Continue Watching drops rows older than your cap, and fills in missing
episode stills from TMDB so every episode of a show stops looking identical.

TMDB episode data fills episode titles, overviews, stills and air dates,
per season, as you select it. Blanks only — the addon knows which cut it is
serving and TMDB does not.

Instant playback preparation resolves the first few cached torrents
through debrid before you pick one. Only cached ones: asking debrid for an
uncached torrent starts a download on your account, which is not something to do
speculatively on your behalf.

The single-column settings layout exists, as the alternative to the two-pane
rail.

Three were removed instead of implemented, because implementing them here would
have been a promise this platform cannot keep. Hero trailers and focused-poster
trailers cannot exist on Apple TV
: Stremio trailers are YouTube ids, tvOS has no
WKWebView, and AVPlayer cannot resolve a YouTube watch page — which is exactly
why the Trailer button hands off to the YouTube app rather than playing anything
itself. Playback issue reports upload to Nuvio's own backend, whose contract
belongs to the Android project.

Settings that had no control either

The other ~60 were stored, synced, and referenced nowhere. Five had real meaning
here and were wired up:

  • Subtitle picker grouping — by language, by addon, or flat. The engine had
    supported all three since the port began; nothing was telling it which to use,
    so everyone got "by language" whatever they picked.
  • Default aspect ratio. The player's Display button already cycled seven
    modes; your default went nowhere.
  • Send the subtitle track to an external player. Infuse and VLC accept one on
    their callback; without this a hand-off silently dropped the track you had
    chosen. nPlayer and Outplayer take a bare URL, and the row says so.
  • Keep the same release across episodes. The next episode plays from the
    source you were already watching — same encode, same audio, same subtitle
    timing — instead of reopening the choice.
  • Wait before auto-playing (see below).

The remaining 52 were deleted. That is safe, and it was checked rather than
assumed: the preference layer syncs its raw storage, not its declared
properties, so a key arriving from Android round-trips whether or not any Swift
property names it. There is a test for exactly that, because the deletions now
depend on it.

Two behaviour changes you will notice

Source auto-play now waits three seconds before choosing for you, which is
the upstream default and your chance to pick something else. It was instant
before. Settings → Playback → Source auto-play → Wait before starting, and
"Immediately" restores the old behaviour.

A first-run walk-through appears on a fresh install: experience mode, home
layout, suggested add-ons. Three Android screens as one screen with three steps,
so Back returns to the previous step rather than leaving a half-configured app.
Upgrades do not see it.

What stops this happening again

The mechanism mattered more than any single finding. The settings stores were
ported from Android's DataStores wholesale, ahead of the features meant to read
them
, and the settings screens were generated from the stores. An inert control
was the default outcome, not an oversight — which is why eleven of them existed
and why nobody noticed.

The build now fails when a declared setting has no reader. It distinguishes a
value somebody acts on from a binding merely being drawn — a setting that only
ever appears with a $ in front of it is a switch wired to nothing. Both failure
modes were introduced deliberately and confirmed to fail the check before being
removed again.

Verification

113 unit tests and 8 UI tests pass.

Not verified, because this machine has no accounts for them: the Trakt progress
fetch, the TMDB episode and enrichment calls, and debrid pre-resolution. Their
pure logic is tested; the network trip is not. Neither is the Android collections
round trip, as above.


Sideloading. Unsigned. Sign it with your own certificate and install with
Xcode, Sideloadly or your usual tool. Requires tvOS 17 or later.

Don't miss a new NuvioTVOS release

NewReleases is sending notifications on new releases.