fix: address duplicate track bug, wrong order in queue, and updated album instant mix

This commit is contained in:
eddyizm 2026-01-01 11:50:48 -08:00
parent 193447d07e
commit d04ed8d430
No known key found for this signature in database
GPG key ID: CF5F671829E8158A
6 changed files with 120 additions and 90 deletions

View file

@ -1,5 +1,7 @@
package com.cappielloantonio.tempo.repository; package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
@ -22,6 +24,7 @@ import retrofit2.Response;
public class SongRepository { public class SongRepository {
private static final String TAG = "SongRepository";
public interface MediaCallbackInternal { public interface MediaCallbackInternal {
void onSongsAvailable(List<Child> songs); void onSongsAvailable(List<Child> songs);
} }
@ -126,7 +129,6 @@ public class SongRepository {
originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs)); originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs));
if (accumulatedSongs.size() >= targetCount) { if (accumulatedSongs.size() >= targetCount) {
isComplete = true; isComplete = true;
android.util.Log.d("SongRepository", "Reached target of " + targetCount + " songs");
} }
} }
} }
@ -150,23 +152,56 @@ public class SongRepository {
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) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { if (response.isSuccessful() && response.body() != null &&
response.body().getSubsonicResponse().getAlbum() != null) {
List<Child> albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); List<Child> albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs();
if (albumSongs != null && !albumSongs.isEmpty()) { if (albumSongs != null && !albumSongs.isEmpty()) {
List<Child> limitedAlbumSongs = albumSongs.subList(0, Math.min(count, albumSongs.size())); int fromAlbum = Math.min(count, albumSongs.size());
List<Child> limitedAlbumSongs = albumSongs.subList(0, fromAlbum);
callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs)); callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs));
// If we need more, get similar songs int remaining = count - fromAlbum;
int remaining = count - limitedAlbumSongs.size(); if (remaining > 0 && albumSongs.get(0).getArtistId() != null) {
if (remaining > 0) {
fetchSimilarByArtist(albumSongs.get(0).getArtistId(), remaining, callback); fetchSimilarByArtist(albumSongs.get(0).getArtistId(), remaining, callback);
} else if (remaining > 0) {
Log.d(TAG, "No artistId available, skipping similar artist fetch");
} }
return; return;
} }
} }
Log.d(TAG, "Album fetch failed or empty, calling fillWithRandom");
fillWithRandom(count, callback); fillWithRandom(count, callback);
} }
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.d(TAG, "Album fetch failed: " + t.getMessage());
fillWithRandom(count, callback);
}
});
}
private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) {
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> similar = extractSongs(response, "similarSongs2");
if (!similar.isEmpty()) {
List<Child> limitedSimilar = similar.subList(0, Math.min(count, similar.size()));
callback.onSongsAvailable(limitedSimilar);
} else {
Log.d(TAG, "No similar songs, calling fillWithRandom");
fillWithRandom(count, callback);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.d(TAG, "getSimilarSongs2 failed: " + t.getMessage());
fillWithRandom(count, callback); fillWithRandom(count, callback);
} }
}); });
@ -195,7 +230,7 @@ public class SongRepository {
}); });
} }
private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback<ApiResponse>() { App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).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) {
@ -211,29 +246,9 @@ private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callba
fillWithRandom(count, callback); fillWithRandom(count, callback);
} }
}); });
}
private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) {
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> similar = extractSongs(response, "similarSongs2");
if (!similar.isEmpty()) {
List<Child> limitedSimilar = similar.subList(0, Math.min(count, similar.size()));
callback.onSongsAvailable(limitedSimilar);
} else {
fillWithRandom(count, callback);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fillWithRandom(count, callback);
}
});
} }
private void fillWithRandom(int target, final MediaCallbackInternal callback) { private void fillWithRandom(int target, final MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getAlbumSongListClient() .getAlbumSongListClient()

View file

@ -184,44 +184,33 @@ public class MediaManager {
@OptIn(markerClass = UnstableApi.class) @OptIn(markerClass = UnstableApi.class)
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) { public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
if (mediaBrowserListenableFuture != null) { if (mediaBrowserListenableFuture != null) {
Log.d(TAG, "startQueue called with " + (media != null ? media.size() : 0) + " songs");
mediaBrowserListenableFuture.addListener(() -> { mediaBrowserListenableFuture.addListener(() -> {
try { try {
if (mediaBrowserListenableFuture.isDone()) { if (mediaBrowserListenableFuture.isDone()) {
Log.d(TAG, "MediaBrowser future is done");
final MediaBrowser browser = mediaBrowserListenableFuture.get(); final MediaBrowser browser = mediaBrowserListenableFuture.get();
Log.d(TAG, "Got MediaBrowser, connected: " + browser.isConnected());
final List<MediaItem> items = MappingUtil.mapMediaItems(media); final List<MediaItem> items = MappingUtil.mapMediaItems(media);
Log.d(TAG, "Mapped " + items.size() + " media items");
new Handler(Looper.getMainLooper()).post(() -> { new Handler(Looper.getMainLooper()).post(() -> {
Log.d(TAG, "Setting " + items.size() + " media items at index " + startIndex);
justStarted.set(true); justStarted.set(true);
browser.setMediaItems(items, startIndex, 0); browser.setMediaItems(items, startIndex, 0);
browser.prepare(); browser.prepare();
Log.d(TAG, "setMediaItems and prepare called");
Player.Listener timelineListener = new Player.Listener() { Player.Listener timelineListener = new Player.Listener() {
@Override @Override
public void onTimelineChanged(Timeline timeline, int reason) { public void onTimelineChanged(Timeline timeline, int reason) {
Log.d(TAG, "onTimelineChanged: itemCount=" + browser.getMediaItemCount() + ", reason=" + reason);
int itemCount = browser.getMediaItemCount(); int itemCount = browser.getMediaItemCount();
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
Log.d(TAG, "Seeking to " + startIndex + " and playing");
browser.seekTo(startIndex, 0); browser.seekTo(startIndex, 0);
browser.play(); browser.play();
browser.removeListener(this); browser.removeListener(this);
Log.d(TAG, "Playback started");
} else { } else {
Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex); Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex);
} }
} }
}; };
Log.d(TAG, "Adding timeline listener");
browser.addListener(timelineListener); browser.addListener(timelineListener);
}); });

View file

@ -5,6 +5,8 @@ import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -1253,19 +1255,24 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture); MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
} }
@Override
public void onMediaClick(Bundle bundle) { public void onMediaClick(Bundle bundle) {
if (bundle.containsKey(Constants.MEDIA_MIX)) { if (bundle.containsKey(Constants.MEDIA_MIX)) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelable(Constants.TRACK_OBJECT)); Child track = bundle.getParcelable(Constants.TRACK_OBJECT);
activity.setBottomSheetInPeek(true); activity.setBottomSheetInPeek(true);
if (mediaBrowserListenableFuture != null) { if (mediaBrowserListenableFuture != null) {
homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> { final boolean[] playbackStarted = {false};
MusicUtil.ratingFilter(songs);
if (songs != null && !songs.isEmpty()) { homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), track)
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); .observe(getViewLifecycleOwner(), songs -> {
} if (playbackStarted[0] || songs == null || songs.isEmpty()) return;
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (playbackStarted[0]) return;
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
playbackStarted[0] = true;
}, 300);
}); });
} }
} else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) { } else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) {

View file

@ -5,6 +5,8 @@ import android.content.ClipboardManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -61,6 +63,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList(); private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture; private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private static final String TAG = "AlbumBottomSheetDialog";
@Nullable @Nullable
@Override @Override
@ -117,11 +120,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
TextView playRadio = view.findViewById(R.id.play_radio_text_view); TextView playRadio = view.findViewById(R.id.play_radio_text_view);
playRadio.setOnClickListener(v -> { playRadio.setOnClickListener(v -> {
AlbumRepository albumRepository = new AlbumRepository(); new AlbumRepository().getInstantMix(album, 20, new MediaCallback() {
albumRepository.getInstantMix(album, 20, new MediaCallback() {
@Override @Override
public void onError(Exception exception) { public void onError(Exception exception) {
exception.printStackTrace(); Log.e(TAG, "Error: " + exception.getMessage());
} }
@Override @Override
@ -149,7 +151,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
} }
} }
} catch (Exception e) { } catch (Exception e) {
// Ignore Log.e(TAG, "Error checking playback: " + e.getMessage());
} }
view.postDelayed(() -> dismissBottomSheet(), 200); view.postDelayed(() -> dismissBottomSheet(), 200);
}, 300); }, 300);

View file

@ -5,6 +5,8 @@ import android.content.ClipboardManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -143,21 +145,36 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
TextView playRadio = view.findViewById(R.id.play_radio_text_view); TextView playRadio = view.findViewById(R.id.play_radio_text_view);
playRadio.setOnClickListener(v -> { playRadio.setOnClickListener(v -> {
MediaManager.startQueue(mediaBrowserListenableFuture, song);
((MainActivity) requireActivity()).setBottomSheetInPeek(true); ((MainActivity) requireActivity()).setBottomSheetInPeek(true);
final boolean[] playbackStarted = {false};
songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> { songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> {
if (playbackStarted[0] || songs == null || songs.isEmpty()) return;
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (playbackStarted[0]) return;
MusicUtil.ratingFilter(songs); MusicUtil.ratingFilter(songs);
if (songs == null) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
playbackStarted[0] = true;
view.postDelayed(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
MediaBrowser browser = mediaBrowserListenableFuture.get();
if (browser != null && browser.isPlaying()) {
dismissBottomSheet(); dismissBottomSheet();
return; return;
} }
if (!songs.isEmpty()) {
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
dismissBottomSheet();
} }
} catch (Exception e) {
// Ignore
}
view.postDelayed(() -> dismissBottomSheet(), 200);
}, 300);
}, 300);
}); });
}); });