From 3c1975f6bf86a0efb2197b9235485e510f9c22b0 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Fri, 26 Dec 2025 17:03:41 -0800 Subject: [PATCH 01/24] wip: initial refactor of instant mix in to be used everywhere else --- .../tempo/repository/SongRepository.java | 334 +++++++++--------- 1 file changed, 174 insertions(+), 160 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 a40b3c97..4457d37b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -6,17 +6,22 @@ import androidx.lifecycle.MutableLiveData; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class SongRepository { - private static final String TAG = "SongRepository"; + + public interface MediaCallbackInternal { + void onSongsAvailable(List songs); + } public MutableLiveData> getStarredSongs(boolean random, int size) { MutableLiveData> starredSongs = new MutableLiveData<>(Collections.emptyList()); @@ -42,219 +47,228 @@ public class SongRepository { } @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } + public void onFailure(@NonNull Call call, @NonNull Throwable t) {} }); return starredSongs; } + /** + * Used by ViewModels. Updates the LiveData list incrementally as songs are found. + */ public MutableLiveData> getInstantMix(String id, int count) { - MutableLiveData> instantMix = new MutableLiveData<>(); + MutableLiveData> instantMix = new MutableLiveData<>(new ArrayList<>()); - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs2(id, count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) { - instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - instantMix.setValue(null); - } - }); + performSmartMix(id, count, songs -> { + List current = instantMix.getValue(); + if (current != null) { + for (Child s : songs) { + if (!current.contains(s)) current.add(s); + } + instantMix.postValue(current); + } + }); return instantMix; } - public MutableLiveData> getRandomSample(int number, Integer fromYear, Integer toYear) { - MutableLiveData> randomSongsSample = new MutableLiveData<>(); + /** + * Overloaded method used by other Repositories + */ + public void getInstantMix(String id, int count, MediaCallbackInternal callback) { + performSmartMix(id, count, callback); + } + private void performSmartMix(final String id, final int count, final MediaCallbackInternal callback) { App.getSubsonicClientInstance(false) - .getAlbumSongListClient() - .getRandomSongs(number, fromYear, toYear) + .getBrowsingClient() + .getSimilarSongs(id, count) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = new ArrayList<>(); - - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) { - songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs()); + List songs = extractSongs(response, "similarSongs"); + if (!songs.isEmpty()) { + callback.onSongsAvailable(songs); + } + if (songs.size() < count / 2) { + fetchContextAndSeed(id, count, callback); } - - randomSongsSample.setValue(songs); } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - + fetchContextAndSeed(id, count, callback); } }); + } + private void fetchContextAndSeed(final String id, final int count, final MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getAlbum(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { + List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); + if (albumSongs != null && !albumSongs.isEmpty()) { + callback.onSongsAvailable(new ArrayList<>(albumSongs)); + String seedArtistId = albumSongs.get(0).getArtistId(); + fetchSimilarByArtist(seedArtistId, count, callback); + return; + } + } + fillWithRandom(count, callback); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); + } + + private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(artistId, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List similar = extractSongs(response, "similarSongs2"); + if (!similar.isEmpty()) { + callback.onSongsAvailable(similar); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); + } + + private void fillWithRandom(int target, final MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getRandomSongs(target, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + callback.onSongsAvailable(extractSongs(response, "randomSongs")); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); + } + + private List extractSongs(Response response, String type) { + if (response.isSuccessful() && response.body() != null) { + SubsonicResponse res = response.body().getSubsonicResponse(); + List list = null; + if (type.equals("similarSongs") && res.getSimilarSongs() != null) { + list = res.getSimilarSongs().getSongs(); + } else if (type.equals("similarSongs2") && res.getSimilarSongs2() != null) { + list = res.getSimilarSongs2().getSongs(); + } else if (type.equals("randomSongs") && res.getRandomSongs() != null) { + list = res.getRandomSongs().getSongs(); + } + return (list != null) ? list : new ArrayList<>(); + } + return new ArrayList<>(); + } + public MutableLiveData> getRandomSample(int number, Integer fromYear, Integer toYear) { + MutableLiveData> randomSongsSample = new MutableLiveData<>(); + App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) { + songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs())); + } + randomSongsSample.setValue(songs); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return randomSongsSample; } public MutableLiveData> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) { MutableLiveData> randomSongsSample = new MutableLiveData<>(); - - App.getSubsonicClientInstance(false) - .getAlbumSongListClient() - .getRandomSongs(number, fromYear, toYear, genre) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = new ArrayList<>(); - - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) { - songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs()); - } - - randomSongsSample.setValue(songs); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) { + songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getRandomSongs().getSongs())); + } + randomSongsSample.setValue(songs); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return randomSongsSample; } public void scrobble(String id, boolean submission) { - App.getSubsonicClientInstance(false) - .getMediaAnnotationClient() - .scrobble(id, submission) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) {} + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); } public void setRating(String id, int rating) { - App.getSubsonicClientInstance(false) - .getMediaAnnotationClient() - .setRating(id, rating) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + App.getSubsonicClientInstance(false).getMediaAnnotationClient().setRating(id, rating).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) {} + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); } public MutableLiveData> getSongsByGenre(String id, int page) { MutableLiveData> songsByGenre = new MutableLiveData<>(); - - App.getSubsonicClientInstance(false) - .getAlbumSongListClient() - .getSongsByGenre(id, 100, 100 * page) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { - songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 100, 100 * page).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { + songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs()); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return songsByGenre; } public MutableLiveData> getSongsByGenres(ArrayList genresId) { MutableLiveData> songsByGenre = new MutableLiveData<>(); - - for (String id : genresId) - App.getSubsonicClientInstance(false) - .getAlbumSongListClient() - .getSongsByGenre(id, 500, 0) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = new ArrayList<>(); - - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { - songs.addAll(response.body().getSubsonicResponse().getSongsByGenre().getSongs()); - } - - songsByGenre.setValue(songs); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + for (String id : genresId) { + App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 500, 0).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { + songs.addAll(Objects.requireNonNull(response.body().getSubsonicResponse().getSongsByGenre().getSongs())); + } + songsByGenre.setValue(songs); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); + } return songsByGenre; } public MutableLiveData getSong(String id) { MutableLiveData song = new MutableLiveData<>(); - - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSong(id) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null) { - song.setValue(response.body().getSubsonicResponse().getSong()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + App.getSubsonicClientInstance(false).getBrowsingClient().getSong(id).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + song.setValue(response.body().getSubsonicResponse().getSong()); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return song; } public MutableLiveData getSongLyrics(Child song) { MutableLiveData lyrics = new MutableLiveData<>(null); - - App.getSubsonicClientInstance(false) - .getMediaRetrievalClient() - .getLyrics(song.getArtist(), song.getTitle()) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) { - lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - + App.getSubsonicClientInstance(false).getMediaRetrievalClient().getLyrics(song.getArtist(), song.getTitle()).enqueue(new Callback() { + @Override public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) { + lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue()); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + }); return lyrics; } -} +} \ No newline at end of file From 2624f396e5c94f4cb0e5f2cd42518be05402cfd3 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Fri, 26 Dec 2025 22:08:07 -0800 Subject: [PATCH 02/24] wip: refactor album repo to use the song repo instant mix --- .../tempo/repository/AlbumRepository.java | 40 +++++-------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java index c3533549..7de91d18 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.repository; import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; + import android.util.Log; import com.cappielloantonio.tempo.App; @@ -204,38 +205,15 @@ public class AlbumRepository { return albumInfo; } - public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) { - Log.d("AlbumRepository", "Attempting getInstantMix for AlbumID: " + album.getId()); - - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs2(album.getId(), count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = new ArrayList<>(); + public void getInstantMix(AlbumID3 album, int count, final MediaCallback callback) { + Log.d("AlbumRepository", "Starting Instant Mix for album: " + album.getName()); - if (response.isSuccessful() - && response.body() != null - && response.body().getSubsonicResponse().getSimilarSongs2() != null) { - - List similarSongs = response.body().getSubsonicResponse().getSimilarSongs2().getSongs(); - - if (similarSongs == null) { - Log.w("AlbumRepository", "API successful but 'songs' list was NULL for AlbumID: " + album.getId()); - } else { - songs.addAll(similarSongs); - } - } + new SongRepository().getInstantMix(album.getId(), count, songs -> { - callback.onLoadMedia(songs); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - callback.onLoadMedia(new ArrayList<>()); - } - }); + if (songs != null && !songs.isEmpty()) { + callback.onLoadMedia(songs); + } + }); } public MutableLiveData> getDecades() { @@ -248,7 +226,7 @@ public class AlbumRepository { @Override public void onLoadYear(int last) { if (first != -1 && last != -1) { - List decadeList = new ArrayList(); + List decadeList = new ArrayList<>(); int startDecade = first - (first % 10); int lastDecade = last - (last % 10); From 3a30b3d3792c19d1094da000175db9b221967de3 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Fri, 26 Dec 2025 22:12:53 -0800 Subject: [PATCH 03/24] feat: finishing up album bottom sheet dialog updates for instant mix refactor --- .../AlbumBottomSheetDialog.java | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index a6167eed..5db3add0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -43,7 +43,6 @@ import com.cappielloantonio.tempo.util.ExternalAudioReader; import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel; import com.cappielloantonio.tempo.viewmodel.HomeViewModel; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import com.google.android.material.snackbar.Snackbar; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; @@ -114,33 +113,36 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null); - favoriteToggle.setOnClickListener(v -> { - albumBottomSheetViewModel.setFavorite(requireContext()); - }); + favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext())); TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { - AlbumRepository albumRepository = new AlbumRepository(); - albumRepository.getInstantMix(album, 20, new MediaCallback() { - @Override - public void onError(Exception exception) { - exception.printStackTrace(); + AlbumRepository albumRepository = new AlbumRepository(); + albumRepository.getInstantMix(album, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + exception.printStackTrace(); + } + + @Override + public void onLoadMedia(List media) { + if (!isAdded() || getActivity() == null) { + return; } - @Override - public void onLoadMedia(List media) { - MusicUtil.ratingFilter((ArrayList) media); + MusicUtil.ratingFilter((ArrayList) media); - if (!media.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + if (!media.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).setBottomSheetInPeek(true); } - - dismissBottomSheet(); } - }); - }); + dismissBottomSheet(); + } + }); + }); TextView playRandom = view.findViewById(R.id.play_random_text_view); playRandom.setOnClickListener(v -> { AlbumRepository albumRepository = new AlbumRepository(); @@ -186,18 +188,16 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements }); TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view); - addToPlaylist.setOnClickListener(v -> { - albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { - Bundle bundle = new Bundle(); - bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); + addToPlaylist.setOnClickListener(v -> albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); - PlaylistChooserDialog dialog = new PlaylistChooserDialog(); - dialog.setArguments(bundle); - dialog.show(requireActivity().getSupportFragmentManager(), null); + PlaylistChooserDialog dialog = new PlaylistChooserDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); - dismissBottomSheet(); - }); - }); + dismissBottomSheet(); + })); removeAllTextView = view.findViewById(R.id.remove_all_text_view); albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { From d034171d92889d30c4216953aa5fb0b6d28cd954 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 08:17:16 -0800 Subject: [PATCH 04/24] chore: formatting --- .../com/cappielloantonio/tempo/repository/SongRepository.java | 1 + 1 file changed, 1 insertion(+) 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 4457d37b..dfb0565b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -172,6 +172,7 @@ public class SongRepository { } return new ArrayList<>(); } + public MutableLiveData> getRandomSample(int number, Integer fromYear, Integer toYear) { MutableLiveData> randomSongsSample = new MutableLiveData<>(); App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback() { From f59f572e5ce1f61192ca46bdfb5462a9ccf8f8ca Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 11:04:43 -0800 Subject: [PATCH 05/24] wip: added queue type to for instant mix calls --- app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index c6a4e3a4..1cbf7495 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -133,4 +133,7 @@ object Constants { const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF" const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE" const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL" + enum class SEEDTYPE { + ARTIST, ALBUM, TRACK + } } \ No newline at end of file From 8de9aff1f6fdaddb05fcd8f1e708d69dda67966a Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 12:27:18 -0800 Subject: [PATCH 06/24] wip: refactor song repo instant mix to take in a type --- .../tempo/repository/SongRepository.java | 134 +++++++++++------- .../tempo/service/MediaManager.java | 3 +- .../tempo/viewmodel/HomeViewModel.java | 3 +- .../viewmodel/PlayerBottomSheetViewModel.java | 2 +- .../viewmodel/SongBottomSheetViewModel.java | 3 +- 5 files changed, 93 insertions(+), 52 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 dfb0565b..900a4bee 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -7,6 +7,7 @@ import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; +import com.cappielloantonio.tempo.util.Constants; import java.util.ArrayList; import java.util.Collections; @@ -56,16 +57,26 @@ public class SongRepository { /** * Used by ViewModels. Updates the LiveData list incrementally as songs are found. */ - public MutableLiveData> getInstantMix(String id, int count) { + public MutableLiveData> getInstantMix(String id, Constants.SEEDTYPE type, int count) { MutableLiveData> instantMix = new MutableLiveData<>(new ArrayList<>()); - performSmartMix(id, count, songs -> { + performSmartMix(id, type, count, songs -> { List current = instantMix.getValue(); if (current != null) { for (Child s : songs) { if (!current.contains(s)) current.add(s); } - instantMix.postValue(current); + + if (current.size() < count / 2) { + fillWithRandom(count - current.size(), remainder -> { + for (Child r : remainder) { + if (!current.contains(r)) current.add(r); + } + instantMix.postValue(current); + }); + } else { + instantMix.postValue(current); + } } }); @@ -75,57 +86,79 @@ public class SongRepository { /** * Overloaded method used by other Repositories */ - public void getInstantMix(String id, int count, MediaCallbackInternal callback) { - performSmartMix(id, count, callback); + public void getInstantMix(String id, Constants.SEEDTYPE type, int count, MediaCallbackInternal callback) { + performSmartMix(id, type, count, callback); } - private void performSmartMix(final String id, final int count, final MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs(id, count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = extractSongs(response, "similarSongs"); - if (!songs.isEmpty()) { - callback.onSongsAvailable(songs); - } - if (songs.size() < count / 2) { - fetchContextAndSeed(id, count, callback); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fetchContextAndSeed(id, count, callback); - } - }); + private void performSmartMix(final String id, final Constants.SEEDTYPE type, final int count, final MediaCallbackInternal callback) { + switch (type) { + case ARTIST: + fetchSimilarByArtist(id, count, callback); + break; + case ALBUM: + fetchAlbumSongsThenSimilar(id, count, callback); + break; + case TRACK: + fetchSingleTrackThenSimilar(id, count, callback); + break; + } } - private void fetchContextAndSeed(final String id, final int count, final MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getAlbum(id) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { - List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); - if (albumSongs != null && !albumSongs.isEmpty()) { - callback.onSongsAvailable(new ArrayList<>(albumSongs)); - String seedArtistId = albumSongs.get(0).getArtistId(); - fetchSimilarByArtist(seedArtistId, count, callback); - return; - } - } - fillWithRandom(count, callback); + private void fetchAlbumSongsThenSimilar(String albumId, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { + List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); + if (albumSongs != null && !albumSongs.isEmpty()) { + callback.onSongsAvailable(new ArrayList<>(albumSongs)); + fetchSimilarByArtist(albumSongs.get(0).getArtistId(), count, callback); + return; } + } + fillWithRandom(count, callback); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); + } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fillWithRandom(count, callback); + private void fetchSingleTrackThenSimilar(String trackId, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getSong(trackId).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + Child song = response.body().getSubsonicResponse().getSong(); + if (song != null) { + callback.onSongsAvailable(Collections.singletonList(song)); + fetchSimilarOnly(trackId, count, callback); + return; } - }); + } + fillWithRandom(count, callback); + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); + } + + private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = extractSongs(response, "similarSongs"); + if (!songs.isEmpty()) { + callback.onSongsAvailable(songs); + } else { + fillWithRandom(count, callback); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); } private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { @@ -138,9 +171,14 @@ public class SongRepository { List similar = extractSongs(response, "similarSongs2"); if (!similar.isEmpty()) { callback.onSongsAvailable(similar); + } else { + fillWithRandom(count, callback); } } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } }); } 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 02cbd239..e7b6c6e6 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -26,6 +26,7 @@ import com.cappielloantonio.tempo.repository.SongRepository; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; @@ -442,7 +443,7 @@ public class MediaManager { if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { Preferences.setLastInstantMix(); - LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10); + LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, Constants.SEEDTYPE.TRACK, 10); instantMix.observeForever(new Observer>() { @Override public void onChanged(List media) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java index 0a9892fc..e0f13edd 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -24,6 +24,7 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Preferences; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; @@ -223,7 +224,7 @@ public class HomeViewModel extends AndroidViewModel { public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { mediaInstantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue); + songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, mediaInstantMix::postValue); return mediaInstantMix; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java index df571690..8b631977 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java @@ -278,7 +278,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { instantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue); + songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, instantMix::postValue); return instantMix; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java index de379dcd..6ea4a9ab 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java @@ -21,6 +21,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.NetworkUtil; @@ -128,7 +129,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel { public LiveData> getInstantMix(LifecycleOwner owner, Child media) { instantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue); + songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, instantMix::postValue); return instantMix; } From 844b57054b40629df061f0098c5bca73b8b44a7c Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 12:31:01 -0800 Subject: [PATCH 07/24] wip: updated album repo for song instant mix type update --- .../com/cappielloantonio/tempo/repository/AlbumRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java index 7de91d18..fd47c7cf 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; import java.util.ArrayList; import java.util.Calendar; @@ -208,7 +209,7 @@ public class AlbumRepository { public void getInstantMix(AlbumID3 album, int count, final MediaCallback callback) { Log.d("AlbumRepository", "Starting Instant Mix for album: " + album.getName()); - new SongRepository().getInstantMix(album.getId(), count, songs -> { + new SongRepository().getInstantMix(album.getId(), Constants.SEEDTYPE.ALBUM, count, songs -> { if (songs != null && !songs.isEmpty()) { callback.onLoadMedia(songs); From f22aea7b1dd63aadf0c2c7337ac89bcde8688249 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 17:46:16 -0800 Subject: [PATCH 08/24] wip: changed seedtype constant to camelcase, updated references --- .../tempo/repository/ArtistRepository.java | 23 +++---------------- .../tempo/repository/SongRepository.java | 8 +++---- .../tempo/service/MediaManager.java | 4 ++-- .../tempo/subsonic/models/SimilarSongs.kt | 2 ++ .../cappielloantonio/tempo/util/Constants.kt | 2 +- .../tempo/viewmodel/HomeViewModel.java | 5 ++-- .../viewmodel/PlayerBottomSheetViewModel.java | 3 +-- .../viewmodel/SongBottomSheetViewModel.java | 4 ++-- 8 files changed, 17 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java index 5bea391f..d823958d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java @@ -11,6 +11,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.IndexID3; +import com.cappielloantonio.tempo.util.Constants; import java.util.ArrayList; import java.util.Arrays; @@ -287,26 +288,8 @@ public class ArtistRepository { } public MutableLiveData> getInstantMix(ArtistID3 artist, int count) { - MutableLiveData> instantMix = new MutableLiveData<>(); - - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs2(artist.getId(), count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) { - instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); - - return instantMix; + // Delegate to the centralized SongRepository + return new SongRepository().getInstantMix(artist.getId(), Constants.SEEDTYPE.ARTIST, count); } public MutableLiveData> getRandomSong(ArtistID3 artist, int count) { 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 900a4bee..5d79e283 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -7,7 +7,7 @@ import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import java.util.ArrayList; import java.util.Collections; @@ -57,7 +57,7 @@ public class SongRepository { /** * Used by ViewModels. Updates the LiveData list incrementally as songs are found. */ - public MutableLiveData> getInstantMix(String id, Constants.SEEDTYPE type, int count) { + public MutableLiveData> getInstantMix(String id, SeedType type, int count) { MutableLiveData> instantMix = new MutableLiveData<>(new ArrayList<>()); performSmartMix(id, type, count, songs -> { @@ -86,11 +86,11 @@ public class SongRepository { /** * Overloaded method used by other Repositories */ - public void getInstantMix(String id, Constants.SEEDTYPE type, int count, MediaCallbackInternal callback) { + public void getInstantMix(String id, SeedType type, int count, MediaCallbackInternal callback) { performSmartMix(id, type, count, callback); } - private void performSmartMix(final String id, final Constants.SEEDTYPE type, final int count, final MediaCallbackInternal callback) { + private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) { switch (type) { case ARTIST: fetchSimilarByArtist(id, count, callback); 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 e7b6c6e6..f3139186 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -26,7 +26,7 @@ import com.cappielloantonio.tempo.repository.SongRepository; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; @@ -443,7 +443,7 @@ public class MediaManager { if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { Preferences.setLastInstantMix(); - LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, Constants.SEEDTYPE.TRACK, 10); + LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, SeedType.TRACK, 10); instantMix.observeForever(new Observer>() { @Override public void onChanged(List media) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt index d9bb2053..23a0ffea 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt @@ -1,8 +1,10 @@ package com.cappielloantonio.tempo.subsonic.models import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName @Keep class SimilarSongs { + @SerializedName("song") var songs: List? = null } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index 1cbf7495..2ae3dbb0 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -133,7 +133,7 @@ object Constants { const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF" const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE" const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL" - enum class SEEDTYPE { + enum class SeedType { ARTIST, ALBUM, TRACK } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java index e0f13edd..86d1f456 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -24,7 +24,7 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.subsonic.models.Share; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import com.cappielloantonio.tempo.util.Preferences; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; @@ -35,7 +35,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; -import java.util.stream.Collectors; public class HomeViewModel extends AndroidViewModel { private static final String TAG = "HomeViewModel"; @@ -224,7 +223,7 @@ public class HomeViewModel extends AndroidViewModel { public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { mediaInstantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, mediaInstantMix::postValue); + songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, mediaInstantMix::postValue); return mediaInstantMix; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java index 8b631977..19ced999 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java @@ -13,7 +13,6 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.media3.common.util.UnstableApi; -import androidx.media3.session.MediaBrowser; import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.model.Download; @@ -278,7 +277,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel { public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { instantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, instantMix::postValue); + songRepository.getInstantMix(media.getId(), Constants.SeedType.TRACK, 20).observe(owner, instantMix::postValue); return instantMix; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java index 6ea4a9ab..efa74e90 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java @@ -21,7 +21,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Share; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.NetworkUtil; @@ -129,7 +129,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel { public LiveData> getInstantMix(LifecycleOwner owner, Child media) { instantMix.setValue(Collections.emptyList()); - songRepository.getInstantMix(media.getId(), Constants.SEEDTYPE.TRACK, 20).observe(owner, instantMix::postValue); + songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, instantMix::postValue); return instantMix; } From 17020e519295b021ff3419cfa8b43b2766c7f3bb Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 17:48:06 -0800 Subject: [PATCH 09/24] wip: album tracks working, album bottom sheet only pulling in the album, not quite right --- .../cappielloantonio/tempo/repository/AlbumRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java index fd47c7cf..dd1a6401 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -12,7 +12,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; import com.cappielloantonio.tempo.subsonic.models.Child; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import java.util.ArrayList; import java.util.Calendar; @@ -209,7 +209,7 @@ public class AlbumRepository { public void getInstantMix(AlbumID3 album, int count, final MediaCallback callback) { Log.d("AlbumRepository", "Starting Instant Mix for album: " + album.getName()); - new SongRepository().getInstantMix(album.getId(), Constants.SEEDTYPE.ALBUM, count, songs -> { + new SongRepository().getInstantMix(album.getId(), SeedType.ALBUM, count, songs -> { if (songs != null && !songs.isEmpty()) { callback.onLoadMedia(songs); From 3b3f55c5de837c99e2609e6d4b51c99ea712e01d Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 17:50:53 -0800 Subject: [PATCH 10/24] wip: point artist repo instant mix to song repo --- .../cappielloantonio/tempo/repository/ArtistRepository.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java index d823958d..7cd69c20 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java @@ -11,7 +11,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.IndexID3; -import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Constants.SeedType; import java.util.ArrayList; import java.util.Arrays; @@ -150,7 +150,7 @@ public class ArtistRepository { if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) { for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) { - if(index != null && index.getArtists() != null) { + if(index.getArtists() != null) { artists.addAll(index.getArtists()); } } @@ -289,7 +289,7 @@ public class ArtistRepository { public MutableLiveData> getInstantMix(ArtistID3 artist, int count) { // Delegate to the centralized SongRepository - return new SongRepository().getInstantMix(artist.getId(), Constants.SEEDTYPE.ARTIST, count); + return new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count); } public MutableLiveData> getRandomSong(ArtistID3 artist, int count) { From c1b2ec09a4a4edac824cafcda5e109286c12b9f5 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 17:52:29 -0800 Subject: [PATCH 11/24] wip: radio working from artist page --- .../tempo/ui/fragment/ArtistPageFragment.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index 9fbce6dc..9f904232 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -9,7 +9,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.NonNull; @@ -32,6 +31,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; @@ -203,22 +203,27 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { } }); } + private void initPlayButtons() { - bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> { - if (!songs.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - activity.setBottomSheetInPeek(true); - } else { - Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show(); + bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + artistPageViewModel.getArtistShuffleList().removeObserver(this); + } } })); - bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> { - if (songs != null && !songs.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - activity.setBottomSheetInPeek(true); - } else { - Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_radio), Toast.LENGTH_SHORT).show(); + bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + artistPageViewModel.getArtistInstantMix().removeObserver(this); + } } })); } From 8140e80d611bac8caacc3a8c826d46b771844945 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 27 Dec 2025 19:05:14 -0800 Subject: [PATCH 12/24] wip: artist logic squared away, seems to be working as expected, mostly. still need more testing --- .../tempo/ui/fragment/ArtistPageFragment.java | 5 +-- .../ArtistBottomSheetDialog.java | 35 +++++++++---------- .../tempo/viewmodel/ArtistPageViewModel.java | 1 - 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index 9f904232..0c850038 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -14,6 +14,7 @@ import android.widget.ToggleButton; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaBrowser; @@ -205,7 +206,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { } private void initPlayButtons() { - bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(List songs) { if (songs != null && !songs.isEmpty()) { @@ -216,7 +217,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { } })); - bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(List songs) { if (songs != null && !songs.isEmpty()) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index 9ec9b549..e890c10e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -7,10 +7,10 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.Nullable; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaBrowser; @@ -22,6 +22,7 @@ import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.MusicUtil; @@ -29,6 +30,8 @@ import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; + @UnstableApi public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { private static final String TAG = "AlbumBottomSheetDialog"; @@ -87,20 +90,20 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { ArtistRepository artistRepository = new ArtistRepository(); - - artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> { - // navidrome may return null for this - if (songs == null) - return; - MusicUtil.ratingFilter(songs); - - if (!songs.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + Observer> observer = new Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty() && isAdded()) { + MusicUtil.ratingFilter(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + artistRepository.getInstantMix(artist, 20).removeObserver(this); + dismissBottomSheet(); + } } + }; - dismissBottomSheet(); - }); + artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), observer); }); TextView playRandom = view.findViewById(R.id.play_random_text_view); @@ -108,16 +111,10 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement ArtistRepository artistRepository = new ArtistRepository(); artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> { MusicUtil.ratingFilter(songs); - if (!songs.isEmpty()) { MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); ((MainActivity) requireActivity()).setBottomSheetInPeek(true); - - dismissBottomSheet(); - } else { - Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show(); } - dismissBottomSheet(); }); }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java index 871565d0..ab6cc609 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java @@ -128,7 +128,6 @@ public class ArtistPageViewModel extends AndroidViewModel { MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()) ); - } else { } } }); From 10673a49d48bbe7a8f8e7957cfefb6d13f9426a6 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 28 Dec 2025 08:20:58 -0800 Subject: [PATCH 13/24] chore: bump version, bringing in dev changes to test --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 43b139e3..036ceed6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 11 - versionName '4.6.1' + versionName '4.6.2' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { From 8c5390bfef82dc65341e2f816bfea3bffdbeb556 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Mon, 29 Dec 2025 16:36:42 -0800 Subject: [PATCH 14/24] wip: more instant mix refactor to be able to accumulate tracks --- .../tempo/repository/SongRepository.java | 100 ++++++++++++++---- 1 file changed, 78 insertions(+), 22 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 5d79e283..a3b14031 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -87,7 +87,45 @@ public class SongRepository { * Overloaded method used by other Repositories */ public void getInstantMix(String id, SeedType type, int count, MediaCallbackInternal callback) { - performSmartMix(id, type, count, callback); + new MediaCallbackAccumulator(callback, count).start(id, type); + } + + private class MediaCallbackAccumulator { + private final MediaCallbackInternal originalCallback; + private final int targetCount; + private final List accumulatedSongs = new ArrayList<>(); + private boolean isComplete = false; + + MediaCallbackAccumulator(MediaCallbackInternal callback, int count) { + this.originalCallback = callback; + this.targetCount = count; + } + + void start(String id, SeedType type) { + performSmartMix(id, type, targetCount, this::onBatchReceived); + } + + private void onBatchReceived(List batch) { + if (isComplete || batch == null || batch.isEmpty()) { + return; + } + + int added = 0; + for (Child s : batch) { + if (!accumulatedSongs.contains(s) && accumulatedSongs.size() < targetCount) { + accumulatedSongs.add(s); + added++; + } + } + + if (added > 0) { + originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs)); + if (accumulatedSongs.size() >= targetCount) { + isComplete = true; + android.util.Log.d("SongRepository", "Reached target of " + targetCount + " songs"); + } + } + } } private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) { @@ -111,8 +149,14 @@ public class SongRepository { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); if (albumSongs != null && !albumSongs.isEmpty()) { - callback.onSongsAvailable(new ArrayList<>(albumSongs)); - fetchSimilarByArtist(albumSongs.get(0).getArtistId(), count, callback); + List limitedAlbumSongs = albumSongs.subList(0, Math.min(count, albumSongs.size())); + callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs)); + + // If we need more, get similar songs + int remaining = count - limitedAlbumSongs.size(); + if (remaining > 0) { + fetchSimilarByArtist(albumSongs.get(0).getArtistId(), remaining, callback); + } return; } } @@ -132,7 +176,10 @@ public class SongRepository { Child song = response.body().getSubsonicResponse().getSong(); if (song != null) { callback.onSongsAvailable(Collections.singletonList(song)); - fetchSimilarOnly(trackId, count, callback); + int remaining = count - 1; + if (remaining > 0) { + fetchSimilarOnly(trackId, remaining, callback); + } return; } } @@ -144,23 +191,23 @@ public class SongRepository { }); } - private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = extractSongs(response, "similarSongs"); - if (!songs.isEmpty()) { - callback.onSongsAvailable(songs); - } else { - fillWithRandom(count, callback); - } - } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { +private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = extractSongs(response, "similarSongs"); + if (!songs.isEmpty()) { + List limitedSongs = songs.subList(0, Math.min(count, songs.size())); + callback.onSongsAvailable(limitedSongs); + } else { fillWithRandom(count, callback); } - }); - } - + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + fillWithRandom(count, callback); + } + }); +} private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { App.getSubsonicClientInstance(false) .getBrowsingClient() @@ -170,7 +217,8 @@ public class SongRepository { public void onResponse(@NonNull Call call, @NonNull Response response) { List similar = extractSongs(response, "similarSongs2"); if (!similar.isEmpty()) { - callback.onSongsAvailable(similar); + List limitedSimilar = similar.subList(0, Math.min(count, similar.size())); + callback.onSongsAvailable(limitedSimilar); } else { fillWithRandom(count, callback); } @@ -189,9 +237,17 @@ public class SongRepository { .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - callback.onSongsAvailable(extractSongs(response, "randomSongs")); + List random = extractSongs(response, "randomSongs"); + if (!random.isEmpty()) { + List limitedRandom = random.subList(0, Math.min(target, random.size())); + callback.onSongsAvailable(limitedRandom); + } else { + callback.onSongsAvailable(new ArrayList<>()); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onSongsAvailable(new ArrayList<>()); } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) {} }); } From f39891dd2cfa5c883ce78f86fa83bf780bfffc2a Mon Sep 17 00:00:00 2001 From: eddyizm Date: Mon, 29 Dec 2025 16:37:20 -0800 Subject: [PATCH 15/24] wip: added logging to media manager to track down bug in bottom sheet dialogs --- .../tempo/service/MediaManager.java | 22 ++++++- .../AlbumBottomSheetDialog.java | 58 ++++++++++++------- .../ArtistBottomSheetDialog.java | 48 +++++++++++---- 3 files changed, 91 insertions(+), 37 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 f3139186..12ae3e85 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -184,41 +184,57 @@ public class MediaManager { @OptIn(markerClass = UnstableApi.class) public static void startQueue(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex) { if (mediaBrowserListenableFuture != null) { + Log.d(TAG, "startQueue called with " + (media != null ? media.size() : 0) + " songs"); + mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { + Log.d(TAG, "MediaBrowser future is done"); final MediaBrowser browser = mediaBrowserListenableFuture.get(); + Log.d(TAG, "Got MediaBrowser, connected: " + browser.isConnected()); + final List items = MappingUtil.mapMediaItems(media); + Log.d(TAG, "Mapped " + items.size() + " media items"); + new Handler(Looper.getMainLooper()).post(() -> { + Log.d(TAG, "Setting " + items.size() + " media items at index " + startIndex); justStarted.set(true); browser.setMediaItems(items, startIndex, 0); browser.prepare(); + Log.d(TAG, "setMediaItems and prepare called"); Player.Listener timelineListener = new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { + Log.d(TAG, "onTimelineChanged: itemCount=" + browser.getMediaItemCount() + ", reason=" + reason); + int itemCount = browser.getMediaItemCount(); if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { + Log.d(TAG, "Seeking to " + startIndex + " and playing"); browser.seekTo(startIndex, 0); browser.play(); browser.removeListener(this); + Log.d(TAG, "Playback started"); + } else { + Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex); } } }; + + Log.d(TAG, "Adding timeline listener"); browser.addListener(timelineListener); }); backgroundExecutor.execute(() -> { + Log.d(TAG, "Background: enqueuing to database"); enqueueDatabase(media, true, 0); }); } } catch (ExecutionException | InterruptedException e) { - Log.e(TAG, "Error executing startQueue logic: " + e.getMessage(), e); + Log.e(TAG, "Error in startQueue: " + e.getMessage(), e); } }, MoreExecutors.directExecutor()); } - - } public static void startQueue(ListenableFuture mediaBrowserListenableFuture, Child media) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 5db3add0..072fa391 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -116,33 +116,47 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext())); TextView playRadio = view.findViewById(R.id.play_radio_text_view); - playRadio.setOnClickListener(v -> { - AlbumRepository albumRepository = new AlbumRepository(); - albumRepository.getInstantMix(album, 20, new MediaCallback() { - @Override - public void onError(Exception exception) { - exception.printStackTrace(); - } - - @Override - public void onLoadMedia(List media) { - if (!isAdded() || getActivity() == null) { - return; + playRadio.setOnClickListener(v -> { + AlbumRepository albumRepository = new AlbumRepository(); + albumRepository.getInstantMix(album, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + exception.printStackTrace(); } - MusicUtil.ratingFilter((ArrayList) media); - - if (!media.isEmpty()) { - MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); - if (getActivity() instanceof MainActivity) { - ((MainActivity) getActivity()).setBottomSheetInPeek(true); + @Override + public void onLoadMedia(List media) { + if (!isAdded() || getActivity() == null) { + return; } - } - dismissBottomSheet(); - } + MusicUtil.ratingFilter((ArrayList) media); + + if (!media.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).setBottomSheetInPeek(true); + } + } + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + // Ignore + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } + }); }); - }); + TextView playRandom = view.findViewById(R.id.play_random_text_view); playRandom.setOnClickListener(v -> { AlbumRepository albumRepository = new AlbumRepository(); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index e890c10e..2f48e4dc 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -2,11 +2,13 @@ package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; import android.content.ComponentName; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.Nullable; @@ -89,21 +91,43 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { + Log.d(TAG, "Artist instant mix clicked"); + ArtistRepository artistRepository = new ArtistRepository(); - Observer> observer = new Observer>() { - @Override - public void onChanged(List songs) { - if (songs != null && !songs.isEmpty() && isAdded()) { - MusicUtil.ratingFilter(songs); - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); - artistRepository.getInstantMix(artist, 20).removeObserver(this); + artistRepository.getInstantMix(artist, 20) + .observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty()) { + Log.d(TAG, "Starting queue with " + songs.size() + " songs"); + MusicUtil.ratingFilter(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + artistRepository.getInstantMix(artist, 20) + .removeObserver(this); + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + // Ignore + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } else { + // No songs at all - all attempts failed + Toast.makeText(requireContext(), + "Could not load songs. Please check your connection.", + Toast.LENGTH_SHORT).show(); dismissBottomSheet(); } - } - }; - - artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), observer); + } + }); }); TextView playRandom = view.findViewById(R.id.play_random_text_view); From a2401302edc0687270f69e42d640ac7a53bece63 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Tue, 30 Dec 2025 21:46:07 -0800 Subject: [PATCH 16/24] wip: beta build --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 036ceed6..d1418559 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 11 - versionName '4.6.2' + versionName '4.6.2.BETA' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { From 193447d07e5ebd84764bb3e9a0cd6a39faaf0b08 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Wed, 31 Dec 2025 08:20:03 -0800 Subject: [PATCH 17/24] fix: used set to address duplicates and removed toast that was firing to early --- .../tempo/repository/SongRepository.java | 4 ++++ .../bottomsheetdialog/ArtistBottomSheetDialog.java | 10 +--------- 2 files changed, 5 insertions(+), 9 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 a3b14031..0c82e022 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -11,8 +11,10 @@ import com.cappielloantonio.tempo.util.Constants.SeedType; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import retrofit2.Call; import retrofit2.Callback; @@ -94,6 +96,7 @@ public class SongRepository { private final MediaCallbackInternal originalCallback; private final int targetCount; private final List accumulatedSongs = new ArrayList<>(); + private final Set trackIds = new HashSet<>(); private boolean isComplete = false; MediaCallbackAccumulator(MediaCallbackInternal callback, int count) { @@ -113,6 +116,7 @@ public class SongRepository { int added = 0; for (Child s : batch) { if (!accumulatedSongs.contains(s) && accumulatedSongs.size() < targetCount) { + trackIds.add(s.getId()); accumulatedSongs.add(s); added++; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index 2f48e4dc..b6e88c1a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -8,11 +8,9 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.Nullable; -import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaBrowser; @@ -119,13 +117,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement } view.postDelayed(() -> dismissBottomSheet(), 200); }, 300); - } else { - // No songs at all - all attempts failed - Toast.makeText(requireContext(), - "Could not load songs. Please check your connection.", - Toast.LENGTH_SHORT).show(); - dismissBottomSheet(); - } + } } }); }); From d04ed8d430a819feca4832e3e465ad41870de815 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Thu, 1 Jan 2026 11:50:48 -0800 Subject: [PATCH 18/24] fix: address duplicate track bug, wrong order in queue, and updated album instant mix --- .../tempo/repository/SongRepository.java | 101 ++++++++++-------- .../tempo/service/MediaManager.java | 13 +-- .../ui/fragment/HomeTabMusicFragment.java | 23 ++-- .../AlbumBottomSheetDialog.java | 34 +++--- .../ArtistBottomSheetDialog.java | 2 +- .../SongBottomSheetDialog.java | 37 +++++-- 6 files changed, 120 insertions(+), 90 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 0c82e022..d457c44a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -1,5 +1,7 @@ package com.cappielloantonio.tempo.repository; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; @@ -22,6 +24,7 @@ import retrofit2.Response; public class SongRepository { + private static final String TAG = "SongRepository"; public interface MediaCallbackInternal { void onSongsAvailable(List songs); } @@ -126,7 +129,6 @@ public class SongRepository { originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs)); if (accumulatedSongs.size() >= targetCount) { isComplete = true; - android.util.Log.d("SongRepository", "Reached target of " + targetCount + " songs"); } } } @@ -150,28 +152,61 @@ public class SongRepository { App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { + if (response.isSuccessful() && response.body() != null && + response.body().getSubsonicResponse().getAlbum() != null) { List albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs(); if (albumSongs != null && !albumSongs.isEmpty()) { - List limitedAlbumSongs = albumSongs.subList(0, Math.min(count, albumSongs.size())); + int fromAlbum = Math.min(count, albumSongs.size()); + List limitedAlbumSongs = albumSongs.subList(0, fromAlbum); callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs)); - - // If we need more, get similar songs - int remaining = count - limitedAlbumSongs.size(); - if (remaining > 0) { + + 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 public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + @Override + public void onFailure(@NonNull Call 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() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List similar = extractSongs(response, "similarSongs2"); + if (!similar.isEmpty()) { + List 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 call, @NonNull Throwable t) { + Log.d(TAG, "getSimilarSongs2 failed: " + t.getMessage()); + fillWithRandom(count, callback); + } + }); + } + private void fetchSingleTrackThenSimilar(String trackId, int count, MediaCallbackInternal callback) { App.getSubsonicClientInstance(false).getBrowsingClient().getSong(trackId).enqueue(new Callback() { @Override @@ -195,45 +230,25 @@ public class SongRepository { }); } -private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List songs = extractSongs(response, "similarSongs"); - if (!songs.isEmpty()) { - List limitedSongs = songs.subList(0, Math.min(count, songs.size())); - callback.onSongsAvailable(limitedSongs); - } else { + private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) { + App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = extractSongs(response, "similarSongs"); + if (!songs.isEmpty()) { + List limitedSongs = songs.subList(0, Math.min(count, songs.size())); + callback.onSongsAvailable(limitedSongs); + } else { + fillWithRandom(count, callback); + } + } + @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { fillWithRandom(count, callback); } - } - @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fillWithRandom(count, callback); - } - }); -} - private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) { - App.getSubsonicClientInstance(false) - .getBrowsingClient() - .getSimilarSongs2(artistId, count) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - List similar = extractSongs(response, "similarSongs2"); - if (!similar.isEmpty()) { - List limitedSimilar = similar.subList(0, Math.min(count, similar.size())); - callback.onSongsAvailable(limitedSimilar); - } else { - fillWithRandom(count, callback); - } - } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - fillWithRandom(count, callback); - } - }); + }); } + private void fillWithRandom(int target, final MediaCallbackInternal callback) { App.getSubsonicClientInstance(false) .getAlbumSongListClient() 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 12ae3e85..ab591104 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -184,44 +184,33 @@ public class MediaManager { @OptIn(markerClass = UnstableApi.class) public static void startQueue(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex) { if (mediaBrowserListenableFuture != null) { - Log.d(TAG, "startQueue called with " + (media != null ? media.size() : 0) + " songs"); - + mediaBrowserListenableFuture.addListener(() -> { try { if (mediaBrowserListenableFuture.isDone()) { - Log.d(TAG, "MediaBrowser future is done"); final MediaBrowser browser = mediaBrowserListenableFuture.get(); - Log.d(TAG, "Got MediaBrowser, connected: " + browser.isConnected()); - final List items = MappingUtil.mapMediaItems(media); - Log.d(TAG, "Mapped " + items.size() + " media items"); new Handler(Looper.getMainLooper()).post(() -> { - Log.d(TAG, "Setting " + items.size() + " media items at index " + startIndex); justStarted.set(true); browser.setMediaItems(items, startIndex, 0); browser.prepare(); - Log.d(TAG, "setMediaItems and prepare called"); Player.Listener timelineListener = new Player.Listener() { @Override public void onTimelineChanged(Timeline timeline, int reason) { - Log.d(TAG, "onTimelineChanged: itemCount=" + browser.getMediaItemCount() + ", reason=" + reason); int itemCount = browser.getMediaItemCount(); if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { - Log.d(TAG, "Seeking to " + startIndex + " and playing"); browser.seekTo(startIndex, 0); browser.play(); browser.removeListener(this); - Log.d(TAG, "Playback started"); } else { Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex); } } }; - Log.d(TAG, "Adding timeline listener"); browser.addListener(timelineListener); }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java index 191c520d..d7f739ae 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java @@ -5,6 +5,8 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -1253,20 +1255,25 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback { MediaBrowser.releaseFuture(mediaBrowserListenableFuture); } - @Override public void onMediaClick(Bundle bundle) { if (bundle.containsKey(Constants.MEDIA_MIX)) { - MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelable(Constants.TRACK_OBJECT)); + Child track = bundle.getParcelable(Constants.TRACK_OBJECT); activity.setBottomSheetInPeek(true); if (mediaBrowserListenableFuture != null) { - homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> { - MusicUtil.ratingFilter(songs); + final boolean[] playbackStarted = {false}; - if (songs != null && !songs.isEmpty()) { - MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); - } - }); + homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), track) + .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)) { List media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 072fa391..f1e4c89a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -5,6 +5,8 @@ import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.os.Bundle; +import android.os.Handler; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -61,6 +63,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements private List currentAlbumMediaItems = Collections.emptyList(); private ListenableFuture mediaBrowserListenableFuture; + private static final String TAG = "AlbumBottomSheetDialog"; @Nullable @Override @@ -116,12 +119,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext())); TextView playRadio = view.findViewById(R.id.play_radio_text_view); - playRadio.setOnClickListener(v -> { - AlbumRepository albumRepository = new AlbumRepository(); - albumRepository.getInstantMix(album, 20, new MediaCallback() { + playRadio.setOnClickListener(v -> { + new AlbumRepository().getInstantMix(album, 20, new MediaCallback() { @Override public void onError(Exception exception) { - exception.printStackTrace(); + Log.e(TAG, "Error: " + exception.getMessage()); } @Override @@ -140,19 +142,19 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements } view.postDelayed(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (browser != null && browser.isPlaying()) { - dismissBottomSheet(); - return; - } - } - } catch (Exception e) { - // Ignore + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; } - view.postDelayed(() -> dismissBottomSheet(), 200); - }, 300); + } + } catch (Exception e) { + Log.e(TAG, "Error checking playback: " + e.getMessage()); + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); } }); }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index b6e88c1a..be69b76a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -89,7 +89,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { - Log.d(TAG, "Artist instant mix clicked"); + Log.d(TAG, "Artist instant mix clicked"); ArtistRepository artistRepository = new ArtistRepository(); artistRepository.getInstantMix(artist, 20) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java index 39ba4394..271f00cc 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -5,6 +5,8 @@ import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -143,21 +145,36 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { - MediaManager.startQueue(mediaBrowserListenableFuture, song); ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + final boolean[] playbackStarted = {false}; + songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> { - MusicUtil.ratingFilter(songs); + if (playbackStarted[0] || songs == null || songs.isEmpty()) return; - if (songs == null) { - dismissBottomSheet(); - return; - } + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (playbackStarted[0]) return; - if (!songs.isEmpty()) { - MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); - dismissBottomSheet(); - } + MusicUtil.ratingFilter(songs); + + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + playbackStarted[0] = true; + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + // Ignore + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + }, 300); }); }); From 05785979e3a31dd7d81d6767f54d40cdd29e0eb4 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 3 Jan 2026 08:17:53 -0800 Subject: [PATCH 19/24] fix: cleans up duplicates --- .../tempo/repository/SongRepository.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 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 d457c44a..25990236 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -117,19 +117,17 @@ public class SongRepository { } int added = 0; - for (Child s : batch) { - if (!accumulatedSongs.contains(s) && accumulatedSongs.size() < targetCount) { - trackIds.add(s.getId()); - accumulatedSongs.add(s); + for (Child song : batch) { + if (!trackIds.contains(song.getId()) && accumulatedSongs.size() < targetCount) { + trackIds.add(song.getId()); + accumulatedSongs.add(song); added++; } } - if (added > 0) { + if (accumulatedSongs.size() >= targetCount) { originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs)); - if (accumulatedSongs.size() >= targetCount) { - isComplete = true; - } + isComplete = true; } } } @@ -194,14 +192,12 @@ public class SongRepository { List 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 call, @NonNull Throwable t) { - Log.d(TAG, "getSimilarSongs2 failed: " + t.getMessage()); fillWithRandom(count, callback); } }); From 99c31f431857bdb846d8f005022717d79d973654 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 3 Jan 2026 08:18:29 -0800 Subject: [PATCH 20/24] wip: bumps debounce time --- .../ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index f1e4c89a..098d74a6 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -154,7 +154,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements Log.e(TAG, "Error checking playback: " + e.getMessage()); } view.postDelayed(() -> dismissBottomSheet(), 200); - }, 300); + }, 1200); } }); }); From a2801f316834075e01d9de5e20bc7c26ce83ea66 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 4 Jan 2026 07:53:07 -0800 Subject: [PATCH 21/24] fix: reduced debounce, added toast --- .../fragment/bottomsheetdialog/AlbumBottomSheetDialog.java | 5 +++-- .../fragment/bottomsheetdialog/ArtistBottomSheetDialog.java | 5 +++-- app/src/main/res/values/strings.xml | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 098d74a6..563b1ab3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -120,6 +120,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { + Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); new AlbumRepository().getInstantMix(album, 20, new MediaCallback() { @Override public void onError(Exception exception) { @@ -153,8 +154,8 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements } catch (Exception e) { Log.e(TAG, "Error checking playback: " + e.getMessage()); } - view.postDelayed(() -> dismissBottomSheet(), 200); - }, 1200); + view.postDelayed(() -> dismissBottomSheet(), 300); + }, 300); } }); }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index be69b76a..4d50565a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -8,6 +8,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import android.widget.ToggleButton; import androidx.annotation.Nullable; @@ -90,7 +91,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { Log.d(TAG, "Artist instant mix clicked"); - + Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_LONG).show(); ArtistRepository artistRepository = new ArtistRepository(); artistRepository.getInstantMix(artist, 20) .observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { @@ -115,7 +116,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement } catch (Exception e) { // Ignore } - view.postDelayed(() -> dismissBottomSheet(), 200); + view.postDelayed(() -> dismissBottomSheet(), 300); }, 300); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee8eebe5..75573707 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Ignore Don\'t ask again Disable + Generating instant mix... Cancel Enable data saver OK From 993374e56c314e0f5eb49f43e93f0d58b75fe30d Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 4 Jan 2026 09:27:53 -0800 Subject: [PATCH 22/24] fix: adde a scheduled delay to allow callbacks to succeed --- .../tempo/repository/ArtistRepository.java | 13 +++ .../AlbumBottomSheetDialog.java | 55 +++++++---- .../ArtistBottomSheetDialog.java | 96 +++++++++++++------ .../viewmodel/AlbumBottomSheetViewModel.java | 4 +- 4 files changed, 122 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java index 7cd69c20..cc47e676 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData; import android.util.Log; import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; @@ -292,6 +293,18 @@ public class ArtistRepository { return new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count); } + public void getInstantMix(ArtistID3 artist, int count, final MediaCallback callback) { + // Delegate to the centralized SongRepository + new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count, songs -> { + if (songs != null && !songs.isEmpty()) { + callback.onLoadMedia(songs); + } else { + callback.onLoadMedia(Collections.emptyList()); + } + }); + } + + public MutableLiveData> getRandomSong(ArtistID3 artist, int count) { MutableLiveData> randomSongs = new MutableLiveData<>(); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 563b1ab3..06db820b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -5,7 +5,6 @@ import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.os.Bundle; -import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -62,6 +61,9 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements private List currentAlbumTracks = Collections.emptyList(); private List currentAlbumMediaItems = Collections.emptyList(); + private boolean playbackStarted = false; + private boolean dismissalScheduled = false; + private ListenableFuture mediaBrowserListenableFuture; private static final String TAG = "AlbumBottomSheetDialog"; @@ -120,11 +122,16 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { + playbackStarted = false; + dismissalScheduled = false; Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); new AlbumRepository().getInstantMix(album, 20, new MediaCallback() { @Override public void onError(Exception exception) { Log.e(TAG, "Error: " + exception.getMessage()); + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } } @Override @@ -136,30 +143,26 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements MusicUtil.ratingFilter((ArrayList) media); if (!media.isEmpty()) { + boolean isFirstBatch = !playbackStarted; MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + playbackStarted = true; + if (getActivity() instanceof MainActivity) { ((MainActivity) getActivity()).setBottomSheetInPeek(true); } - } - - view.postDelayed(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (browser != null && browser.isPlaying()) { - dismissBottomSheet(); - return; - } - } - } catch (Exception e) { - Log.e(TAG, "Error checking playback: " + e.getMessage()); + if (isFirstBatch && !dismissalScheduled) { + scheduleDelayedDismissal(v); } - view.postDelayed(() -> dismissBottomSheet(), 300); - }, 300); + } else { + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } } }); }); + TextView playRandom = view.findViewById(R.id.play_random_text_view); playRandom.setOnClickListener(v -> { AlbumRepository albumRepository = new AlbumRepository(); @@ -308,4 +311,24 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements private void refreshShares() { homeViewModel.refreshShares(requireActivity()); } + + private void scheduleDelayedDismissal(View view) { + if (dismissalScheduled) return; + dismissalScheduled = true; + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + Log.e(TAG, "Error checking playback: " + e.getMessage()); + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index 4d50565a..91b2cf90 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -19,6 +19,7 @@ import androidx.media3.session.SessionToken; import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.repository.ArtistRepository; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; @@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.common.util.concurrent.ListenableFuture; +import java.util.ArrayList; import java.util.List; @UnstableApi @@ -42,6 +44,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement private ListenableFuture mediaBrowserListenableFuture; + private boolean playbackStarted = false; + private boolean dismissalScheduled = false; + @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -91,36 +96,48 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { Log.d(TAG, "Artist instant mix clicked"); - Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_LONG).show(); - ArtistRepository artistRepository = new ArtistRepository(); - artistRepository.getInstantMix(artist, 20) - .observe(getViewLifecycleOwner(), new androidx.lifecycle.Observer>() { - @Override - public void onChanged(List songs) { - if (songs != null && !songs.isEmpty()) { - Log.d(TAG, "Starting queue with " + songs.size() + " songs"); - MusicUtil.ratingFilter(songs); - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); - artistRepository.getInstantMix(artist, 20) - .removeObserver(this); - view.postDelayed(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (browser != null && browser.isPlaying()) { - dismissBottomSheet(); - return; - } - } - } catch (Exception e) { - // Ignore - } - view.postDelayed(() -> dismissBottomSheet(), 300); - }, 300); + Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); + playbackStarted = false; + dismissalScheduled = false; + + new ArtistRepository().getInstantMix(artist, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + Log.e(TAG, "Error: " + exception.getMessage()); + + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } + + @Override + public void onLoadMedia(List media) { + if (!isAdded() || getActivity() == null) { + return; + } + + Log.d(TAG, "Received " + media.size() + " songs for artist"); + + MusicUtil.ratingFilter((ArrayList) media); + + if (!media.isEmpty()) { + boolean isFirstBatch = !playbackStarted; + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + playbackStarted = true; + + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).setBottomSheetInPeek(true); + } + if (isFirstBatch && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } else { + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); } } - }); + } + }); }); TextView playRandom = view.findViewById(R.id.play_random_text_view); @@ -153,4 +170,25 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement private void releaseMediaBrowser() { MediaBrowser.releaseFuture(mediaBrowserListenableFuture); } -} \ No newline at end of file + + private void scheduleDelayedDismissal(View view) { + if (dismissalScheduled) return; + dismissalScheduled = true; + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + Log.e(TAG, "Error checking playback: " + e.getMessage()); + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } + +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java index da1ec831..07b17a48 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java @@ -4,10 +4,12 @@ import android.app.Application; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.OptIn; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; +import androidx.media3.common.util.UnstableApi; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.interfaces.StarCallback; @@ -33,7 +35,6 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel { private final ArtistRepository artistRepository; private final FavoriteRepository favoriteRepository; private final SharingRepository sharingRepository; - private AlbumID3 album; public AlbumBottomSheetViewModel(@NonNull Application application) { @@ -116,6 +117,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel { MutableLiveData> tracksLiveData = albumRepository.getAlbumTracks(album.getId()); tracksLiveData.observeForever(new Observer>() { + @OptIn(markerClass = UnstableApi.class) @Override public void onChanged(List songs) { if (songs != null && !songs.isEmpty()) { From 6110a9c8e7ef9fdca17f0fdac439db6b92f4e5c1 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 4 Jan 2026 10:05:16 -0800 Subject: [PATCH 23/24] fix: added a timeout for the callbacks to dismiss dialog and notify the user --- app/build.gradle | 2 +- .../AlbumBottomSheetDialog.java | 30 +++++++++++++++++++ .../ArtistBottomSheetDialog.java | 29 +++++++++++++++++- app/src/main/res/values/strings.xml | 1 + 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d1418559..036ceed6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 11 - versionName '4.6.2.BETA' + versionName '4.6.2' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index 06db820b..ce4a8e08 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -125,10 +125,29 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements playbackStarted = false; dismissalScheduled = false; Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); + final Runnable failsafeTimeout = () -> { + if (!playbackStarted && !dismissalScheduled) { + Log.w(TAG, "No response received within 3 seconds"); + if (isAdded() && getActivity() != null) { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + } + } + }; + view.postDelayed(failsafeTimeout, 3000); + new AlbumRepository().getInstantMix(album, 20, new MediaCallback() { @Override public void onError(Exception exception) { + view.removeCallbacks(failsafeTimeout); Log.e(TAG, "Error: " + exception.getMessage()); + if (isAdded() && getActivity() != null) { + String message = isOffline(exception) ? + "You're offline" : "Network error"; + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } if (!playbackStarted && !dismissalScheduled) { scheduleDelayedDismissal(v); } @@ -136,6 +155,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements @Override public void onLoadMedia(List media) { + view.removeCallbacks(failsafeTimeout); if (!isAdded() || getActivity() == null) { return; } @@ -154,6 +174,9 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements scheduleDelayedDismissal(v); } } else { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); if (!playbackStarted && !dismissalScheduled) { scheduleDelayedDismissal(v); } @@ -331,4 +354,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements view.postDelayed(() -> dismissBottomSheet(), 200); }, 300); } + + private boolean isOffline(Exception exception) { + return exception != null && exception.getMessage() != null && + (exception.getMessage().contains("Network") || + exception.getMessage().contains("timeout") || + exception.getMessage().contains("offline")); + } } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java index 91b2cf90..3f6f1a5e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -99,12 +99,29 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); playbackStarted = false; dismissalScheduled = false; + final Runnable failsafeTimeout = () -> { + if (!playbackStarted && !dismissalScheduled) { + Log.w(TAG, "No response received within 3 seconds"); + if (isAdded() && getActivity() != null) { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + } + } + }; + view.postDelayed(failsafeTimeout, 3000); new ArtistRepository().getInstantMix(artist, 20, new MediaCallback() { @Override public void onError(Exception exception) { + view.removeCallbacks(failsafeTimeout); Log.e(TAG, "Error: " + exception.getMessage()); - + if (isAdded() && getActivity() != null) { + String message = isOffline(exception) ? + "You're offline" : "Network error"; + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } if (!playbackStarted && !dismissalScheduled) { scheduleDelayedDismissal(v); } @@ -112,6 +129,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement @Override public void onLoadMedia(List media) { + view.removeCallbacks(failsafeTimeout); if (!isAdded() || getActivity() == null) { return; } @@ -132,6 +150,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement scheduleDelayedDismissal(v); } } else { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); if (!playbackStarted && !dismissalScheduled) { scheduleDelayedDismissal(v); } @@ -191,4 +212,10 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement }, 300); } + private boolean isOffline(Exception exception) { + return exception != null && exception.getMessage() != null && + (exception.getMessage().contains("Network") || + exception.getMessage().contains("timeout") || + exception.getMessage().contains("offline")); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75573707..0ae9be02 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,6 +52,7 @@ Don\'t ask again Disable Generating instant mix... + Could not retrieve tracks from subsonic server. Cancel Enable data saver OK From 431014adc4b4898864e8d0e71f6c4bba64c40a37 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 4 Jan 2026 11:31:53 -0800 Subject: [PATCH 24/24] fix: updated song bottom sheet to match album/artist bottom sheets --- .../SongBottomSheetDialog.java | 115 ++++++++++++++---- .../viewmodel/SongBottomSheetViewModel.java | 12 ++ 2 files changed, 102 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java index 271f00cc..27f15b3d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -7,6 +7,7 @@ import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,6 +26,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; @@ -52,6 +54,7 @@ import com.cappielloantonio.tempo.util.ExternalAudioWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.List; @UnstableApi public class SongBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { @@ -69,7 +72,11 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements private AssetLinkUtil.AssetLink currentAlbumLink; private AssetLinkUtil.AssetLink currentArtistLink; + private boolean playbackStarted = false; + private boolean dismissalScheduled = false; + private ListenableFuture mediaBrowserListenableFuture; + private static final String TAG = "SongBottomSheetDialog"; @Nullable @Override @@ -145,37 +152,68 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements TextView playRadio = view.findViewById(R.id.play_radio_text_view); playRadio.setOnClickListener(v -> { - ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + playbackStarted = false; + dismissalScheduled = false; + Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show(); - final boolean[] playbackStarted = {false}; + final Runnable failsafeTimeout = () -> { + if (!playbackStarted && !dismissalScheduled) { + Log.w(TAG, "No response received within 3 seconds"); + if (isAdded() && getActivity() != null) { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + } + } + }; + view.postDelayed(failsafeTimeout, 3000); + songBottomSheetViewModel.getInstantMix(song, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + view.removeCallbacks(failsafeTimeout); + Log.e(TAG, "Error: " + exception.getMessage()); + if (isAdded() && getActivity() != null) { + String message = isOffline(exception) ? + "You're offline" : "Network error"; + Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); + } + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } - songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> { - if (playbackStarted[0] || songs == null || songs.isEmpty()) return; + @Override + public void onLoadMedia(List media) { + view.removeCallbacks(failsafeTimeout); + if (!isAdded() || getActivity() == null) { + return; + } - new Handler(Looper.getMainLooper()).postDelayed(() -> { - if (playbackStarted[0]) return; + MusicUtil.ratingFilter((ArrayList) media); - MusicUtil.ratingFilter(songs); - - MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); - playbackStarted[0] = true; - - view.postDelayed(() -> { - try { - if (mediaBrowserListenableFuture.isDone()) { - MediaBrowser browser = mediaBrowserListenableFuture.get(); - if (browser != null && browser.isPlaying()) { - dismissBottomSheet(); - return; - } - } - } catch (Exception e) { - // Ignore + if (!media.isEmpty()) { + boolean isFirstBatch = !playbackStarted; + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + playbackStarted = true; + + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).setBottomSheetInPeek(true); } - view.postDelayed(() -> dismissBottomSheet(), 200); - }, 300); - }, 300); + if (isFirstBatch && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } else { + Toast.makeText(getContext(), + R.string.bottom_sheet_problem_generating_instant_mix, + Toast.LENGTH_SHORT).show(); + if (!playbackStarted && !dismissalScheduled) { + scheduleDelayedDismissal(v); + } + } + } }); + }); TextView playNext = view.findViewById(R.id.play_next_text_view); @@ -414,4 +452,31 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements private void refreshShares() { homeViewModel.refreshShares(requireActivity()); } + + private void scheduleDelayedDismissal(View view) { + if (dismissalScheduled) return; + dismissalScheduled = true; + + view.postDelayed(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + if (browser != null && browser.isPlaying()) { + dismissBottomSheet(); + return; + } + } + } catch (Exception e) { + Log.e(TAG, "Error checking playback: " + e.getMessage()); + } + view.postDelayed(() -> dismissBottomSheet(), 200); + }, 300); + } + + private boolean isOffline(Exception exception) { + return exception != null && exception.getMessage() != null && + (exception.getMessage().contains("Network") || + exception.getMessage().contains("timeout") || + exception.getMessage().contains("offline")); + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java index efa74e90..665756a7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java @@ -10,6 +10,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.media3.common.util.UnstableApi; +import com.cappielloantonio.tempo.interfaces.MediaCallback; import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.repository.AlbumRepository; @@ -134,6 +135,17 @@ public class SongBottomSheetViewModel extends AndroidViewModel { return instantMix; } + public void getInstantMix(Child media, int count, MediaCallback callback) { + + songRepository.getInstantMix(media.getId(), SeedType.TRACK, count, songs -> { + if (songs != null && !songs.isEmpty()) { + callback.onLoadMedia(songs); + } else { + callback.onLoadMedia(Collections.emptyList()); + } + }); + } + public MutableLiveData shareTrack() { return sharingRepository.createShare(song.getId(), song.getTitle(), null); }