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`) 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 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 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 albumsFromSongs(List 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 = >{}; for (final s in songs) { final key = _albumKey(s); if (key == null) continue; // no album identity → skip groups.putIfAbsent(key, () => []).add(s); } final out = []; 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 artistsFromSongs(List songs) { final groups = >{}; for (final s in songs) { final key = _artistKey(s); if (key == null) continue; // no artist identity → skip groups.putIfAbsent(key, () => []).add(s); } final out = []; 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 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 songs, String id) { for (final a in artistsFromSongs(songs)) { if (a.id == id) return a; } return null; }