import 'dart:async'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/database/database.dart'; enum HistoryDuration { allTime, days7, days30, months6, year, years2, } final playbackHistoryTopDurationProvider = StateProvider((ref) => HistoryDuration.days30); typedef PlaybackHistoryTrack = ({int count, Track track}); typedef PlaybackHistoryAlbum = ({int count, AlbumSimple album}); typedef PlaybackHistoryPlaylist = ({int count, PlaylistSimple playlist}); typedef PlaybackHistoryArtist = ({int count, Artist artist}); class PlaybackHistoryTopState { final List tracks; final List albums; final List playlists; final List artists; const PlaybackHistoryTopState({ required this.tracks, required this.albums, required this.playlists, required this.artists, }); PlaybackHistoryTopState copyWith({ List? tracks, List? albums, List? playlists, List? artists, }) { return PlaybackHistoryTopState( tracks: tracks ?? this.tracks, albums: albums ?? this.albums, playlists: playlists ?? this.playlists, artists: artists ?? this.artists, ); } } class PlaybackHistoryTopNotifier extends FamilyAsyncNotifier { @override build(arg) async { final database = ref.watch(databaseProvider); final duration = switch (arg) { HistoryDuration.allTime => const Duration(days: 365 * 2003), HistoryDuration.days7 => const Duration(days: 7), HistoryDuration.days30 => const Duration(days: 30), HistoryDuration.months6 => const Duration(days: 30 * 6), HistoryDuration.year => const Duration(days: 365), HistoryDuration.years2 => const Duration(days: 365 * 2), }; final tracksQuery = (database.select(database.historyTable) ..where( (tbl) => tbl.type.equalsValue(HistoryEntryType.track) & tbl.createdAt.isBiggerOrEqualValue( DateTime.now().subtract(duration), ), )); final albumsQuery = database.select(database.historyTable) ..where( (tbl) => tbl.type.equalsValue(HistoryEntryType.album) & tbl.createdAt.isBiggerOrEqualValue( DateTime.now().subtract(duration), ), ); final playlistsQuery = database.select(database.historyTable) ..where( (tbl) => tbl.type.equalsValue(HistoryEntryType.playlist) & tbl.createdAt.isBiggerOrEqualValue( DateTime.now().subtract(duration), ), ); final subscriptions = [ tracksQuery.watch().listen((event) { if (state.asData == null) return; final artists = event .map((track) => track.track!.artists) .expand((e) => e ?? []); state = AsyncData(state.asData!.value.copyWith( tracks: getTracksWithCount(event), artists: getArtistsWithCount(artists), )); }), albumsQuery.watch().listen((event) async { if (state.asData == null) return; final tracks = await tracksQuery.get(); final albumsWithTrackAlbums = [ for (final historicAlbum in event) historicAlbum.album!, for (final track in tracks) track.track!.album! ]; state = AsyncData(state.asData!.value.copyWith( albums: getAlbumsWithCount(albumsWithTrackAlbums), )); }), playlistsQuery.watch().listen((event) { if (state.asData == null) return; state = AsyncData(state.asData!.value.copyWith( playlists: getPlaylistsWithCount(event), )); }), ]; ref.onDispose(() { for (final subscription in subscriptions) { subscription.cancel(); } }); return database.transaction(() async { final tracks = await tracksQuery.get(); final albums = await albumsQuery.get(); final playlists = await playlistsQuery.get(); final tracksWithCount = getTracksWithCount(tracks); final albumsWithTrackAlbums = [ for (final historicAlbum in albums) historicAlbum.album!, for (final track in tracks) track.track!.album! ]; final albumsWithCount = getAlbumsWithCount(albumsWithTrackAlbums); final artists = tracks .map((track) => track.track!.artists) .expand((e) => e ?? []); final artistsWithCount = getArtistsWithCount(artists); final playlistsWithCount = getPlaylistsWithCount(playlists); return PlaybackHistoryTopState( tracks: tracksWithCount, albums: albumsWithCount, artists: artistsWithCount, playlists: playlistsWithCount, ); }); } List getTracksWithCount(List tracks) { return groupBy( tracks, (track) => track.track!.id!, ) .entries .map((entry) { return (count: entry.value.length, track: entry.value.first.track!); }) .sorted((a, b) => b.count.compareTo(a.count)) .toList(); } List getAlbumsWithCount( List albumsWithTrackAlbums, ) { return groupBy(albumsWithTrackAlbums, (album) => album.id!) .entries .map((entry) { return (count: entry.value.length, album: entry.value.first); }) .sorted((a, b) => b.count.compareTo(a.count)) .toList(); } List getArtistsWithCount(Iterable artists) { return groupBy(artists, (artist) => artist.id!) .entries .map((entry) { return (count: entry.value.length, artist: entry.value.first); }) .sorted((a, b) => b.count.compareTo(a.count)) .toList(); } List getPlaylistsWithCount( List playlists, ) { return groupBy(playlists, (playlist) => playlist.playlist!.id!) .entries .map((entry) { return ( count: entry.value.length, playlist: entry.value.first.playlist!, ); }) .sorted((a, b) => b.count.compareTo(a.count)) .toList(); } } final playbackHistoryTopProvider = AsyncNotifierProviderFamily< PlaybackHistoryTopNotifier, PlaybackHistoryTopState, HistoryDuration>(PlaybackHistoryTopNotifier.new);