mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-22 07:18:38 +02:00
perf(autoplay): index listening history and reuse normalized metadata
This commit is contained in:
1 parent
a3102b3b34
commit
6d68ef00ab
2 files changed
+85
-18
No files matched your search
@@ -96,46 +96,56 @@ List<PlayableMedia> selectAutoplayTracks({
|
||||
...dismissedSources,
|
||||
};
|
||||
final songs = {songKey(seed), ...upcoming.map(songKey)};
|
||||
final scored = <(PlayableMedia, double)>[];
|
||||
final seedArtist = normalized(seed.artist);
|
||||
final seedAlbum = normalized(seed.album);
|
||||
final seedGenre = normalized(seed.genre);
|
||||
final recentSources = <String, int>{};
|
||||
final recentSongs = <String, int>{};
|
||||
for (var i = 0; i < recent.length; i++) {
|
||||
recentSources[recent[i].source] = i;
|
||||
recentSongs[songKey(recent[i])] = i;
|
||||
}
|
||||
final scored = <(PlayableMedia, double, String)>[];
|
||||
for (final item in candidates) {
|
||||
final key = songKey(item);
|
||||
if (item.source.trim().isEmpty ||
|
||||
item.source.startsWith('http://') ||
|
||||
item.source.startsWith('https://') ||
|
||||
sources.contains(item.source) ||
|
||||
songs.contains(songKey(item))) {
|
||||
songs.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
sources.add(item.source);
|
||||
songs.add(songKey(item));
|
||||
songs.add(key);
|
||||
final artist = normalized(item.artist);
|
||||
var score = random.nextDouble() * 2;
|
||||
if (normalized(seed.artist).isNotEmpty &&
|
||||
normalized(seed.artist) == normalized(item.artist)) {
|
||||
if (seedArtist.isNotEmpty && seedArtist == artist) {
|
||||
score += 5;
|
||||
}
|
||||
if (normalized(seed.album).isNotEmpty &&
|
||||
normalized(seed.album) == normalized(item.album)) {
|
||||
if (seedAlbum.isNotEmpty && seedAlbum == normalized(item.album)) {
|
||||
score += 3;
|
||||
}
|
||||
if (normalized(seed.genre).isNotEmpty &&
|
||||
normalized(seed.genre) == normalized(item.genre)) {
|
||||
if (seedGenre.isNotEmpty && seedGenre == normalized(item.genre)) {
|
||||
score += 4;
|
||||
}
|
||||
final played = recent.lastIndexWhere(
|
||||
(previous) =>
|
||||
previous.source == item.source || songKey(previous) == songKey(item),
|
||||
// Match the latest source OR normalized song occurrence without walking
|
||||
// the entire listening history again for every candidate.
|
||||
final played = max(
|
||||
recentSources[item.source] ?? -1,
|
||||
recentSongs[key] ?? -1,
|
||||
);
|
||||
if (played >= 0) score -= 20 + played.toDouble();
|
||||
scored.add((item, score));
|
||||
scored.add((item, score, artist));
|
||||
}
|
||||
final selected = <PlayableMedia>[];
|
||||
while (selected.length < limit && scored.isNotEmpty) {
|
||||
scored.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
final next = scored.removeAt(0).$1;
|
||||
selected.add(next);
|
||||
final next = scored.removeAt(0);
|
||||
selected.add(next.$1);
|
||||
for (var i = 0; i < scored.length; i++) {
|
||||
final (item, score) = scored[i];
|
||||
if (normalized(item.artist) == normalized(next.artist)) {
|
||||
scored[i] = (item, score - 4);
|
||||
final (item, score, artist) = scored[i];
|
||||
if (artist == next.$3) {
|
||||
scored[i] = (item, score - 4, artist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,15 @@ PlayableMedia track(
|
||||
genre: genre,
|
||||
);
|
||||
|
||||
class _NoNoise implements Random {
|
||||
@override
|
||||
double nextDouble() => 0;
|
||||
@override
|
||||
int nextInt(int max) => 0;
|
||||
@override
|
||||
bool nextBool() => false;
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Autoplay prefers related unplayed Library tracks and deduplicates', () {
|
||||
final seed = track('seed', genre: 'Rock');
|
||||
@@ -58,6 +67,54 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('recency uses the latest source or normalized song match', () {
|
||||
final source = track('source-match', artist: 'A', source: '/same.flac');
|
||||
final song = track(' Song ', artist: ' ARTIST ');
|
||||
final older = track('older', artist: 'D');
|
||||
final unplayed = track('new', artist: 'E');
|
||||
final selected = selectAutoplayTracks(
|
||||
seed: track('seed', artist: 'Seed'),
|
||||
candidates: [source, song, older, unplayed],
|
||||
upcoming: [],
|
||||
recent: [
|
||||
track('source-match', artist: ' a ', source: '/copy.flac'),
|
||||
older,
|
||||
track(' song ', artist: 'artist', source: '/other.flac'),
|
||||
track('renamed', artist: 'different', source: '/same.flac'),
|
||||
],
|
||||
dismissedSources: {},
|
||||
random: _NoNoise(),
|
||||
);
|
||||
expect(selected, [unplayed, older, song, source]);
|
||||
});
|
||||
|
||||
test('bounded ranking retains its seeded ordering with a full history', () {
|
||||
PlayableMedia song(int id) => PlayableMedia(
|
||||
id: '$id',
|
||||
source: '/music/$id.flac',
|
||||
title: ' Song $id ',
|
||||
artist: ' Artist ${id % 24} ',
|
||||
album: ' Album ${id % 35} ',
|
||||
genre: ' Genre ${id % 8} ',
|
||||
);
|
||||
final selected = selectAutoplayTracks(
|
||||
seed: song(1000),
|
||||
candidates: List.generate(384, song),
|
||||
upcoming: [song(16), song(17)],
|
||||
recent: List.generate(200, (i) => song(i + 192)),
|
||||
dismissedSources: {'/music/18.flac'},
|
||||
random: Random(1),
|
||||
);
|
||||
expect(selected.map((item) => item.id), [
|
||||
'160',
|
||||
'40',
|
||||
'96',
|
||||
'80',
|
||||
'90',
|
||||
'20',
|
||||
]);
|
||||
});
|
||||
|
||||
test('Autoplay preference and recommendation provenance survive restore', () {
|
||||
expect(AppSettings.fromJson({}).autoplay, isFalse);
|
||||
final settings = const AppSettings().copyWith(autoplay: true);
|
||||
|
||||
Reference in new issue
Block a user