spotube/lib/provider/history/top.dart
2024-06-30 15:44:24 +06:00

221 lines
6.6 KiB
Dart

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<PlaybackHistoryTrack> tracks;
final List<PlaybackHistoryAlbum> albums;
final List<PlaybackHistoryPlaylist> playlists;
final List<PlaybackHistoryArtist> artists;
const PlaybackHistoryTopState({
required this.tracks,
required this.albums,
required this.playlists,
required this.artists,
});
PlaybackHistoryTopState copyWith({
List<PlaybackHistoryTrack>? tracks,
List<PlaybackHistoryAlbum>? albums,
List<PlaybackHistoryPlaylist>? playlists,
List<PlaybackHistoryArtist>? artists,
}) {
return PlaybackHistoryTopState(
tracks: tracks ?? this.tracks,
albums: albums ?? this.albums,
playlists: playlists ?? this.playlists,
artists: artists ?? this.artists,
);
}
}
class PlaybackHistoryTopNotifier
extends FamilyAsyncNotifier<PlaybackHistoryTopState, HistoryDuration> {
@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 = <StreamSubscription>[
tracksQuery.watch().listen((event) {
if (state.asData == null) return;
final artists = event
.map((track) => track.track!.artists)
.expand((e) => e ?? <Artist>[]);
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 ?? <Artist>[]);
final artistsWithCount = getArtistsWithCount(artists);
final playlistsWithCount = getPlaylistsWithCount(playlists);
return PlaybackHistoryTopState(
tracks: tracksWithCount,
albums: albumsWithCount,
artists: artistsWithCount,
playlists: playlistsWithCount,
);
});
}
List<PlaybackHistoryTrack> getTracksWithCount(List<HistoryTableData> 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<PlaybackHistoryAlbum> getAlbumsWithCount(
List<AlbumSimple> 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<PlaybackHistoryArtist> getArtistsWithCount(Iterable<Artist> 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<PlaybackHistoryPlaylist> getPlaylistsWithCount(
List<HistoryTableData> 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);