fix: retry live tv tune, clamp timeline duration

This commit is contained in:
edde746
2026-04-22 08:45:02 +02:00
parent 06b3c9645c
commit b2cd0c2c37
5 changed files with 53 additions and 7 deletions
+3 -1
View File
@@ -2602,8 +2602,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// For live TV, player position/duration are unreliable (often 0).
// Use playbackTime as time, and program duration from tune metadata.
// Plex rejects timeline pings where time > duration; grow duration to
// match — otherwise Tunarr-style short synthetic programs 400 mid-stream.
final time = playbackTime;
final duration = _liveDurationMs ?? 0;
final duration = max(_liveDurationMs ?? 0, time);
final updatedBuffer = await client.updateLiveTimeline(
ratingKey: ratingKey,
+16 -6
View File
@@ -262,11 +262,21 @@ class PlexClient {
}
bool _shouldAttemptFailover(PlexHttpException e) {
if (e.isTransient) return true;
final sc = e.statusCode;
return e.type == PlexHttpErrorType.connectionTimeout ||
e.type == PlexHttpErrorType.receiveTimeout ||
e.type == PlexHttpErrorType.connectionError ||
(sc != null && sc >= 500 && sc <= 599);
return sc != null && sc >= 500 && sc <= 599;
}
/// POST the tune endpoint with one retry on transient HTTP failure.
Future<PlexResponse> _postTuneWithRetry(String path, String sessionIdentifier) async {
final query = {'X-Plex-Session-Identifier': sessionIdentifier};
try {
return await _http.post(path, queryParameters: query, timeout: ConnectionTimeouts.tune);
} on PlexHttpException catch (e) {
if (!e.isTransient) rethrow;
appLogger.w('Tune channel: transient failure, retrying once', error: e);
return await _http.post(path, queryParameters: query, timeout: ConnectionTimeouts.tune);
}
}
/// Fetch /media/providers and parse libraries + EPG providers from the response.
@@ -2658,9 +2668,9 @@ class PlexClient {
try {
final sessionIdentifier = generateSessionIdentifier();
final response = await _http.post(
final response = await _postTuneWithRetry(
'/livetv/dvrs/$dvrKey/channels/$channelIdentifier/tune',
queryParameters: {'X-Plex-Session-Identifier': sessionIdentifier},
sessionIdentifier,
);
if (response.statusCode >= 400) {
+4
View File
@@ -11,6 +11,10 @@ class ConnectionTimeouts {
/// HTTP connect timeout for individual HTTP requests to a Plex server.
static const connect = Duration(seconds: 10);
/// HTTP timeout for the live-TV tune POST. Matches Plex web's value — the
/// default 10s connect budget is too tight on Fire-TV cold starts.
static const tune = Duration(seconds: 30);
/// Per-server connection budget: preferred probe + race + HTTPS upgrade attempt + 1s buffer.
static const perServerConnect = Duration(milliseconds: 1500 + 2000 + 2000 + 1000);
+6
View File
@@ -45,6 +45,12 @@ class PlexHttpException implements Exception {
return PlexHttpException(type: PlexHttpErrorType.unknown, message: error.toString(), requestUri: uri);
}
/// Whether the error looks transient (network/timeout) and worth retrying.
bool get isTransient =>
type == PlexHttpErrorType.connectionTimeout ||
type == PlexHttpErrorType.connectionError ||
type == PlexHttpErrorType.receiveTimeout;
@override
String toString() => 'PlexHttpException(${type.name}: $message)';
}
+24
View File
@@ -84,4 +84,28 @@ void main() {
expect(e.toString(), 'PlexHttpException(cancelled: halt)');
});
});
group('PlexHttpException.isTransient', () {
PlexHttpException ex(PlexHttpErrorType t) => PlexHttpException(type: t);
test('connectionTimeout is transient', () {
expect(ex(PlexHttpErrorType.connectionTimeout).isTransient, isTrue);
});
test('receiveTimeout is transient', () {
expect(ex(PlexHttpErrorType.receiveTimeout).isTransient, isTrue);
});
test('connectionError is transient', () {
expect(ex(PlexHttpErrorType.connectionError).isTransient, isTrue);
});
test('cancelled is NOT transient (user-driven abort)', () {
expect(ex(PlexHttpErrorType.cancelled).isTransient, isFalse);
});
test('unknown is NOT transient', () {
expect(ex(PlexHttpErrorType.unknown).isTransient, isFalse);
});
});
}