Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -239,27 +239,51 @@ public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}


public MutableLiveData<List<Child>> getContinuousMix(String id, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
public MutableLiveData<List<Child>> getContinuousMix(String trackId, String artistId, int count) {
MutableLiveData<List<Child>> continuousMix = new MutableLiveData<>();

if (artistId != null && !artistId.isEmpty()) {
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(artistId, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = extractSongs(response, "similarSongs2");
if (!songs.isEmpty()) {
continuousMix.postValue(songs);
} else {
fetchContinuousFallback(trackId, count, continuousMix);
}
}

@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fetchContinuousFallback(trackId, count, continuousMix);
}
});
} else {
fetchContinuousFallback(trackId, count, continuousMix);
}

return continuousMix;
}

private void fetchContinuousFallback(String trackId, int count, MutableLiveData<List<Child>> target) {
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs(id, count)
.getSimilarSongs(trackId, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> 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<ApiResponse> call, @NonNull Throwable t) {
instantMix.setValue(null);
target.postValue(new ArrayList<>());
}
});

return instantMix;
}

private List<Child> extractSongs(Response<ApiResponse> response, String type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -479,50 +481,82 @@ 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<List<Child>> instantMix =
getSongRepository().getContinuousMix(mediaItem.mediaId, 25);
getSongRepository().getContinuousMix(trackId, artistId, 25);

instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> 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<Child> 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<Child> filteredMedia;
List<String> currentIds = new ArrayList<>();
for (int i = 0; i < Objects.requireNonNull(browser).getMediaItemCount(); i++) {
currentIds.add(browser.getMediaItemAt(i).mediaId);
if (Preferences.isFallbackToRandomTracksEnabled()) {
Log.w(TAG, "Continuous Play: no new similar tracks, falling back to random songs");
LiveData<List<Child>> randomSongs = getSongRepository().getRandomSample(25, null, null);
randomSongs.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> random) {
randomSongs.removeObserver(this);
if (random != null && !random.isEmpty()) {
List<Child> 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");
}
continuousPlayIsRunning.set(false);
}
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);
}
});
} else {
Log.w(TAG, "Continuous Play: no new similar tracks, random fallback disabled");
continuousPlayIsRunning.set(false);
}
instantMix.removeObserver(this);
continuousPlayIsRunning.set(false);
if (onComplete != null) onComplete.run();
}
});
}

private static List<Child> dedupAgainstQueue(List<Child> candidates,
ListenableFuture<MediaBrowser> existingBrowserFuture) {
if (existingBrowserFuture == null) return new ArrayList<>(candidates);

final MediaBrowser browser;
try {
browser = existingBrowserFuture.get();
} catch (ExecutionException | InterruptedException e) {
return new ArrayList<>(candidates);
}

Set<String> 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));
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@
<string name="settings_clear_download_folder">Clear download folder</string>
<string name="settings_continuous_play_summary">Allows music to keep playing after a playlist has ended, playing similar songs</string>
<string name="settings_continuous_play_title">Continuous play</string>
<string name="settings_fallback_to_random_tracks_summary">Play random songs as a fallback when similar tracks are unavailable</string>
<string name="settings_fallback_to_random_tracks_title">Fallback to random tracks</string>
<string name="settings_covers_cache">Size of artwork cache</string>
<string name="settings_data_saving_mode_summary">In order to reduce data consumption, avoid downloading covers.</string>
<string name="settings_data_saving_mode_title">Limit mobile data usage</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/xml/global_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@
android:summary="@string/settings_continuous_play_summary"
android:key="continuous_play" />

<SwitchPreference
android:title="@string/settings_fallback_to_random_tracks_title"
android:defaultValue="false"
android:summary="@string/settings_fallback_to_random_tracks_summary"
android:key="fallback_to_random_tracks" />

<ListPreference
app:defaultValue="[heartID]"
app:dialogTitle="@string/settings_custom_command_first_button"
Expand Down