From c81a1f9a976d250c44cb2eeab61bb845767f90b2 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 12 Jul 2026 06:19:26 +0200 Subject: [PATCH] fix(plex): tolerate malformed activity rows --- lib/models/plex/plex_activity.dart | 13 +++--- lib/models/plex/plex_activity.g.dart | 12 +++--- lib/services/plex_client.dart | 12 +++++- test/models/plex_activity_test.dart | 22 ++++++++++ .../plex_client_http_contract_test.dart | 40 +++++++++++++++++++ 5 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 test/models/plex_activity_test.dart diff --git a/lib/models/plex/plex_activity.dart b/lib/models/plex/plex_activity.dart index e560ab1a..6e7286f6 100644 --- a/lib/models/plex/plex_activity.dart +++ b/lib/models/plex/plex_activity.dart @@ -1,20 +1,23 @@ import 'package:json_annotation/json_annotation.dart'; +import '../../utils/json_utils.dart'; + part 'plex_activity.g.dart'; /// Represents a running background task on a Plex Media Server (from /activities endpoint). @JsonSerializable(createToJson: false) class PlexActivity { - @JsonKey(defaultValue: '') + @JsonKey(fromJson: stringOrEmpty) final String uuid; - @JsonKey(defaultValue: '') + @JsonKey(fromJson: stringOrEmpty) final String type; - @JsonKey(defaultValue: '') + @JsonKey(fromJson: stringOrEmpty) final String title; + @JsonKey(readValue: readStringField) final String? subtitle; - @JsonKey(defaultValue: 0) + @JsonKey(fromJson: flexibleIntOrZero) final int progress; // 0–100 - @JsonKey(defaultValue: false) + @JsonKey(fromJson: flexibleBool) final bool cancellable; const PlexActivity({ diff --git a/lib/models/plex/plex_activity.g.dart b/lib/models/plex/plex_activity.g.dart index 1c6f5555..980cc0a4 100644 --- a/lib/models/plex/plex_activity.g.dart +++ b/lib/models/plex/plex_activity.g.dart @@ -7,10 +7,10 @@ part of 'plex_activity.dart'; // ************************************************************************** PlexActivity _$PlexActivityFromJson(Map json) => PlexActivity( - uuid: json['uuid'] as String? ?? '', - type: json['type'] as String? ?? '', - title: json['title'] as String? ?? '', - subtitle: json['subtitle'] as String?, - progress: (json['progress'] as num?)?.toInt() ?? 0, - cancellable: json['cancellable'] as bool? ?? false, + uuid: stringOrEmpty(json['uuid']), + type: stringOrEmpty(json['type']), + title: stringOrEmpty(json['title']), + subtitle: readStringField(json, 'subtitle') as String?, + progress: flexibleIntOrZero(json['progress']), + cancellable: flexibleBool(json['cancellable']), ); diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 11ee69f5..d18dc006 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -826,7 +826,17 @@ class PlexClient if (container == null) return []; final activityList = container['Activity'] as List?; if (activityList == null) return []; - return activityList.map((json) => PlexActivity.fromJson(json as Map)).toList(); + final activities = []; + for (final json in activityList) { + if (json is! Map) continue; + try { + final activity = PlexActivity.fromJson(json); + if (activity.uuid.isNotEmpty) activities.add(activity); + } catch (e) { + appLogger.d('Skipping malformed Plex activity', error: e); + } + } + return activities; } catch (e) { appLogger.e('Failed to get activities', error: e); return []; diff --git a/test/models/plex_activity_test.dart b/test/models/plex_activity_test.dart new file mode 100644 index 00000000..5a75031a --- /dev/null +++ b/test/models/plex_activity_test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/models/plex/plex_activity.dart'; + +void main() { + test('PlexActivity tolerates stringified and scalar fields', () { + final activity = PlexActivity.fromJson({ + 'uuid': 42, + 'type': 7, + 'title': true, + 'subtitle': 99, + 'progress': '75', + 'cancellable': '1', + }); + + expect(activity.uuid, '42'); + expect(activity.type, '7'); + expect(activity.title, 'true'); + expect(activity.subtitle, '99'); + expect(activity.progress, 75); + expect(activity.cancellable, isTrue); + }); +} diff --git a/test/services/plex_client_http_contract_test.dart b/test/services/plex_client_http_contract_test.dart index ce092344..6b02516a 100644 --- a/test/services/plex_client_http_contract_test.dart +++ b/test/services/plex_client_http_contract_test.dart @@ -85,4 +85,44 @@ void main() { expect(queue?.playQueueVersion, 5); expect(queue?.size, 3); }); + + test('activities tolerate scalar drift and skip only malformed rows', () async { + final client = makeClient( + (_) async => http.Response( + jsonEncode({ + 'MediaContainer': { + 'Activity': [ + { + 'uuid': 'activity-1', + 'type': 'library.update', + 'title': 'Scanning', + 'subtitle': 'Movies', + 'progress': 25, + 'cancellable': false, + }, + 'not-an-activity', + {'type': 'library.update', 'title': 'Missing identity'}, + {'uuid': 42, 'type': 7, 'title': true, 'subtitle': 99, 'progress': '75', 'cancellable': '1'}, + ], + }, + }), + 200, + headers: {'content-type': 'application/json'}, + ), + ); + addTearDown(client.close); + + final activities = await client.getActivities(); + + expect(activities, hasLength(2)); + expect(activities.first.uuid, 'activity-1'); + expect(activities.first.progress, 25); + expect(activities.first.cancellable, isFalse); + expect(activities.last.uuid, '42'); + expect(activities.last.type, '7'); + expect(activities.last.title, 'true'); + expect(activities.last.subtitle, '99'); + expect(activities.last.progress, 75); + expect(activities.last.cancellable, isTrue); + }); }