Delete declarations that Periphery reports unused on every platform that
compiles them (iOS, macOS, tvOS), including nine views used only by their
own previews. Platform-gated code still used on another platform, device-
only MPV render paths, migration models, CloudKit record names, App
Intents metadata and unused parameters are kept.
Open Link gets a Load action that resolves one link and shows its video
details page, playlist or channel screen instead of only playing or
downloading it. Play and Download stay as shortcuts for video links.
Timed links start at their timestamp from the details page, the Show
Details link action opens the details page, and a short extraction cache
avoids re-extracting the same link for details and playback.
Ten review findings in the remote-playlist work, all of them the same
shape: something acted on what it remembered instead of what the server
says now.
- A confirmed-gone playlist is forgotten whole — snapshot, list entry,
and both ordering fences — so a refresh that started before the delete
can no longer republish it, and a list refetch that fails offline no
longer leaves it on screen.
- `.accountsDidChange` discards account-scoped content when the account
it belonged to is no longer the effective one, as the status
notification already did.
- A tab that stops being offered no longer stays selected: the browse
view moves to the first available one, which reloads by itself.
- A Handoff account id is accepted only when that account is on the
instance the playlist lives on.
- Composite writes moved up into `AccountService`, over a transport whose
every method is exactly one request: create-with-privacy, rename, and
set-description. The credential is rechecked between each pair, so a
sign-out between the two halves of a create or an edit can no longer
send the second half under the previous user's secret.
- Editing a playlist no longer forces it private. Invidious serialises
visibility as `isListed` — `privacy.public?`, so unlisted and private
are indistinguishable — which the snapshot now carries; the picker
starts there, and sends nothing at all unless it was moved. Invidious
keeps the stored visibility when the PATCH omits it (measured), so an
untouched picker leaves it exactly as it was.
- Tapping Edit on a server-side playlist awaits an authenticated refetch
first and populates the form from it; a failed refetch shows an error
and no sheet.
- Playlist rows are keyed by row rather than by video id, which two rows
of the same video share.
- The add-to-playlist sheet offers only accounts whose instance is
enabled and that hold a credential, alongside the write capability.
A handful of call sites built their own InvidiousAPI or PipedAPI over the
shared HTTP client, which quietly opted them out of everything an instance
may require of a request: the reverse-proxy basic auth, the certificate
policy, and the User-Agent some instances whitelist and answer 401 without.
The feed on an instance behind a proxy could load in one place and fail in
another for no reason the user could see. They now all go through the
service that already configures a client per instance.
Login was the site that had noticed. It reproduced the basic-auth handling
itself, including the header Invidious' login needs passed explicitly
because that POST runs on its own session to read the SID out of a cookie.
That logic moves into the service verbatim rather than being dropped: the
view now asks for a credential and is told nothing about how it was
obtained.
Paging is the one thing the service could not already express. Invidious
feeds have pages and Piped feeds do not, so the paged variant says so in
its return value instead of leaving each caller to know.
The browse view's subscriptions arrive as channels now rather than as raw
Invidious rows, so the avatar enrichment works over channels — a fetched
or cached avatar still wins over whatever the listing carried, which is
the precedence the conversion applied before.
Reading a server-side playlist was all the app could do: the write half of
both APIs was missing, and the only thing that knew which credential to use
was the subscription layer. So playlists arrive here as an account concern —
hand AccountService an account and it finds the instance, reads the secret,
refuses what the backend cannot do, and records what the answer implies
about the login.
Capabilities decide, never the instance type: a privacy setting sent to a
backend that has none is dropped rather than refused, because the playlist
is still perfectly creatable there. Every call captures the credential it
used and re-reads it before recording an outcome, so a late 401 cannot
reject the login the user just fixed, nor a late success clear a rejection
it never earned.
Removal is where the backends stop being alike. Invidious names the row
itself with an indexId that survives refetches, so it removes directly.
Piped removes by position, and a position is only true of the fetch that
minted it — so the playlist is refetched and the position checked to still
hold the same video before anything is deleted. That narrows the window; it
cannot close it, and only the refetch afterwards shows what really happened.
The cache exists so that refetch cannot be forgotten. Every mutation goes
through it and republishes what it touched — on failure as much as on
success, because a write that reported failure may still have landed — and
nothing is ever edited optimistically. It is in memory alone: the data
belongs to a login rather than to this device, and a stored Piped entry id
would be aimed at whatever occupies that slot next launch. A per-account
generation fences results that come back after the account was cleared,
since clearing has never cancelled the request that was filling it.
Along the way, clients minted per instance sent no User-Agent at all —
instances that whitelist the Yattee prefix answered them with a 401 the
shared client never earned. The factory now bakes the app's User-Agent in
at construction, where no request can outrun it.
The Sources -> Edit Source -> Proxy toggle now renders for Yattee
Server entries (supportsVideoProxying gains .yatteeServer). When the
toggle is on, playback fetches go through
videoWithProxyStreamsAndCaptionsAndStoryboards with mode .relay, so
the server returns signed /proxy/relay URLs (byte-relay, supports
HTTP Range, no on-disk caching). Downloads keep going through mode
.download so the server-side /proxy/fast/ flow continues to cache
files for repeat use.
InvidiousAPI.proxyStreamsIfNeeded early-returns for .yatteeServer
since proxying is now done at fetch time via ?proxy=true rather than
client-side host rewriting.
Replace the YatteeServerAPI setAuthHeader/buildHeaders pattern (which was
race-prone on the shared actor across multiple instances) with a generic
mechanism: HTTPClient now supports a defaultHeaders dictionary applied to
every request, and ContentService builds a per-instance HTTPClient with the
basic-auth Authorization header baked in whenever credentials are configured.
The same code path now works uniformly for Invidious, Piped, PeerTube, and
Yattee Server, so any instance sitting behind a reverse proxy that requires
HTTP Basic Auth can be authenticated regardless of backend type. Cached
default API actors are still reused when no basic-auth header is needed.
Renames YatteeServerCredentialsManager → BasicAuthCredentialsManager so the
same Keychain-backed username/password storage can be reused for any instance
type that sits behind a reverse proxy requiring HTTP Basic Auth. Adds a
one-time migration that moves existing items from the legacy
'com.yattee.yatteeserver' Keychain service to 'com.yattee.basicauth',
preserving the iCloud-sync attribute. No behavior change for end users.
Downloads were using direct YouTube CDN URLs even when proxiesVideos
was enabled. Apply the same proxyStreamsIfNeeded used by the player
to the download code path in ContentService.