init
This commit is contained in:
commit
d205277cdd
182 changed files with 22978 additions and 0 deletions
331
lib/subsonic/models.dart
Normal file
331
lib/subsonic/models.dart
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue