mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-14 16:25:16 +00:00
197 lines
6.4 KiB
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(),
|
|
);
|