spotube/lib/provider/history/top/tracks.dart

197 lines
6.4 KiB
Dart

import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/history/top.dart';
import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
typedef PlaybackHistoryTrack = ({int count, SpotubeTrackObject track});
typedef PlaybackHistoryArtist = ({int count, SpotubeSimpleArtistObject artist});
class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
PlaybackHistoryTrack, HistoryDuration> {
HistoryTopTracksNotifier() : super();
SimpleSelectStatement<$HistoryTableTable, HistoryTableData>
createTracksQuery() {
final database = ref.read(databaseProvider);
return database.select(database.historyTable)
..where(
(tbl) =>
tbl.type.equalsValue(HistoryEntryType.track) &
tbl.createdAt.isBiggerOrEqualValue(switch (arg) {
HistoryDuration.allTime => DateTime(1970),
// from start of the week
HistoryDuration.days7 => DateTime.now()
.subtract(Duration(days: DateTime.now().weekday - 1)),
// from start of the month
HistoryDuration.days30 =>
DateTime.now().subtract(Duration(days: DateTime.now().day - 1)),
// from start of the 6th month
HistoryDuration.months6 => DateTime.now()
.subtract(Duration(days: DateTime.now().day - 1))
.subtract(const Duration(days: 30 * 6)),
// from start of the year
HistoryDuration.year => DateTime.now()
.subtract(Duration(days: DateTime.now().day - 1))
.subtract(const Duration(days: 30 * 12)),
HistoryDuration.years2 => DateTime.now()
.subtract(Duration(days: DateTime.now().day - 1))
.subtract(const Duration(days: 30 * 12 * 2)),
}),
);
}
Future<void> fixImageNotLoadingForArtistIssue(
List<HistoryTableData> entries,
) async {
final nonImageArtistTracks =
entries.where((e) => e.track!.artists.any((a) => a.images == null));
if (nonImageArtistTracks.isEmpty) return;
final artistIds = nonImageArtistTracks
.map((e) => e.track!.artists.map((a) => a.id))
.expand((e) => e)
.toSet()
.toList();
if (artistIds.isEmpty) return;
final artists = await Future.wait([
for (final id in artistIds)
ref.read(metadataPluginArtistProvider(id).future),
]);
final imagedArtistTracks = nonImageArtistTracks.map((e) {
var track = e.track!;
final includedArtists = track.artists
.map((a) {
final fullArtist =
artists.firstWhereOrNull((artist) => artist.id == a.id);
return fullArtist != null
? a.copyWith(images: fullArtist.images)
: a;
})
.nonNulls
.toList();
track = track.copyWith(artists: includedArtists);
return e.copyWith(data: track.toJson());
});
assert(
imagedArtistTracks
.every((e) => e.track!.artists.every((a) => a.images != null)),
'Tracks artists should have images',
);
final database = ref.read(databaseProvider);
await database.batch((batch) {
batch.insertAllOnConflictUpdate(
database.historyTable,
imagedArtistTracks,
);
});
}
@override
fetch(offset, limit) async {
final tracksQuery = createTracksQuery()..limit(limit, offset: offset);
final entries = await tracksQuery.get();
final items = getTracksWithCount(entries);
return SpotubePaginationResponseObject<PlaybackHistoryTrack>(
items: items,
nextOffset: offset + limit,
total: items.length,
limit: limit,
hasMore: items.length == limit,
);
}
@override
build(arg) async {
final subscription = createTracksQuery().watch().listen((event) {
if (state.asData == null) return;
state = AsyncData(state.asData!.value.copyWith(
items: getTracksWithCount(event),
hasMore: false,
));
});
ref.onDispose(() {
subscription.cancel();
});
return await fetch(0, 20);
}
List<PlaybackHistoryArtist> get artists {
return getArtistsWithCount(
state.asData?.value.items.expand((e) => e.track.artists) ?? [],
);
}
List<PlaybackHistoryArtist> getArtistsWithCount(
Iterable<SpotubeSimpleArtistObject> artists,
) {
return groupBy(artists, (artist) => artist.id)
.entries
.map((entry) {
return (
count: entry.value.length,
/// Previously, due to a bug, artist images were not being saved.
/// Now it's fixed, but we need to handle the case where images are null.
/// So we take the first artist with images if available, otherwise the first one.
artist: entry.value.firstWhereOrNull((a) => a.images != null) ??
entry.value.first,
);
})
.sorted((a, b) => b.count.compareTo(a.count))
.toList();
}
List<PlaybackHistoryTrack> getTracksWithCount(List<HistoryTableData> tracks) {
fixImageNotLoadingForArtistIssue(tracks);
return groupBy(
tracks,
(track) => track.track!.id,
)
.entries
.map((entry) {
return (
count: entry.value.length,
/// Previously, due to a bug, artist images were not being saved.
/// Now it's fixed, but we need to handle the case where images are null.
/// So we take the first artist with images if available, otherwise the first one.
track: entry.value
.firstWhereOrNull(
(t) => t.track!.artists.every((a) => a.images != null))
?.track! ??
entry.value.first.track!,
);
})
.sorted((a, b) => b.count.compareTo(a.count))
.toList();
}
}
final historyTopTracksProvider = AsyncNotifierProviderFamily<
HistoryTopTracksNotifier,
SpotubePaginationResponseObject<PlaybackHistoryTrack>,
HistoryDuration>(
() => HistoryTopTracksNotifier(),
);