feat(tvos): fetch Top Shelf content live and show poster art

The Top Shelf extension now fetches Continue Watching directly from
Plex/Jellyfin/Emby instead of replaying a cache the app wrote on its
last foreground Discover pass. The app publishes per-profile server
descriptors on every shelf sync (`updateSources`): token-free metadata
in the app group, tokens in an app-group-shared keychain item, both
wiped by `clear`. On success the extension rewrites the cached payload
as the offline fallback; any fetch failure falls back to the previous
cache-replay behavior. Poster images are passed as remote URLs, so the
extension no longer depends on app-side artwork downloads.

Episodes now render season/series poster art (2:3, `.poster` shape)
instead of 16:9 episode stills, and labels lead with the S/E marker so
long titles no longer hide it behind the focused-item marquee. Shelf
schema v3 (Dart, Android, tvOS envelopes bumped together) discards
stale wide-art caches instead of letterboxing them into poster slots.

close #1474
close #1835
This commit is contained in:
edde746
2026-08-09 10:59:16 +02:00
parent 0b4fd9e8f3
commit 291a22a4a4
14 changed files with 1605 additions and 31 deletions
@@ -24,7 +24,7 @@ class WatchNextPlugin() :
companion object {
private const val TAG = "WatchNextPlugin"
private const val METHOD_CHANNEL = "com.plezy/watch_next"
private const val SCHEMA_VERSION = 2
internal const val SCHEMA_VERSION = 3
private var pendingDeepLink: String? = null
fun handleIntent(intent: Intent?): String? {
@@ -291,7 +291,7 @@ class WatchNextProviderTest {
MethodCall(
"sync",
mapOf(
"schemaVersion" to 2,
"schemaVersion" to 3,
"ownerId" to "active-owner",
"generation" to 1L,
"items" to listOf(mapOf("contentId" to "active", "title" to "Active"))
@@ -308,7 +308,7 @@ class WatchNextProviderTest {
MethodCall(
"sync",
mapOf(
"schemaVersion" to 2,
"schemaVersion" to 3,
"ownerId" to "queued-owner",
"generation" to 2L,
"items" to listOf(mapOf("contentId" to "queued", "title" to "Queued"))
@@ -1281,7 +1281,7 @@ class WatchNextProviderTest {
private fun syncCall(ownerId: String, generation: Long) = MethodCall(
"sync",
mapOf(
"schemaVersion" to 2,
"schemaVersion" to 3,
"ownerId" to ownerId,
"generation" to generation,
"items" to emptyList<Map<String, Any?>>()