From e74df0d245cdf24f5a874e1188078a82c037313f Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 23 May 2026 22:02:18 +0200 Subject: [PATCH] fix(plex): fail open on unknown transcode capability --- lib/services/plex_client.dart | 17 ++- .../plex_transcoder_capability_test.dart | 115 ++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 test/services/plex_transcoder_capability_test.dart diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index dc932e3e..b8b1337e 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -188,6 +188,17 @@ class ConnectionTestResult { ConnectionTestResult({required this.success, required this.latencyMs, this.error, this.transcoderVideo}); } +bool? _parsePlexTranscoderVideoCapability(Object? value) { + return switch (value) { + final bool b => b, + final int n when n == 1 => true, + final int n when n == 0 => false, + final String s when s.trim().toLowerCase() == 'true' || s.trim() == '1' => true, + final String s when s.trim().toLowerCase() == 'false' || s.trim() == '0' => false, + _ => null, + }; +} + class PlexClient with MediaServerCacheMixin, _PlexLiveTvClientMethods implements MediaServerClient, GracefullyCloseable { @@ -592,7 +603,9 @@ class PlexClient bool? transcoderVideo; if (success && response.data is Map && response.data['MediaContainer'] is Map) { - transcoderVideo = flexibleBool((response.data['MediaContainer'] as Map)['transcoderVideo']); + transcoderVideo = _parsePlexTranscoderVideoCapability( + (response.data['MediaContainer'] as Map)['transcoderVideo'], + ); } return ConnectionTestResult( @@ -2815,7 +2828,7 @@ class PlexClient final response = await _http.get('/', timeout: const Duration(seconds: 5)); final container = _getMediaContainer(response); final value = container?['transcoderVideo']; - final supported = flexibleBool(value); + final supported = _parsePlexTranscoderVideoCapability(value) ?? true; _serverTranscoderCached = supported; return supported; } catch (e) { diff --git a/test/services/plex_transcoder_capability_test.dart b/test/services/plex_transcoder_capability_test.dart new file mode 100644 index 00000000..6dcdcc12 --- /dev/null +++ b/test/services/plex_transcoder_capability_test.dart @@ -0,0 +1,115 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:plezy/database/app_database.dart'; +import 'package:plezy/models/plex/plex_config.dart'; +import 'package:plezy/services/plex_api_cache.dart'; +import 'package:plezy/services/plex_client.dart'; + +void main() { + late AppDatabase db; + + setUp(() { + db = AppDatabase.forTesting(NativeDatabase.memory()); + PlexApiCache.initialize(db); + }); + + tearDown(() async { + await db.close(); + }); + + group('Plex transcoder capability', () { + test('client probe fails open when transcoderVideo is absent', () async { + final client = _makeClient({'friendlyName': 'Plex'}); + addTearDown(client.close); + + final supported = await client.serverSupportsVideoTranscoding(); + + expect(supported, isTrue); + expect(client.capabilities.videoTranscoding, isTrue); + }); + + test('client probe preserves explicit transcoderVideo false', () async { + final client = _makeClient({'transcoderVideo': false}); + addTearDown(client.close); + + final supported = await client.serverSupportsVideoTranscoding(); + + expect(supported, isFalse); + expect(client.capabilities.videoTranscoding, isFalse); + }); + + test('connection probe keeps absent transcoderVideo unknown', () async { + final server = await _startRootServer({'friendlyName': 'Plex'}); + addTearDown(() async => server.close(force: true)); + + final result = await PlexClient.testConnectionWithLatency( + _serverBaseUrl(server), + 'token', + timeout: const Duration(seconds: 2), + clientIdentifier: 'client-id', + ); + + expect(result.success, isTrue); + expect(result.transcoderVideo, isNull); + }); + + test('connection probe preserves explicit transcoderVideo false', () async { + final server = await _startRootServer({'transcoderVideo': false}); + addTearDown(() async => server.close(force: true)); + + final result = await PlexClient.testConnectionWithLatency( + _serverBaseUrl(server), + 'token', + timeout: const Duration(seconds: 2), + clientIdentifier: 'client-id', + ); + + expect(result.success, isTrue); + expect(result.transcoderVideo, isFalse); + }); + }); +} + +PlexClient _makeClient(Map rootContainer) { + return PlexClient.forTesting( + config: PlexConfig( + baseUrl: 'https://plex.example.com', + token: 'token', + clientIdentifier: 'client-id', + product: 'Plezy', + version: 'test', + ), + serverId: 'server-id', + httpClient: MockClient((request) async { + expect(request.url.path, '/'); + return http.Response( + jsonEncode({'MediaContainer': rootContainer}), + 200, + headers: {'content-type': 'application/json'}, + ); + }), + ); +} + +Future _startRootServer(Map rootContainer) async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + server.listen((request) async { + if (request.uri.path != '/') { + request.response.statusCode = HttpStatus.notFound; + await request.response.close(); + return; + } + request.response.statusCode = HttpStatus.ok; + request.response.headers.contentType = ContentType.json; + request.response.write(jsonEncode({'MediaContainer': rootContainer})); + await request.response.close(); + }); + return server; +} + +String _serverBaseUrl(HttpServer server) => 'http://${server.address.host}:${server.port}';