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

331 lines
9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String, dynamic> 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<String, dynamic> 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<Song> songs;
factory Album.fromJson(Map<String, dynamic> 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<Album> albums;
factory Artist.fromJson(Map<String, dynamic> 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` (`<index name="A">`).
class ArtistIndex {
ArtistIndex({required this.name, required this.artists});
final String name;
final List<Artist> artists;
factory ArtistIndex.fromJson(Map<String, dynamic> j) => ArtistIndex(
name: asString(j['name']) ?? '',
artists: oneOrMany(j['artist'], Artist.fromJson),
);
}
/// Flattened result of `getArtists`.
class ArtistsResult {
ArtistsResult({required this.indexes});
final List<ArtistIndex> indexes;
List<Artist> get all => [for (final i in indexes) ...i.artists];
factory ArtistsResult.fromJson(Map<String, dynamic> 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<Artist> artists;
final List<Album> albums;
final List<Song> songs;
factory Starred2.fromJson(Map<String, dynamic> 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<Artist> artists;
final List<Album> albums;
final List<Song> songs;
factory SearchResult3.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<Song> songs;
factory PlaylistDetail.fromJson(Map<String, dynamic> 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<String, dynamic> 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,
);
}