import 'json_helpers.dart'; /// Subsonic domain models, ported from `ratune-subsonic/src/models.rs`. /// Nearly every field is nullable — servers omit fields freely. All parsing /// goes through the defensive helpers in `json_helpers.dart`. class Song { Song({ required this.id, this.title, this.album, this.artist, this.albumId, this.artistId, this.track, this.discNumber, this.year, this.genre, this.coverArt, this.duration, this.bitRate, this.contentType, this.suffix, this.size, this.path, this.starred = false, this.userRating, }); final String id; final String? title; final String? album; final String? artist; final String? albumId; final String? artistId; final int? track; final int? discNumber; final int? year; final String? genre; final String? coverArt; /// Track length in seconds. final int? duration; final int? bitRate; final String? contentType; final String? suffix; final int? size; final String? path; final bool starred; /// 1–5, or null if unrated. final int? userRating; factory Song.fromJson(Map j) => Song( id: asString(j['id']) ?? '', title: asString(j['title']), album: asString(j['album']), artist: asString(j['artist']), albumId: asString(j['albumId']), artistId: asString(j['artistId']), track: asInt(j['track']), discNumber: asInt(j['discNumber']), year: asInt(j['year']), genre: asString(j['genre']), coverArt: asString(j['coverArt']), duration: asInt(j['duration']), bitRate: asInt(j['bitRate']), contentType: asString(j['contentType']), suffix: asString(j['suffix']), size: asInt(j['size']), path: asString(j['path']), starred: j['starred'] != null, userRating: asInt(j['userRating']), ); /// Round-trips through [Song.fromJson]. Backs the persisted library index /// (`library_index.dart`); only emits set fields to keep the cache compact. Map toJson() => { 'id': id, if (title != null) 'title': title, if (album != null) 'album': album, if (artist != null) 'artist': artist, if (albumId != null) 'albumId': albumId, if (artistId != null) 'artistId': artistId, if (track != null) 'track': track, if (discNumber != null) 'discNumber': discNumber, if (year != null) 'year': year, if (genre != null) 'genre': genre, if (coverArt != null) 'coverArt': coverArt, if (duration != null) 'duration': duration, if (bitRate != null) 'bitRate': bitRate, if (contentType != null) 'contentType': contentType, if (suffix != null) 'suffix': suffix, if (size != null) 'size': size, if (path != null) 'path': path, if (starred) 'starred': true, if (userRating != null) 'userRating': userRating, }; } class Album { Album({ required this.id, this.name, this.artist, this.artistId, this.coverArt, this.songCount, this.duration, this.year, this.genre, this.starred = false, this.userRating, this.songs = const [], }); final String id; final String? name; final String? artist; final String? artistId; final String? coverArt; final int? songCount; final int? duration; final int? year; final String? genre; final bool starred; final int? userRating; /// Populated only by `getAlbum`. final List songs; factory Album.fromJson(Map j) => Album( id: asString(j['id']) ?? '', name: asString(j['name']) ?? asString(j['album']), artist: asString(j['artist']), artistId: asString(j['artistId']), coverArt: asString(j['coverArt']), songCount: asInt(j['songCount']), duration: asInt(j['duration']), year: asInt(j['year']), genre: asString(j['genre']), starred: j['starred'] != null, userRating: asInt(j['userRating']), songs: oneOrMany(j['song'], Song.fromJson), ); } class Artist { Artist({ required this.id, this.name, this.albumCount, this.coverArt, this.starred = false, this.userRating, this.albums = const [], }); final String id; final String? name; final int? albumCount; final String? coverArt; final bool starred; final int? userRating; /// Populated only by `getArtist`. final List albums; factory Artist.fromJson(Map j) => Artist( id: asString(j['id']) ?? '', name: asString(j['name']), albumCount: asInt(j['albumCount']), coverArt: asString(j['coverArt']), starred: j['starred'] != null, userRating: asInt(j['userRating']), albums: oneOrMany(j['album'], Album.fromJson), ); } /// A lettered bucket of artists from `getArtists` (``). class ArtistIndex { ArtistIndex({required this.name, required this.artists}); final String name; final List artists; factory ArtistIndex.fromJson(Map j) => ArtistIndex( name: asString(j['name']) ?? '', artists: oneOrMany(j['artist'], Artist.fromJson), ); } /// Flattened result of `getArtists`. class ArtistsResult { ArtistsResult({required this.indexes}); final List indexes; List get all => [for (final i in indexes) ...i.artists]; factory ArtistsResult.fromJson(Map j) => ArtistsResult( indexes: oneOrMany(j['index'], ArtistIndex.fromJson), ); } /// Result of `getStarred2` — the user's favorites. class Starred2 { Starred2({ required this.artists, required this.albums, required this.songs, }); final List artists; final List albums; final List songs; factory Starred2.fromJson(Map j) => Starred2( artists: oneOrMany(j['artist'], Artist.fromJson), albums: oneOrMany(j['album'], Album.fromJson), songs: oneOrMany(j['song'], Song.fromJson), ); } /// Result of `search3`. class SearchResult3 { SearchResult3({ required this.artists, required this.albums, required this.songs, }); final List artists; final List albums; final List songs; factory SearchResult3.fromJson(Map j) => SearchResult3( artists: oneOrMany(j['artist'], Artist.fromJson), albums: oneOrMany(j['album'], Album.fromJson), songs: oneOrMany(j['song'], Song.fromJson), ); } /// A playlist summary from `getPlaylists` (no tracks), ported from /// `ratune-subsonic/src/models.rs`. [toJson] backs the offline playlist mirror /// (`playlists/playlists.dart`). class Playlist { Playlist({ required this.id, required this.name, this.songCount, this.duration, this.owner, this.public, this.coverArt, }); final String id; final String name; final int? songCount; final int? duration; final String? owner; final bool? public; final String? coverArt; factory Playlist.fromJson(Map j) => Playlist( id: asString(j['id']) ?? '', name: asString(j['name']) ?? 'Untitled', songCount: asInt(j['songCount']), duration: asInt(j['duration']), owner: asString(j['owner']), public: j['public'] is bool ? j['public'] as bool : null, coverArt: asString(j['coverArt']), ); Map toJson() => { 'id': id, 'name': name, if (songCount != null) 'songCount': songCount, if (duration != null) 'duration': duration, if (owner != null) 'owner': owner, if (public != null) 'public': public, if (coverArt != null) 'coverArt': coverArt, }; } /// A playlist with its full track list from `getPlaylist`. The Subsonic API /// nests the tracks under the key `entry` (not `song`). class PlaylistDetail { PlaylistDetail({ required this.id, required this.name, this.songCount, this.duration, this.coverArt, this.songs = const [], }); final String id; final String name; final int? songCount; final int? duration; final String? coverArt; final List songs; factory PlaylistDetail.fromJson(Map j) => PlaylistDetail( id: asString(j['id']) ?? '', name: asString(j['name']) ?? 'Untitled', songCount: asInt(j['songCount']), duration: asInt(j['duration']), coverArt: asString(j['coverArt']), songs: oneOrMany(j['entry'], Song.fromJson), ); Map toJson() => { 'id': id, 'name': name, if (songCount != null) 'songCount': songCount, if (duration != null) 'duration': duration, if (coverArt != null) 'coverArt': coverArt, 'entry': songs.map((s) => s.toJson()).toList(), }; Playlist toSummary() => Playlist( id: id, name: name, songCount: songCount ?? songs.length, duration: duration, coverArt: coverArt, ); }