/// 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 oneOrMany( dynamic value, T Function(Map) fromJson, ) { if (value == null) return const []; if (value is List) { return value .whereType() .map((e) => fromJson(e.cast())) .toList(); } if (value is Map) { return [fromJson(value.cast())]; } 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; }