fix(subtitles): apply downloaded subtitles instead of polling the stale metadata cache
In-player subtitle search always ended with "Subtitle downloaded, but it
could not be selected": the apply flow polls getVideoPlaybackData for the
new external stream, but the fresh-cache-first fast path (be9197ed) served
the pre-download /library/metadata row for every iteration — playback
start leaves a fresh row, and the first network poll re-stamps it, so the
new stream was never observed and the flow timed out.
Add forceRefresh to PlexClient.getVideoPlaybackData (mirroring
getPlaybackExtras) and pass it from the download poll loop. The forced
fetch still rewrites the cache row, so the subsequent source switch sees
the new track.
This commit is contained in:
@@ -1922,11 +1922,18 @@ class PlexClient
|
||||
/// Get consolidated video playback data in one cache-aware API call.
|
||||
/// Request/decode failures throw. Only a valid absent metadata/media/part
|
||||
/// shape returns an aggregate without a playable URL.
|
||||
///
|
||||
/// [forceRefresh] skips the fresh-cache fast path so server-side stream
|
||||
/// changes become observable. Required by pollers (the OpenSubtitles
|
||||
/// download flow watches for a new external stream to appear): without it,
|
||||
/// each successful network fetch re-stamps the shared cache row, so every
|
||||
/// later poll inside the freshness window is served the stale snapshot.
|
||||
Future<PlexVideoPlaybackData> getVideoPlaybackData(
|
||||
String ratingKey, {
|
||||
int mediaIndex = 0,
|
||||
String? selectedMediaSourceId,
|
||||
String? preferredVersionSignature,
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
// Fresh-cache-first: the detail screen writes a strict superset of this
|
||||
// query shape (includeChapters+includeMarkers+includeOnDeck+checkFiles+
|
||||
@@ -1936,24 +1943,26 @@ class PlexClient
|
||||
// staleness, thin row (getPlaybackExtras' lean fetch overwrites the shared
|
||||
// row without includeStreams/checkFiles), or shape failure falls through
|
||||
// to the network-first fetch below unchanged.
|
||||
final freshRow = await cache.getIfFresh(
|
||||
ServerId(cacheServerId),
|
||||
'/library/metadata/$ratingKey',
|
||||
maxAge: playbackMetadataCacheFreshness,
|
||||
);
|
||||
if (freshRow != null) {
|
||||
try {
|
||||
final cachedMetadataJson = _validatedPlaybackMetadataJson(freshRow);
|
||||
if (cachedMetadataJson != null && _plexMetadataHasStreamDetail(cachedMetadataJson)) {
|
||||
return parseVideoPlaybackDataFromJson(
|
||||
cachedMetadataJson,
|
||||
mediaIndex: mediaIndex,
|
||||
selectedMediaSourceId: selectedMediaSourceId,
|
||||
preferredVersionSignature: preferredVersionSignature,
|
||||
);
|
||||
if (!forceRefresh) {
|
||||
final freshRow = await cache.getIfFresh(
|
||||
ServerId(cacheServerId),
|
||||
'/library/metadata/$ratingKey',
|
||||
maxAge: playbackMetadataCacheFreshness,
|
||||
);
|
||||
if (freshRow != null) {
|
||||
try {
|
||||
final cachedMetadataJson = _validatedPlaybackMetadataJson(freshRow);
|
||||
if (cachedMetadataJson != null && _plexMetadataHasStreamDetail(cachedMetadataJson)) {
|
||||
return parseVideoPlaybackDataFromJson(
|
||||
cachedMetadataJson,
|
||||
mediaIndex: mediaIndex,
|
||||
selectedMediaSourceId: selectedMediaSourceId,
|
||||
preferredVersionSignature: preferredVersionSignature,
|
||||
);
|
||||
}
|
||||
} on FormatException {
|
||||
// Malformed cached row: the network fetch below overwrites it.
|
||||
}
|
||||
} on FormatException {
|
||||
// Malformed cached row: the network fetch below overwrites it.
|
||||
}
|
||||
}
|
||||
final data = await fetchWithCacheFallback<Map<String, dynamic>>(
|
||||
|
||||
@@ -117,7 +117,10 @@ extension _PlexVideoControlsNavigationMethods on _PlexVideoControlsState {
|
||||
|
||||
try {
|
||||
if (!targetIsCurrent()) return SubtitleDownloadApplyOutcome.superseded;
|
||||
final data = await client.getVideoPlaybackData(ratingKey);
|
||||
// forceRefresh: playback start left a fresh /library/metadata row in
|
||||
// the cache (and each network poll would re-stamp it), so a
|
||||
// cache-eligible read here would never observe the new stream.
|
||||
final data = await client.getVideoPlaybackData(ratingKey, forceRefresh: true);
|
||||
if (!targetIsCurrent()) return SubtitleDownloadApplyOutcome.superseded;
|
||||
if (data.mediaInfo == null) continue;
|
||||
|
||||
|
||||
@@ -318,6 +318,21 @@ void main() {
|
||||
expect(data.mediaInfo?.subtitleTracks.single.id, 401);
|
||||
});
|
||||
|
||||
test('forceRefresh bypasses a fresh stream-rich cached row', () async {
|
||||
// The subtitle-download poller relies on this: it must observe the new
|
||||
// external stream appearing server-side while the shared row is fresh.
|
||||
await PlexApiCache.instance.put(cacheScope, endpoint, richPlaybackPayload());
|
||||
final requests = <Uri>[];
|
||||
final client = makeCountingClient(requests);
|
||||
addTearDown(client.close);
|
||||
|
||||
final data = await client.getVideoPlaybackData('42', forceRefresh: true);
|
||||
|
||||
expect(requests, hasLength(1));
|
||||
expect(requests.single.queryParameters['includeStreams'], '1');
|
||||
expect(data.mediaInfo?.subtitleTracks.single.id, 401);
|
||||
});
|
||||
|
||||
test('fresh but stream-less cached row still fetches from the network', () async {
|
||||
// getPlaybackExtras' lean fetch overwrites the shared row without
|
||||
// includeStreams/checkFiles; that shape must never satisfy playback.
|
||||
|
||||
Reference in New Issue
Block a user