mobile-music/lib/subsonic/json_helpers.dart
2026-07-29 14:14:18 -04:00

59 lines
1.9 KiB
Dart

/// Defensive JSON helpers for the Subsonic protocol.
///
/// Subsonic servers are inconsistent (Ratune handles the same quirks in
/// `ratune-subsonic/src/models.rs` via `OneOrMany` / `deserialize_flexible_id`):
/// * a field that is sometimes a single object and sometimes an array
/// * IDs that are sometimes strings and sometimes integers
/// Every accessor below tolerates nulls and mixed types rather than throwing.
library;
/// Coerce a value that may be a single object, a list, or null into a list of
/// `T`, applying [fromJson] to each map element.
List<T> oneOrMany<T>(
dynamic value,
T Function(Map<String, dynamic>) fromJson,
) {
if (value == null) return const [];
if (value is List) {
return value
.whereType<Map>()
.map((e) => fromJson(e.cast<String, dynamic>()))
.toList();
}
if (value is Map) {
return [fromJson(value.cast<String, dynamic>())];
}
return const [];
}
/// Read a string that the server might encode as an int (e.g. IDs).
String? asString(dynamic value) {
if (value == null) return null;
if (value is String) return value;
return value.toString();
}
/// Read an int from a value that might be an int, num, or numeric string.
int? asInt(dynamic value) {
if (value == null) return null;
if (value is int) return value;
if (value is num) return value.toInt();
if (value is String) return int.tryParse(value);
return null;
}
/// Read a double from an int/num/numeric-string.
double? asDouble(dynamic value) {
if (value == null) return null;
if (value is double) return value;
if (value is num) return value.toDouble();
if (value is String) return double.tryParse(value);
return null;
}
/// Read a bool that might arrive as a bool or the string "true"/"false".
bool asBool(dynamic value, {bool orElse = false}) {
if (value is bool) return value;
if (value is String) return value.toLowerCase() == 'true';
return orElse;
}