From 3c1975f6bf86a0efb2197b9235485e510f9c22b0 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Fri, 26 Dec 2025 17:03:41 -0800 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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 5c94e9122c5890ab9ed024b08125f04d4af9dabd Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 28 Dec 2025 08:12:48 -0800 Subject: [PATCH 13/27] chore: bumped version --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c74d9cc2..43b139e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { targetSdk 35 versionCode 11 - versionName '4.6.0' + versionName '4.6.1' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { From 10673a49d48bbe7a8f8e7957cfefb6d13f9426a6 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 28 Dec 2025 08:20:58 -0800 Subject: [PATCH 14/27] 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 15/27] 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 16/27] 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 17/27] 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 1725b0de2e8123e2be3daffcfe1b0de81379d064 Mon Sep 17 00:00:00 2001 From: hongwei <72594688+hongwei1203@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:49:55 +0800 Subject: [PATCH 18/27] feat(i18n): add missing keys, update Chinese translation and alphabetize --- app/src/main/res/values-zh/arrays.xml | 30 +++ app/src/main/res/values-zh/strings.xml | 335 ++++++++++++++++++------- 2 files changed, 271 insertions(+), 94 deletions(-) diff --git a/app/src/main/res/values-zh/arrays.xml b/app/src/main/res/values-zh/arrays.xml index df659e57..a3b53543 100644 --- a/app/src/main/res/values-zh/arrays.xml +++ b/app/src/main/res/values-zh/arrays.xml @@ -32,6 +32,21 @@ 300 + + 禁用 + 128 MiB + 256 MiB + 512 MiB + 1024 MiB + + + 0 + 128 + 256 + 512 + 1024 + + 原始 32 kbps @@ -224,4 +239,19 @@ 4 8 + + + 不筛选评分 + 1 星及以上 + 2 星及以上 + 3 星及以上 + 4 星及以上 + + + 0 + 1 + 2 + 3 + 4 + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index dd7eed95..b1275066 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,13 +1,13 @@  如果遇到问题,请访问 https://dontkillmyapp.com。 省电优化选项可能会影响应用的性能,网站上提供了如何禁用这些选项的详细说明。 - 请禁用针对媒体锁屏播放的电池优化。 + 请禁用针对锁屏播放的电池优化。 电池优化 离线模式 添加到播放列表 添加到队列 全部下载 查看该艺术家 - 即时混合 + 即时混听 下一首播放 移除所有 分享 @@ -17,25 +17,27 @@ 检索艺术家时出错 已下载的专辑 最常播放的专辑 - 新发行 + 新发行的专辑 最近添加的专辑 最近播放的专辑 收藏的专辑 专辑 更多相似 播放 + 发行日期:%1$s + 发行日期:%1$s(原版发行于 %2$s) 随机播放 %1$d 首歌曲 • %2$d 分钟 Tempus 正在搜索... - 即时混合 + 即时混听 随机播放 艺术家 浏览艺术家 检索艺术家的电台时出错 - 检索艺术家曲目时出错 + 检索艺术家歌曲时出错 已下载的艺术家 - 收藏的艺人 + 收藏的艺术家 艺术家 电台 随机播放 @@ -43,33 +45,63 @@ 更多相似 专辑 更多 - 个人简介 + 艺术家简介 最常播放的歌曲 查看全部 + %1$s • %2$s + Tempus 资源链接 + 已将 %1$s 复制到剪贴板 + 资源链接:%1$s + 无法打开该专辑 + 无法打开该艺术家页 + 无法打开该播放列表 + 无法打开该歌曲 + 不支持的资源链接 + 专辑 UID + 艺术家 UID + 流派 UID + 播放列表 UID + 歌曲 UID + 资源 UID + 年份 UID 忽略 - 不要再问 + 不再询问 禁用 + 加载中... 取消 启用流量节省 确定 已限制通过 Wi-Fi 以外的连接访问 Subsonic 服务器。 要阻止此警告对话框再次出现,请在应用程序设置中禁用连接检查。 - Wi-Fi网络未连接 + Wi-Fi 网络未连接 + 随机 取消 继续 请注意,继续执行此操作将永久删除从所有服务器下载的所有已保存的项目。 删除已保存的项目 - 没有可用的描述 + 没有可用的歌词 + 第 %1$s 张光盘 - %2$s + 第 %1$s 张光盘 取消 下载 - 该文件夹中的所有曲目将被下载。 子文件夹中的曲目将不会被下载。 - 下载曲目 - 下载歌曲后,您可以在这里找到它。 + 该文件夹中的所有歌曲将被下载。子文件夹中的歌曲将不会被下载。 + 下载歌曲 + 设置歌曲下载位置 + 下载歌曲后,您可以在这里找到它 还没有下载! %1$s • %2$s 个项目 %1$s 个项目 + 刷新下载项 + 没有遗漏的下载项。 + 设置下载目录以刷新下载内容。 + + 已移除 %d 个缺失的下载项。 + 已移除 %d 个缺失的下载项。 + + 随机播放全部 要使更改生效,请重新启动应用程序。 更改已下载文件的目录将会立即删除以前已下载的所有文件。 选择存储选项 + 目录 外部 内部 下载 @@ -78,31 +110,75 @@ 移除 移除所有 随机播放 - + + 启用 + 均衡器 + 此设备不支持 + 重置 必需 必须是 http 或 https 前缀 + 取消收藏 + 收藏 下载 + 筛选艺术家 选择两个或多个过滤器 筛选 筛选流派 + 正在读取文件夹中的歌曲... + 文件夹内未发现歌曲 + 正在播放 %d 首歌曲 + (%1$d) + (+%1$d) 流派目录 浏览流派 + 稍后提醒 + 支持项目 + 立即下载 + GitHub 上发布了新版本。 + 有可用更新 + 定制首页 + 取消 + 重置 + 保存 + 请重启应用以应用更改。 + 主页排序 + 音乐 + 播客 + 电台 您最喜欢的艺术家的热门歌曲 - 从您喜欢的歌曲开始混音 + 从您喜欢的歌曲开始混听 添加新的电台 添加新的播客频道 + + %d 个待同步专辑 + %d 个待同步专辑 + + 标记为收藏的专辑可离线使用 + 同步收藏的专辑 + 你收藏的艺术家有未下载的歌曲 + 同步收藏的艺术家 + + %d 个待同步艺术家 + %d 个待同步艺术家 + 取消 下载 - 下载这些曲目可能需要大量移动数据 - 似乎有一些已收藏的曲目需要同步 + + %d 首待同步歌曲 + %d 首待同步歌曲 + + 下载这些歌曲可能需要大量移动数据流量 + 似乎有一些收藏的歌曲需要同步 最佳 发现 - 全部随机播放 - 闪回 + 随机播放全部 + 重温旧曲 网络广播电台 + 上月 最近播放 查看全部 上周 + 去年 为您定制 最常播放 查看全部 @@ -119,7 +195,7 @@ 查看全部 ★ 收藏的艺术家 查看全部 - ★ 收藏的曲目 + ★ 收藏的歌曲 查看全部 你最喜欢的歌曲 @@ -146,31 +222,53 @@ 专辑 艺术家 流派 - 曲目 + 歌曲 年份 首页 + 上月 + 上周 去年 曲库 + 添加到主页 + 专辑评分 搜索 设置 + 专辑数量 艺术家 - 姓名 + 最早收藏 + 最多播放 + 最近收藏 + 名称 随机 最近添加 + 最近播放 年份 + 从主页移除 + 下载离线歌词 + 暂无歌词可供下载。 + 离线歌词已保存。 + 已下载的离线歌词 %1$.2fx 清空队列 + 加载队列 + 保存队列 + 保存队列到播放列表 服务器优先级 + 正在转码 + 已请求转码 + 未知格式 播放列表目录 浏览播放列表 尚未创建播放列表 取消 新建 添加到播放列表 - 将歌曲添加到播放列表 未能将歌曲添加到播放列表 - %1$d 首曲目 • %2$s - 持续时间 • %1$s + 将歌曲添加到播放列表 + 所有歌曲已存在,无需重复添加 + %1$d 首歌曲 • %2$s + 时长 • %1$s + 长按删除 播放列表名称 取消 删除 @@ -189,6 +287,7 @@ 浏览频道 RSS 网址 播客频道 + 此服务器不支持播客。 描述 剧集 没有可用的剧集 @@ -197,6 +296,8 @@ 添加频道后,您将在此处找到它 未找到播客! %1$s • %2$s + 服务器不支持网络电台管理。 + 已添加电台 电台主页 URL 电台名称 广播流 URL @@ -204,6 +305,7 @@ 删除 保存 网络广播电台 + 已更新电台 单击以隐藏该部分\n重启应用后生效 添加广播电台后,您可以在此处找到它 没有找到电台! @@ -212,10 +314,14 @@ 评分 搜索标题、艺术家或专辑 输入至少三个字符 + 启用后将按时间排序搜索,关闭则按名称排序。 + 按时间排序最近搜索 专辑 艺术家 歌曲 + 长按删除 低安全性 + 本地 URL 服务器名称 密码 服务器地址 @@ -231,84 +337,118 @@ 服务器无法访问 Tempus 是 Subsonic 的开源轻量级音乐客户端,专为 Android 设计和构建。 关于 + 显示专辑详情 + 启用后将在专辑页显示流派、歌曲数量等信息 + 允许添加重复歌曲到播放列表 + 启用后则添加到播放列表时将不再检查重复内容。 保持屏幕常亮 + 均衡器 + 打开内置均衡器 + 按专辑数量排序艺术家 + 启用后按专辑数量排序;关闭则按名称排序。 + 显示音频质量 + 显示歌曲的码率和音频格式。 转码格式 - 如果启用,Tempus 将不会强制使用下面的转码设置下载曲目。 + 启用后 Tempus 将不会强制使用下面的转码设置下载歌曲。 优先考虑服务器上用于流式传输的设置 - 如果启用,Tempus 将下载转码后的曲目。 - 下载转码后的曲目 - 如果启用,将发送请求到服务器以查询曲目的估计持续时间。 + 启用后 Tempus 将下载转码后的歌曲。 + 下载转码后的歌曲 + 启用后将发送请求到服务器以查询歌曲的估计持续时间。 估计内容长度 用于下载的转码格式 移动数据下的转码格式 Wi-Fi 下的转码格式 - 如果启用,Tempus 将不会强制使用下面的转码设置流式传输曲目。 + 启用后 Tempus 将不会强制使用下面的转码设置流式传输歌曲。 优先考虑服务器转码设置 - 曲目转码设置优先级设置为服务器 + 歌曲转码设置优先级设置为服务器 + 自动下载歌词 + 自动保存可用歌词,以便离线时查看。 缓存策略 为了使更改生效,您必须手动重新启动应用程序。 - 允许在播放列表结束后,播放相似的曲目。 + 选择一个音乐下载目录 + 清空下载文件夹 + 允许在播放列表结束后,播放相似的歌曲。 连续播放 图片缓存大小 为了减少数据消耗,请避免下载封面。 限制移动数据使用 继续当前操作将导致所有已保存的项目被永久删除。 删除已保存的项目 + 下载文件夹已清除。 + 已设置下载文件夹 下载存储 - 调整音频设置 - 系统均衡器 https://github.com/eddyizm/tempus 关注开发进展 Github + 更新 + GitHub 版本默认会自动检查 APK 更新。您可以关闭此开关以禁用自动检查。 + 请访问 Github 以检查更新 设置图像分辨率 + 显示评分 + 启用后则显示项目的评分和收藏状态。 语言 注销登录 - 用于下载的比特率 - 移动数据下的比特率 - Wi-Fi 下的比特率 + 用于下载的码率 + 移动数据下的码率 + Wi-Fi 下的码率 媒体文件缓存大小 显示音乐目录 - 如果启用,则显示音乐目录部分。 请注意,要使文件夹导航正常工作,服务器必须支持此功能。 + 启用后则显示音乐目录部分。 请注意,要使文件夹导航正常工作,服务器必须支持此功能。 显示播客 - 如果启用,则显示播客部分。 - 显示音频质量 - 显示曲目的比特率和音频格式。 - 显示评分 - 如果启用,则显示项目的评分和收藏状态。 + 启用后则显示播客部分。 同步定时器 - 如果启用,将允许当前用户保存其播放队列,并能够在打开应用程序时加载保存状态。 + 启用后将允许当前用户保存其播放队列,并能够在打开应用程序时加载保存状态。 同步当前用户的播放队列 - 显示广播 - 如果启用,则显示电台部分。 + 显示电台 + 启用后,则显示电台部分。 设置播放增益模式 圆角 圆角大小 设置圆角的大小。 - 如果启用,则为所有渲染的封面设置圆角。 更改将在应用重新启动后生效。 + 启用后则为所有渲染的封面设置圆角。更改将在应用重新启动后生效。 + 正在扫描:已发现 %1$d 首歌曲 扫描曲库 启用音乐记录 + 设置下载文件夹 启用音乐共享 + 显示随机按钮 + 启用后,在迷你播放器中显示随机播放按钮,并移除收藏按钮。 + 显示歌曲评分 + 启用后歌曲详情页将显示五星评分。\n\n*需重启应用后生效 播放缓存大小 + 缓存目录设置 请注意,音乐记录同时也依赖于服务器是否能够接收这些数据。 - 收听电台,即时混合和随机播放时,低于特定评分的曲目将会被忽略。 - 播放增益(Replay gain)允许您通过调整音轨的音量,以获得始终如一的聆听体验。 仅当曲目标签包含必要的元数据时,此设置才有效。 + 播放增益(Replay gain)允许您通过调整音轨的音量,以获得始终如一的聆听体验。 仅当歌曲标签包含必要的元数据时,此设置才有效。 音乐记录(Scrobbling)允许您的设备将您收听的歌曲的相关信息发送到音乐服务器。 这些信息有助于基于您的音乐偏好生成个性化推荐。 - 允许用户通过链接共享音乐。 该功能需要服务器端支持并启用,并且仅限于单个曲目、专辑和队列。 - 返回当前用户的播放队列状态。 这包括播放队列中的曲目、正在播放的曲目以及曲目播放进度。需要服务器支持此功能。 + 允许用户通过链接共享音乐。 该功能需要服务器端支持并启用,并且仅限于单首歌曲、专辑和队列。 + 收听电台,即时混合和随机播放时,低于特定评分的歌曲将会被忽略。 %1$s \n已使用: %2$s MiB - 转码模式优先级设置。 如果设置为“播放原始”,文件的比特率将不会更改。 - 下载转码后的媒体。 如果启用,将不会下载原始数据,而是使用以下设置。\n如果“用于下载的转码格式”设置为“下载原始”,则文件的比特率不会更改。 - 当文件即时转码时,客户端通常不会显示曲目长度。 可以向支持该功能的服务器发送请求,估计正在播放的曲目的持续时间,但可能响应变慢。 - 如果启用,将下载已收藏的曲目以供离线使用。 - 同步已收藏的曲目以供离线使用 + 返回当前用户的播放队列状态。 这包括播放队列中的歌曲、正在播放的歌曲以及歌曲播放进度。需要服务器支持此功能。 + 转码模式优先级设置。 如果设置为“播放原始”,文件的码率将不会更改。 + 下载转码后的媒体。 启用后将不会下载原始数据,而是使用以下设置。\n如果“用于下载的转码格式”设置为“下载原始”,则文件的码率不会更改。 + 当文件即时转码时,客户端通常不会显示歌曲长度。 可以向支持该功能的服务器发送请求,估计正在播放的歌曲的持续时间,但可能响应变慢。 + https://github.com/eddyizm/tempus/discussions + 加入社区讨论并获取帮助 + 用户支持 + 启用后将下载收藏的专辑以供离线使用。 + 下载收藏的专辑以供离线使用 + 启用后将自动下载收藏的艺术家以供离线使用。 + 同步收藏的艺术家以供离线使用 + 启用后将下载收藏的歌曲以供离线使用。 + 同步收藏的歌曲以供离线使用 + 调整音频设置 + 系统均衡器 + 系统语言 主题 数据 通用 + 播放列表 评分 播放增益 音乐记录 - 根据评分忽略歌曲 分享 + 根据评分忽略歌曲 + 根据歌曲评分筛选: 同步 转码 转码下载 @@ -321,6 +461,7 @@ 复制链接 删除分享 更新分享 + 永不过期 到期日期:%1$s 不支持分享或未启用 描述 @@ -341,63 +482,69 @@ 移除 分享 已下载 - 最常播放的曲目 - 最近添加的曲目 - 最近播放的曲目 - 已收藏的曲目 - %1$s 的热门曲目 + 最常播放的歌曲 + 最近添加的歌曲 + 最近播放的歌曲 + 已收藏的歌曲 + %1$s 的热门歌曲 年份 %1$d %1$s • %2$s %3$s + + 正在下载 %d 首歌曲 + 正在下载 %d 首歌曲 + + 下载收藏的专辑可能会消耗大量移动数据流量。 + 同步收藏的专辑 + 下载收藏的艺术家可能会消耗大量移动数据流量。 + 同步收藏的艺术家 取消 继续 继续并下载 - 下载收藏曲目可能需要大量数据。 - 同步已收藏的曲目 + 下载收藏的歌曲可能会消耗大量移动数据流量。 + 同步收藏的歌曲 + 要使更改生效,请重新启动应用程序。 + 切换缓存文件的存储路径可能会导致原位置存储的缓存文件被清空。 + 选择存储位置 + 外部 + 内部 + https://ko-fi.com/eddyizm 专辑 艺术家 - 比特率 + 位深 + 码率 内容类型 确定 - 曲目信息 + 歌曲信息 碟片编号 持续时间 流派 路径 + 采样率 大小 后缀 - 该文件已使用 Subsonic API 下载。 文件的编码和比特率与源文件一致。 - 本应用将请求服务器对文件进行转码并修改其比特率。 用户请求的编解码器是%1$s,比特率为%2$s。 对所选格式的文件的编码和比特率的任何潜在更改都将由服务器处理,服务器可能支持也可能不支持该操作。 - 本应用只会读取服务器提供的原始文件。 本应用将明确向服务器请求具有原始源比特率的未转码文件。 - 要播放的文件质量取决于服务器设置。 本应用不会强制选择任何用于潜在转码的编码和比特率。 - 本应用将请求服务器修改文件的比特率。 用户请求的比特率为%1$s,而源文件的编码将保持不变。 对所选格式的文件比特率的任何更改都将由服务器完成,服务器可能支持也可能不支持该操作。 - 本应用将请求服务器对文件进行转码。 用户请求的编解码器是%1$s,而比特率将与源文件相同。 将文件转码为所选格式的可能性取决于服务器,因为它可能支持也可能不支持该操作。 + 该文件已使用 Subsonic API 下载。 文件的编码和码率与源文件一致。 + 本应用将请求服务器对文件进行转码并修改其码率。 用户请求的编解码器是%1$s,码率为%2$s。 对所选格式的文件的编码和码率的任何潜在更改都将由服务器处理,服务器可能支持也可能不支持该操作。 + 本应用只会读取服务器提供的原始文件。 本应用将明确向服务器请求具有原始源码率的未转码文件。 + 要播放的文件质量取决于服务器设置。 本应用不会强制选择任何用于潜在转码的编码和码率。 + 本应用将请求服务器修改文件的码率。 用户请求的码率为%1$s,而源文件的编码将保持不变。 对所选格式的文件码率的任何更改都将由服务器完成,服务器可能支持也可能不支持该操作。 + 本应用将请求服务器对文件进行转码。 用户请求的编解码器是%1$s,而码率将与源文件相同。 将文件转码为所选格式的可能性取决于服务器,因为它可能支持也可能不支持该操作。 标题 - 曲目编号 + 歌曲编号 转码内容类型 转码后缀 年份 - 外部 - 内部 unDraw 特别感谢 unDraw,没有它提供的插图,我们的应用不可能会如此精美。 https://undraw.co/ - 发布于 %1$s - 第 %1$s 张光盘 - %2$s - 第 %1$s 张光盘 - 随机播放 - 稍后提醒 - 现在下载 - 有可用更新 - 取消 - 重置 - 保存 - 上个月 - 去年 - 上周 - 上个月 - 添加到主屏幕 - 从主屏幕移除 - 长按删除 - 长按删除 - 本地 URL + 专辑封面 + 下一首 + 播放或暂停 + 上一首 + 更改循环模式 + 切换随机播放 + Tempus 小组件 + 未播放 + 打开 Tempus + 0:00 + 0:00 From 193447d07e5ebd84764bb3e9a0cd6a39faaf0b08 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Wed, 31 Dec 2025 08:20:03 -0800 Subject: [PATCH 19/27] 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 20/27] 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 586a1a160e4a658a3f7d71a965d2fd10a2e6978c Mon Sep 17 00:00:00 2001 From: Age Bosma Date: Sat, 3 Jan 2026 15:53:37 +0100 Subject: [PATCH 21/27] Clarify Android Auto enablement --- USAGE.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/USAGE.md b/USAGE.md index d3a946db..4b60af75 100644 --- a/USAGE.md +++ b/USAGE.md @@ -160,7 +160,23 @@ If your server supports it - add a internet radio station feed ## Android Auto ### Enabling on your head unit -- You have to enable Android Auto developer options, which are different from actual Android dev options. Then you have to enable "Unknown sources" in Android Auto, otherwise the app won't appear as it isn't downloaded from Play Store. (screenshots needed) +To allow the Tempus app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempus isn't installed through Play Store. Note that the Android Auto developer settings are different from the global Android "Developer options". +1. Switch to developer mode in the Android Auto settings by tapping ten times on the "Version" item at the bottom, followed by giving your permission. +

+ 1a + 1b + 1c +

+ +2. Go to the "Developer settings" by the menu at the top right. +

+ 2 +

+ +3. Scroll down to the bottom and check "Unknown sources". +

+ 3 +

### Server Settings From 05785979e3a31dd7d81d6767f54d40cdd29e0eb4 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 3 Jan 2026 08:17:53 -0800 Subject: [PATCH 22/27] 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 23/27] 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 24/27] 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 25/27] 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 26/27] 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 27/27] 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); }