offline updates and playhead fix
This commit is contained in:
parent
7a199fe4df
commit
6663330260
14 changed files with 1673 additions and 545 deletions
153
lib/library/offline_library.dart
Normal file
153
lib/library/offline_library.dart
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import '../subsonic/models.dart';
|
||||
|
||||
/// Reconstructs [Album]s and [Artist]s from a flat list of downloaded [Song]s.
|
||||
/// The offline library only ever has tracks (that's all that's cached on disk),
|
||||
/// so album/artist entities are synthesized on demand. Pure — no I/O.
|
||||
|
||||
/// Compare two strings case-insensitively, treating null as empty (sorts first).
|
||||
int _byString(String? a, String? b) =>
|
||||
(a ?? '').toLowerCase().compareTo((b ?? '').toLowerCase());
|
||||
|
||||
/// Compare where a null [a]/[b] always sorts *last*. Takes bare [Comparable] so
|
||||
/// `int` (`Comparable<num>`) works for disc/track ordering.
|
||||
int _nullsLast(Comparable? a, Comparable? b) {
|
||||
if (a == null && b == null) return 0;
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
/// True for a present, non-blank id/name.
|
||||
bool _has(String? v) => v != null && v.trim().isNotEmpty;
|
||||
|
||||
/// First non-blank value in [values], or null if none.
|
||||
String? _firstNonNull(Iterable<String?> values) {
|
||||
for (final v in values) {
|
||||
if (_has(v)) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// First non-null value in [values], or null if none. For nullable ints.
|
||||
int? _firstNonNullInt(Iterable<int?> values) {
|
||||
for (final v in values) {
|
||||
if (v != null) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Stable album key: prefer [Song.albumId], else fall back to the album NAME.
|
||||
/// The synthesized [Album.id] mirrors this exactly (see [albumsFromSongs]), so
|
||||
/// Phase 2 providers can look an album up by the same key it was built under.
|
||||
String? _albumKey(Song s) => _has(s.albumId) ? s.albumId : s.album;
|
||||
|
||||
/// Stable artist key: prefer [Song.artistId], else fall back to the artist NAME.
|
||||
String? _artistKey(Song s) => _has(s.artistId) ? s.artistId : s.artist;
|
||||
|
||||
/// Synthesize [Album]s from downloaded [Song]s, grouped by [_albumKey].
|
||||
///
|
||||
/// Songs with neither an albumId nor an album name are skipped — with no album
|
||||
/// identity they can't form a meaningful album. The synthesized [Album.id] is
|
||||
/// the albumId when present, else the album *name* itself (the same string used
|
||||
/// as the grouping key), so lookups by id stay stable across rebuilds.
|
||||
///
|
||||
/// Returned albums are sorted by name (case-insensitive ascending) for a stable
|
||||
/// default order.
|
||||
List<Album> albumsFromSongs(List<Song> songs) {
|
||||
// Preserve first-seen insertion order within groups; output order is imposed
|
||||
// by the final sort, so the map's own ordering only needs to be deterministic.
|
||||
final groups = <String, List<Song>>{};
|
||||
for (final s in songs) {
|
||||
final key = _albumKey(s);
|
||||
if (key == null) continue; // no album identity → skip
|
||||
groups.putIfAbsent(key, () => []).add(s);
|
||||
}
|
||||
|
||||
final out = <Album>[];
|
||||
groups.forEach((key, group) {
|
||||
// id: albumId if any song carries one, else the name-derived key.
|
||||
final albumId = _firstNonNull(group.map((s) => s.albumId));
|
||||
final id = albumId ?? key;
|
||||
|
||||
final sorted = [...group]
|
||||
..sort((a, b) {
|
||||
final d = _nullsLast(a.discNumber, b.discNumber);
|
||||
if (d != 0) return d;
|
||||
final t = _nullsLast(a.track, b.track);
|
||||
return t != 0 ? t : _byString(a.title, b.title);
|
||||
});
|
||||
|
||||
out.add(
|
||||
Album(
|
||||
id: id,
|
||||
name: _firstNonNull(group.map((s) => s.album)),
|
||||
artist: _firstNonNull(group.map((s) => s.artist)),
|
||||
artistId: _firstNonNull(group.map((s) => s.artistId)),
|
||||
coverArt: _firstNonNull(group.map((s) => s.coverArt)),
|
||||
year: _firstNonNullInt(group.map((s) => s.year)),
|
||||
genre: _firstNonNull(group.map((s) => s.genre)),
|
||||
songCount: group.length,
|
||||
songs: sorted,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
out.sort((a, b) => _byString(a.name, b.name));
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Synthesize [Artist]s from downloaded [Song]s, grouped by [_artistKey].
|
||||
///
|
||||
/// Songs with neither an artistId nor an artist name are skipped. Each artist's
|
||||
/// [Artist.albums] is built by running [albumsFromSongs] over that artist's own
|
||||
/// songs, and its [Artist.id] follows the same id/name fallback as albums.
|
||||
///
|
||||
/// Returned artists are sorted by name (case-insensitive ascending).
|
||||
List<Artist> artistsFromSongs(List<Song> songs) {
|
||||
final groups = <String, List<Song>>{};
|
||||
for (final s in songs) {
|
||||
final key = _artistKey(s);
|
||||
if (key == null) continue; // no artist identity → skip
|
||||
groups.putIfAbsent(key, () => []).add(s);
|
||||
}
|
||||
|
||||
final out = <Artist>[];
|
||||
groups.forEach((key, group) {
|
||||
// id: artistId if any song carries one, else the name-derived key.
|
||||
final artistId = _firstNonNull(group.map((s) => s.artistId));
|
||||
final id = artistId ?? key;
|
||||
|
||||
final albums = albumsFromSongs(group);
|
||||
|
||||
out.add(
|
||||
Artist(
|
||||
id: id,
|
||||
name: _firstNonNull(group.map((s) => s.artist)),
|
||||
coverArt: _firstNonNull(albums.map((a) => a.coverArt)),
|
||||
albums: albums,
|
||||
albumCount: albums.length,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
out.sort((a, b) => _byString(a.name, b.name));
|
||||
return out;
|
||||
}
|
||||
|
||||
/// The synthesized album whose id == [id], or null. Keyed lookup for the album
|
||||
/// detail provider.
|
||||
Album? albumFromSongs(List<Song> songs, String id) {
|
||||
for (final a in albumsFromSongs(songs)) {
|
||||
if (a.id == id) return a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The synthesized artist whose id == [id], or null. Keyed lookup for the artist
|
||||
/// detail provider.
|
||||
Artist? artistFromSongs(List<Song> songs, String id) {
|
||||
for (final a in artistsFromSongs(songs)) {
|
||||
if (a.id == id) return a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue