fix: instant mix random songs (#354)

* wip: updated instant mix request size

* Address broken continuous play 

* wip: filling queue, getting dupes

* fix: deduped the song track list
This commit is contained in:
eddyizm 2026-01-13 20:00:46 -08:00 committed by GitHub
parent 55265615e6
commit e77f3bf9b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 55 additions and 58 deletions

View file

@ -25,6 +25,7 @@ import retrofit2.Response;
public class SongRepository { public class SongRepository {
private static final String TAG = "SongRepository"; private static final String TAG = "SongRepository";
public interface MediaCallbackInternal { public interface MediaCallbackInternal {
void onSongsAvailable(List<Child> songs); void onSongsAvailable(List<Child> songs);
} }
@ -64,18 +65,25 @@ public class SongRepository {
*/ */
public MutableLiveData<List<Child>> getInstantMix(String id, SeedType type, int count) { public MutableLiveData<List<Child>> getInstantMix(String id, SeedType type, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(new ArrayList<>()); MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(new ArrayList<>());
Set<String> trackIds = new HashSet<>();
performSmartMix(id, type, count, songs -> { performSmartMix(id, type, count, songs -> {
List<Child> current = instantMix.getValue(); List<Child> current = instantMix.getValue();
if (current != null) { if (current != null) {
for (Child s : songs) { for (Child s : songs) {
if (!current.contains(s)) current.add(s); if (!trackIds.contains(s.getId())) {
current.add(s);
trackIds.add(s.getId());
}
} }
if (current.size() < count / 2) { if (current.size() < count / 2) {
fillWithRandom(count - current.size(), remainder -> { fetchSimilarOnly(id, count, remainder -> {
for (Child r : remainder) { for (Child r : remainder) {
if (!current.contains(r)) current.add(r); if (!trackIds.contains(r.getId())) {
current.add(r);
trackIds.add(r.getId());
}
} }
instantMix.postValue(current); instantMix.postValue(current);
}); });
@ -130,6 +138,7 @@ public class SongRepository {
isComplete = true; isComplete = true;
} }
} }
} }
private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) { private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) {
@ -138,7 +147,7 @@ public class SongRepository {
fetchSimilarByArtist(id, count, callback); fetchSimilarByArtist(id, count, callback);
break; break;
case ALBUM: case ALBUM:
fetchAlbumSongsThenSimilar(id, count, callback); fetchAlbumSongs(id, count, callback);
break; break;
case TRACK: case TRACK:
fetchSingleTrackThenSimilar(id, count, callback); fetchSingleTrackThenSimilar(id, count, callback);
@ -146,7 +155,7 @@ public class SongRepository {
} }
} }
private void fetchAlbumSongsThenSimilar(String albumId, int count, MediaCallbackInternal callback) { private void fetchAlbumSongs(String albumId, int count, MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback<ApiResponse>() { App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback<ApiResponse>() {
@Override @Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) { public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
@ -158,24 +167,13 @@ public class SongRepository {
List<Child> limitedAlbumSongs = albumSongs.subList(0, fromAlbum); List<Child> limitedAlbumSongs = albumSongs.subList(0, fromAlbum);
callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs)); callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs));
int remaining = count - fromAlbum;
if (remaining > 0 && albumSongs.get(0).getArtistId() != null) {
fetchSimilarByArtist(albumSongs.get(0).getArtistId(), remaining, callback);
} else if (remaining > 0) {
Log.d(TAG, "No artistId available, skipping similar artist fetch");
}
return;
} }
} }
Log.d(TAG, "Album fetch failed or empty, calling fillWithRandom");
fillWithRandom(count, callback);
} }
@Override @Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.d(TAG, "Album fetch failed: " + t.getMessage()); Log.e(TAG, "fetchAlbumSongsThenSimilar.onFailure()", t);
fillWithRandom(count, callback);
} }
}); });
} }
@ -188,17 +186,17 @@ public class SongRepository {
@Override @Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) { public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> similar = extractSongs(response, "similarSongs2"); List<Child> similar = extractSongs(response, "similarSongs2");
Log.d(TAG, "fetchSimilarByArtist.onResponse() - similar songs: " + similar.size());
if (!similar.isEmpty()) { if (!similar.isEmpty()) {
List<Child> limitedSimilar = similar.subList(0, Math.min(count, similar.size())); List<Child> limitedSimilar = similar.subList(0, Math.min(count, similar.size()));
callback.onSongsAvailable(limitedSimilar); callback.onSongsAvailable(limitedSimilar);
} else {
fillWithRandom(count, callback);
} }
} }
@Override @Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fillWithRandom(count, callback); Log.e(TAG, "fetchSimilarByArtist.onFailure()", t);
} }
}); });
} }
@ -211,17 +209,13 @@ public class SongRepository {
Child song = response.body().getSubsonicResponse().getSong(); Child song = response.body().getSubsonicResponse().getSong();
if (song != null) { if (song != null) {
callback.onSongsAvailable(Collections.singletonList(song)); callback.onSongsAvailable(Collections.singletonList(song));
int remaining = count - 1;
if (remaining > 0) {
fetchSimilarOnly(trackId, remaining, callback);
}
return;
} }
} }
fillWithRandom(count, callback);
} }
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fillWithRandom(count, callback); @Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.e(TAG, "fetchSingleTrackThenSimilar.onFailure()", t);
} }
}); });
} }
@ -232,38 +226,40 @@ public class SongRepository {
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) { public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = extractSongs(response, "similarSongs"); List<Child> songs = extractSongs(response, "similarSongs");
if (!songs.isEmpty()) { if (!songs.isEmpty()) {
List<Child> limitedSongs = songs.subList(0, Math.min(count, songs.size())); int limit = Math.min(count, songs.size());
callback.onSongsAvailable(limitedSongs); callback.onSongsAvailable(songs.subList(0, limit));
} else {
fillWithRandom(count, callback);
} }
} }
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fillWithRandom(count, callback); @Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.e(TAG, "fetchSimilarOnly.onFailure()", t);
} }
}); });
} }
private void fillWithRandom(int target, final MediaCallbackInternal callback) { public MutableLiveData<List<Child>> getContinuousMix(String id, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getAlbumSongListClient() .getBrowsingClient()
.getRandomSongs(target, null, null) .getSimilarSongs(id, count)
.enqueue(new Callback<ApiResponse>() { .enqueue(new Callback<ApiResponse>() {
@Override @Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) { public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> random = extractSongs(response, "randomSongs"); if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs() != null) {
if (!random.isEmpty()) { instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs().getSongs());
List<Child> limitedRandom = random.subList(0, Math.min(target, random.size()));
callback.onSongsAvailable(limitedRandom);
} else {
callback.onSongsAvailable(new ArrayList<>());
} }
} }
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
callback.onSongsAvailable(new ArrayList<>()); @Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
instantMix.setValue(null);
} }
}); });
return instantMix;
} }
private List<Child> extractSongs(Response<ApiResponse> response, String type) { private List<Child> extractSongs(Response<ApiResponse> response, String type) {
@ -274,11 +270,10 @@ public class SongRepository {
list = res.getSimilarSongs().getSongs(); list = res.getSimilarSongs().getSongs();
} else if (type.equals("similarSongs2") && res.getSimilarSongs2() != null) { } else if (type.equals("similarSongs2") && res.getSimilarSongs2() != null) {
list = res.getSimilarSongs2().getSongs(); list = res.getSimilarSongs2().getSongs();
} else if (type.equals("randomSongs") && res.getRandomSongs() != null) {
list = res.getRandomSongs().getSongs();
} }
return (list != null) ? list : new ArrayList<>(); return (list != null) ? list : new ArrayList<>();
} }
return new ArrayList<>(); return new ArrayList<>();
} }
@ -299,6 +294,7 @@ public class SongRepository {
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) { public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>(); MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback<ApiResponse>() { App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) { @Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = new ArrayList<>(); List<Child> songs = new ArrayList<>();

View file

@ -448,7 +448,8 @@ public class MediaManager {
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
Preferences.setLastInstantMix(); Preferences.setLastInstantMix();
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, SeedType.TRACK, 10); LiveData<List<Child>> instantMix = getSongRepository().getContinuousMix(mediaItem.mediaId,25);
instantMix.observeForever(new Observer<List<Child>>() { instantMix.observeForever(new Observer<List<Child>>() {
@Override @Override
public void onChanged(List<Child> media) { public void onChanged(List<Child> media) {

View file

@ -138,7 +138,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
public LiveData<List<Child>> getAlbumInstantMix(LifecycleOwner owner, AlbumID3 album) { public LiveData<List<Child>> getAlbumInstantMix(LifecycleOwner owner, AlbumID3 album) {
instantMix.setValue(Collections.emptyList()); instantMix.setValue(Collections.emptyList());
albumRepository.getInstantMix(album, 20).observe(owner, instantMix::postValue); albumRepository.getInstantMix(album, 30).observe(owner, instantMix::postValue);
return instantMix; return instantMix;
} }

View file

@ -126,7 +126,7 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
public LiveData<List<Child>> getArtistInstantMix(LifecycleOwner owner, ArtistID3 artist) { public LiveData<List<Child>> getArtistInstantMix(LifecycleOwner owner, ArtistID3 artist) {
instantMix.setValue(Collections.emptyList()); instantMix.setValue(Collections.emptyList());
artistRepository.getInstantMix(artist, 20).observe(owner, instantMix::postValue); artistRepository.getInstantMix(artist, 30).observe(owner, instantMix::postValue);
return instantMix; return instantMix;
} }

View file

@ -60,7 +60,7 @@ public class ArtistPageViewModel extends AndroidViewModel {
} }
public LiveData<List<Child>> getArtistInstantMix() { public LiveData<List<Child>> getArtistInstantMix() {
return artistRepository.getInstantMix(artist, 20); return artistRepository.getInstantMix(artist, 30);
} }
public ArtistID3 getArtist() { public ArtistID3 getArtist() {

View file

@ -130,7 +130,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) { public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
instantMix.setValue(Collections.emptyList()); instantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, instantMix::postValue); songRepository.getInstantMix(media.getId(), SeedType.TRACK, 30).observe(owner, instantMix::postValue);
return instantMix; return instantMix;
} }