From 320cdf199f054bbe7f29fbe8e83a04260968ea11 Mon Sep 17 00:00:00 2001 From: spicyPoke Date: Wed, 3 Jun 2026 21:18:26 +0700 Subject: [PATCH 1/2] fix: fallback to random tracks if getSimilar returns empty --- .../tempo/repository/SongRepository.java | 42 +++++++-- .../tempo/service/MediaManager.java | 91 ++++++++++++------- 2 files changed, 93 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java index 6d5d17eed..70da4b62f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -239,27 +239,51 @@ public void onFailure(@NonNull Call call, @NonNull Throwable t) { } - public MutableLiveData> getContinuousMix(String id, int count) { - MutableLiveData> instantMix = new MutableLiveData<>(); + public MutableLiveData> getContinuousMix(String trackId, String artistId, int count) { + MutableLiveData> continuousMix = new MutableLiveData<>(); + + if (artistId != null && !artistId.isEmpty()) { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(artistId, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = extractSongs(response, "similarSongs2"); + if (!songs.isEmpty()) { + continuousMix.postValue(songs); + } else { + fetchContinuousFallback(trackId, count, continuousMix); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fetchContinuousFallback(trackId, count, continuousMix); + } + }); + } else { + fetchContinuousFallback(trackId, count, continuousMix); + } + return continuousMix; + } + + private void fetchContinuousFallback(String trackId, int count, MutableLiveData> target) { App.getSubsonicClientInstance(false) .getBrowsingClient() - .getSimilarSongs(id, count) + .getSimilarSongs(trackId, count) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs() != null) { - instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs().getSongs()); - } + target.postValue(extractSongs(response, "similarSongs")); } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - instantMix.setValue(null); + target.postValue(new ArrayList<>()); } }); - - return instantMix; } private List extractSongs(Response response, String type) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index e274471cc..1c720d6a0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -35,7 +35,9 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -479,50 +481,77 @@ public static void continuousPlay(MediaItem mediaItem, Preferences.setLastInstantMix(); continuousPlayIsRunning.set(true); + String trackId = mediaItem.mediaId; + String artistId = mediaItem.mediaMetadata.extras != null + ? mediaItem.mediaMetadata.extras.getString("artistId") + : null; + LiveData> instantMix = - getSongRepository().getContinuousMix(mediaItem.mediaId, 25); + getSongRepository().getContinuousMix(trackId, artistId, 25); instantMix.observeForever(new Observer>() { @Override public void onChanged(List media) { - if (media == null || media.isEmpty()) { - Log.w(TAG, "Continuous Play: no similar track found. Is server correctly configured?"); + instantMix.removeObserver(this); + + // Filter against current queue before deciding if we need fallback. + // getSimilarSongs2 doesn't know what's already queued, so it may + // return tracks we already have. Filter first, then decide. + if (media != null && !media.isEmpty()) { + List filtered = dedupAgainstQueue(media, existingBrowserFuture); + if (!filtered.isEmpty()) { + Log.d(TAG, "Continuous Play: adding " + filtered.size() + " similar tracks"); + enqueue(existingBrowserFuture, filtered, true); + continuousPlayIsRunning.set(false); + return; + } } - else - { - if (existingBrowserFuture != null) { - Log.d(TAG, "Continuous Play: found " + media.size() + " similar tracks"); - - final MediaBrowser browser; - try { - browser = existingBrowserFuture.get(); - } catch (ExecutionException | InterruptedException e) { - Log.e(TAG, "Continuous Play: browser unavailable", e); - instantMix.removeObserver(this); - continuousPlayIsRunning.set(false); - return; - } - List filteredMedia; - List currentIds = new ArrayList<>(); - for (int i = 0; i < Objects.requireNonNull(browser).getMediaItemCount(); i++) { - currentIds.add(browser.getMediaItemAt(i).mediaId); + Log.w(TAG, "Continuous Play: no new similar tracks, falling back to random songs"); + LiveData> randomSongs = getSongRepository().getRandomSample(25, null, null); + randomSongs.observeForever(new Observer>() { + @Override + public void onChanged(List random) { + randomSongs.removeObserver(this); + if (random != null && !random.isEmpty()) { + List filtered = dedupAgainstQueue(random, existingBrowserFuture); + if (!filtered.isEmpty()) { + Log.d(TAG, "Continuous Play: adding " + filtered.size() + " random tracks"); + enqueue(existingBrowserFuture, filtered, true); + } else { + Log.w(TAG, "Continuous Play: random tracks already in queue"); + } + } else { + Log.w(TAG, "Continuous Play: random fallback also empty"); } - filteredMedia = media.stream() - .filter(child -> !currentIds.contains(child.getId())) - .collect(Collectors.toList()); - - Log.d(TAG, "Continuous Play: adding " + filteredMedia.size() + " tracks to queue"); - enqueue(existingBrowserFuture, filteredMedia, true); + continuousPlayIsRunning.set(false); } - } - instantMix.removeObserver(this); - continuousPlayIsRunning.set(false); - if (onComplete != null) onComplete.run(); + }); } }); } + private static List dedupAgainstQueue(List candidates, + ListenableFuture existingBrowserFuture) { + if (existingBrowserFuture == null) return new ArrayList<>(candidates); + + final MediaBrowser browser; + try { + browser = existingBrowserFuture.get(); + } catch (ExecutionException | InterruptedException e) { + return new ArrayList<>(candidates); + } + + Set currentIds = new HashSet<>(); + for (int i = 0; i < Objects.requireNonNull(browser).getMediaItemCount(); i++) { + currentIds.add(browser.getMediaItemAt(i).mediaId); + } + + return candidates.stream() + .filter(child -> !currentIds.contains(child.getId())) + .collect(Collectors.toList()); + } + public static void saveChronology(MediaItem mediaItem) { if (mediaItem != null) { getChronologyRepository().insert(new Chronology(mediaItem)); From 6c87675a24d434f2049e28d3379555c5bb264f01 Mon Sep 17 00:00:00 2001 From: spicyPoke Date: Tue, 9 Jun 2026 22:39:37 +0700 Subject: [PATCH 2/2] feat: add fallback to random tracks preference switch --- .../tempo/service/MediaManager.java | 39 +++++++++++-------- .../tempo/util/Preferences.kt | 13 +++++++ app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/global_preferences.xml | 6 +++ 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java index 1c720d6a0..f84c7869f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -507,26 +507,31 @@ public void onChanged(List media) { } } - Log.w(TAG, "Continuous Play: no new similar tracks, falling back to random songs"); - LiveData> randomSongs = getSongRepository().getRandomSample(25, null, null); - randomSongs.observeForever(new Observer>() { - @Override - public void onChanged(List random) { - randomSongs.removeObserver(this); - if (random != null && !random.isEmpty()) { - List filtered = dedupAgainstQueue(random, existingBrowserFuture); - if (!filtered.isEmpty()) { - Log.d(TAG, "Continuous Play: adding " + filtered.size() + " random tracks"); - enqueue(existingBrowserFuture, filtered, true); + if (Preferences.isFallbackToRandomTracksEnabled()) { + Log.w(TAG, "Continuous Play: no new similar tracks, falling back to random songs"); + LiveData> randomSongs = getSongRepository().getRandomSample(25, null, null); + randomSongs.observeForever(new Observer>() { + @Override + public void onChanged(List random) { + randomSongs.removeObserver(this); + if (random != null && !random.isEmpty()) { + List filtered = dedupAgainstQueue(random, existingBrowserFuture); + if (!filtered.isEmpty()) { + Log.d(TAG, "Continuous Play: adding " + filtered.size() + " random tracks"); + enqueue(existingBrowserFuture, filtered, true); + } else { + Log.w(TAG, "Continuous Play: random tracks already in queue"); + } } else { - Log.w(TAG, "Continuous Play: random tracks already in queue"); + Log.w(TAG, "Continuous Play: random fallback also empty"); } - } else { - Log.w(TAG, "Continuous Play: random fallback also empty"); + continuousPlayIsRunning.set(false); } - continuousPlayIsRunning.set(false); - } - }); + }); + } else { + Log.w(TAG, "Continuous Play: no new similar tracks, random fallback disabled"); + continuousPlayIsRunning.set(false); + } } }); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index c8fc8353b..414b94813 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -80,6 +80,7 @@ object Preferences { private const val NEXT_UPDATE_CHECK = "next_update_check" private const val GITHUB_UPDATE_CHECK = "github_update_check" private const val CONTINUOUS_PLAY = "continuous_play" + private const val FALLBACK_TO_RANDOM_TRACKS = "fallback_to_random_tracks" private const val LAST_INSTANT_MIX = "last_instant_mix" private const val ALLOW_PLAYLIST_DUPLICATES = "allow_playlist_duplicates" private const val HOME_SORT_PLAYLISTS = "home_sort_playlists" @@ -717,6 +718,18 @@ object Preferences { return App.getInstance().preferences.getBoolean(CONTINUOUS_PLAY, true) } + @JvmStatic + fun isFallbackToRandomTracksEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(FALLBACK_TO_RANDOM_TRACKS, false) + } + + @JvmStatic + fun setFallbackToRandomTracksEnabled(enabled: Boolean) { + App.getInstance().preferences.edit() + .putBoolean(FALLBACK_TO_RANDOM_TRACKS, enabled) + .apply() + } + @JvmStatic fun setLastInstantMix() { App.getInstance().preferences.edit().putLong(LAST_INSTANT_MIX, System.currentTimeMillis()).apply() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3107956b9..5a273a2c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -373,6 +373,8 @@ Clear download folder Allows music to keep playing after a playlist has ended, playing similar songs Continuous play + Play random songs as a fallback when similar tracks are unavailable + Fallback to random tracks Size of artwork cache In order to reduce data consumption, avoid downloading covers. Limit mobile data usage diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index f53276483..eb7d655c3 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -227,6 +227,12 @@ android:summary="@string/settings_continuous_play_summary" android:key="continuous_play" /> + +