Merge branch 'development' into improve/update-zh

This commit is contained in:
eddyizm 2026-01-04 15:41:31 -08:00 committed by GitHub
commit 53ca88989f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 681 additions and 296 deletions

View file

@ -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;
@ -11,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.SeedType;
import java.util.ArrayList;
import java.util.Calendar;
@ -204,38 +206,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<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> 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<Child> 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(), SeedType.ALBUM, count, songs -> {
callback.onLoadMedia(songs);
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
callback.onLoadMedia(new ArrayList<>());
}
});
if (songs != null && !songs.isEmpty()) {
callback.onLoadMedia(songs);
}
});
}
public MutableLiveData<List<Integer>> getDecades() {
@ -248,7 +227,7 @@ public class AlbumRepository {
@Override
public void onLoadYear(int last) {
if (first != -1 && last != -1) {
List<Integer> decadeList = new ArrayList();
List<Integer> decadeList = new ArrayList<>();
int startDecade = first - (first % 10);
int lastDecade = last - (last % 10);

View file

@ -5,12 +5,14 @@ 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;
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.SeedType;
import java.util.ArrayList;
import java.util.Arrays;
@ -149,7 +151,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());
}
}
@ -287,28 +289,22 @@ public class ArtistRepository {
}
public MutableLiveData<List<Child>> getInstantMix(ArtistID3 artist, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(artist.getId(), count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return instantMix;
// Delegate to the centralized SongRepository
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<List<Child>> getRandomSong(ArtistID3 artist, int count) {
MutableLiveData<List<Child>> randomSongs = new MutableLiveData<>();

View file

@ -1,22 +1,33 @@
package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull;
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 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;
import retrofit2.Response;
public class SongRepository {
private static final String TAG = "SongRepository";
public interface MediaCallbackInternal {
void onSongsAvailable(List<Child> songs);
}
public MutableLiveData<List<Child>> getStarredSongs(boolean random, int size) {
MutableLiveData<List<Child>> starredSongs = new MutableLiveData<>(Collections.emptyList());
@ -42,219 +53,332 @@ public class SongRepository {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return starredSongs;
}
public MutableLiveData<List<Child>> getInstantMix(String id, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
/**
* Used by ViewModels. Updates the LiveData list incrementally as songs are found.
*/
public MutableLiveData<List<Child>> getInstantMix(String id, SeedType type, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(new ArrayList<>());
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(id, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
performSmartMix(id, type, count, songs -> {
List<Child> current = instantMix.getValue();
if (current != null) {
for (Child s : songs) {
if (!current.contains(s)) current.add(s);
}
if (current.size() < count / 2) {
fillWithRandom(count - current.size(), remainder -> {
for (Child r : remainder) {
if (!current.contains(r)) current.add(r);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
instantMix.setValue(null);
}
});
instantMix.postValue(current);
});
} else {
instantMix.postValue(current);
}
}
});
return instantMix;
}
public MutableLiveData<List<Child>> getRandomSample(int number, Integer fromYear, Integer toYear) {
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
/**
* Overloaded method used by other Repositories
*/
public void getInstantMix(String id, SeedType type, int count, MediaCallbackInternal callback) {
new MediaCallbackAccumulator(callback, count).start(id, type);
}
private class MediaCallbackAccumulator {
private final MediaCallbackInternal originalCallback;
private final int targetCount;
private final List<Child> accumulatedSongs = new ArrayList<>();
private final Set<String> trackIds = new HashSet<>();
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<Child> batch) {
if (isComplete || batch == null || batch.isEmpty()) {
return;
}
int added = 0;
for (Child song : batch) {
if (!trackIds.contains(song.getId()) && accumulatedSongs.size() < targetCount) {
trackIds.add(song.getId());
accumulatedSongs.add(song);
added++;
}
}
if (accumulatedSongs.size() >= targetCount) {
originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs));
isComplete = true;
}
}
}
private void performSmartMix(final String id, final 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 fetchAlbumSongsThenSimilar(String albumId, int count, MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null &&
response.body().getSubsonicResponse().getAlbum() != null) {
List<Child> albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs();
if (albumSongs != null && !albumSongs.isEmpty()) {
int fromAlbum = Math.min(count, albumSongs.size());
List<Child> limitedAlbumSongs = albumSongs.subList(0, fromAlbum);
callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs));
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<ApiResponse> call, @NonNull Throwable t) {
Log.d(TAG, "Album fetch failed: " + t.getMessage());
fillWithRandom(count, callback);
}
});
}
private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getRandomSongs(number, fromYear, toYear)
.getBrowsingClient()
.getSimilarSongs2(artistId, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> 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<Child> similar = extractSongs(response, "similarSongs2");
if (!similar.isEmpty()) {
List<Child> limitedSimilar = similar.subList(0, Math.min(count, similar.size()));
callback.onSongsAvailable(limitedSimilar);
} else {
fillWithRandom(count, callback);
}
randomSongsSample.setValue(songs);
}
@Override
@Override
public void onFailure(@NonNull Call<ApiResponse> 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<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
Child song = response.body().getSubsonicResponse().getSong();
if (song != null) {
callback.onSongsAvailable(Collections.singletonList(song));
int remaining = count - 1;
if (remaining > 0) {
fetchSimilarOnly(trackId, remaining, callback);
}
return;
}
}
fillWithRandom(count, callback);
}
@Override public void onFailure(@NonNull Call<ApiResponse> 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<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> songs = extractSongs(response, "similarSongs");
if (!songs.isEmpty()) {
List<Child> limitedSongs = songs.subList(0, Math.min(count, songs.size()));
callback.onSongsAvailable(limitedSongs);
} else {
fillWithRandom(count, callback);
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
fillWithRandom(count, callback);
}
});
}
private void fillWithRandom(int target, final MediaCallbackInternal callback) {
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getRandomSongs(target, null, null)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> random = extractSongs(response, "randomSongs");
if (!random.isEmpty()) {
List<Child> limitedRandom = random.subList(0, Math.min(target, random.size()));
callback.onSongsAvailable(limitedRandom);
} else {
callback.onSongsAvailable(new ArrayList<>());
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
callback.onSongsAvailable(new ArrayList<>());
}
});
}
private List<Child> extractSongs(Response<ApiResponse> response, String type) {
if (response.isSuccessful() && response.body() != null) {
SubsonicResponse res = response.body().getSubsonicResponse();
List<Child> 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<List<Child>> getRandomSample(int number, Integer fromYear, Integer toYear) {
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> 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<ApiResponse> call, @NonNull Throwable t) {}
});
return randomSongsSample;
}
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getRandomSongs(number, fromYear, toYear, genre)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> 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<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> 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<ApiResponse> call, @NonNull Throwable t) {}
});
return randomSongsSample;
}
public void scrobble(String id, boolean submission) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.scrobble(id, submission)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
}
public void setRating(String id, int rating) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.setRating(id, rating)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getMediaAnnotationClient().setRating(id, rating).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
}
public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) {
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getSongsByGenre(id, 100, 100 * page)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 100, 100 * page).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return songsByGenre;
}
public MutableLiveData<List<Child>> getSongsByGenres(ArrayList<String> genresId) {
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
for (String id : genresId)
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getSongsByGenre(id, 500, 0)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> 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<ApiResponse> call, @NonNull Throwable t) {
}
});
for (String id : genresId) {
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 500, 0).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
List<Child> 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<ApiResponse> call, @NonNull Throwable t) {}
});
}
return songsByGenre;
}
public MutableLiveData<Child> getSong(String id) {
MutableLiveData<Child> song = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSong(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
song.setValue(response.body().getSubsonicResponse().getSong());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getBrowsingClient().getSong(id).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
song.setValue(response.body().getSubsonicResponse().getSong());
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return song;
}
public MutableLiveData<String> getSongLyrics(Child song) {
MutableLiveData<String> lyrics = new MutableLiveData<>(null);
App.getSubsonicClientInstance(false)
.getMediaRetrievalClient()
.getLyrics(song.getArtist(), song.getTitle())
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
App.getSubsonicClientInstance(false).getMediaRetrievalClient().getLyrics(song.getArtist(), song.getTitle()).enqueue(new Callback<ApiResponse>() {
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
}
}
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
});
return lyrics;
}
}
}

View file

@ -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.SeedType;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
@ -183,11 +184,13 @@ public class MediaManager {
@OptIn(markerClass = UnstableApi.class)
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
final MediaBrowser browser = mediaBrowserListenableFuture.get();
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
new Handler(Looper.getMainLooper()).post(() -> {
justStarted.set(true);
browser.setMediaItems(items, startIndex, 0);
@ -196,28 +199,31 @@ public class MediaManager {
Player.Listener timelineListener = new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
int itemCount = browser.getMediaItemCount();
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
browser.seekTo(startIndex, 0);
browser.play();
browser.removeListener(this);
} else {
Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex);
}
}
};
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<MediaBrowser> mediaBrowserListenableFuture, Child media) {
@ -442,7 +448,7 @@ public class MediaManager {
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
Preferences.setLastInstantMix();
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10);
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, SeedType.TRACK, 10);
instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> media) {

View file

@ -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<Child>? = null
}

View file

@ -9,12 +9,12 @@ 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;
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;
@ -32,6 +32,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 +204,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 Observer<List<Child>>() {
@Override
public void onChanged(List<Child> 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 Observer<List<Child>>() {
@Override
public void onChanged(List<Child> songs) {
if (songs != null && !songs.isEmpty()) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
artistPageViewModel.getArtistInstantMix().removeObserver(this);
}
}
}));
}

View file

@ -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<Child> media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT);

View file

@ -5,6 +5,7 @@ import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -43,7 +44,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;
@ -61,7 +61,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
private List<Child> currentAlbumTracks = Collections.emptyList();
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
private boolean playbackStarted = false;
private boolean dismissalScheduled = false;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private static final String TAG = "AlbumBottomSheetDialog";
@Nullable
@Override
@ -114,33 +118,74 @@ 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() {
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) {
exception.printStackTrace();
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);
}
}
@Override
public void onLoadMedia(List<?> media) {
view.removeCallbacks(failsafeTimeout);
if (!isAdded() || getActivity() == null) {
return;
}
MusicUtil.ratingFilter((ArrayList<Child>) media);
if (!media.isEmpty()) {
boolean isFirstBatch = !playbackStarted;
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
playbackStarted = true;
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).setBottomSheetInPeek(true);
}
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);
}
}
dismissBottomSheet();
}
});
});
TextView playRandom = view.findViewById(R.id.play_random_text_view);
playRandom.setOnClickListener(v -> {
AlbumRepository albumRepository = new AlbumRepository();
@ -186,18 +231,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 -> {
@ -291,4 +334,31 @@ 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);
}
private boolean isOffline(Exception exception) {
return exception != null && exception.getMessage() != null &&
(exception.getMessage().contains("Network") ||
exception.getMessage().contains("timeout") ||
exception.getMessage().contains("offline"));
}
}

View file

@ -2,6 +2,7 @@ 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;
@ -18,10 +19,12 @@ 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;
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 +32,9 @@ 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
public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
private static final String TAG = "AlbumBottomSheetDialog";
@ -38,6 +44,9 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private boolean playbackStarted = false;
private boolean dismissalScheduled = false;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -86,20 +95,69 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
playRadio.setOnClickListener(v -> {
ArtistRepository artistRepository = new ArtistRepository();
Log.d(TAG, "Artist instant mix clicked");
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);
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);
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);
}
}
dismissBottomSheet();
@Override
public void onLoadMedia(List<?> media) {
view.removeCallbacks(failsafeTimeout);
if (!isAdded() || getActivity() == null) {
return;
}
Log.d(TAG, "Received " + media.size() + " songs for artist");
MusicUtil.ratingFilter((ArrayList<Child>) media);
if (!media.isEmpty()) {
boolean isFirstBatch = !playbackStarted;
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
playbackStarted = true;
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).setBottomSheetInPeek(true);
}
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);
}
}
}
});
});
@ -108,16 +166,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();
});
});
@ -139,4 +191,31 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
private void releaseMediaBrowser() {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
}
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"));
}
}

View file

@ -5,6 +5,9 @@ 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.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -23,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;
@ -50,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 {
@ -67,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<MediaBrowser> mediaBrowserListenableFuture;
private static final String TAG = "SongBottomSheetDialog";
@Nullable
@Override
@ -143,22 +152,68 @@ 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);
playbackStarted = false;
dismissalScheduled = false;
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> {
MusicUtil.ratingFilter(songs);
if (songs == null) {
dismissBottomSheet();
return;
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);
}
}
if (!songs.isEmpty()) {
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
dismissBottomSheet();
@Override
public void onLoadMedia(List<?> media) {
view.removeCallbacks(failsafeTimeout);
if (!isAdded() || getActivity() == null) {
return;
}
MusicUtil.ratingFilter((ArrayList<Child>) media);
if (!media.isEmpty()) {
boolean isFirstBatch = !playbackStarted;
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
playbackStarted = true;
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).setBottomSheetInPeek(true);
}
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);
@ -397,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"));
}
}

View file

@ -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
}
}

View file

@ -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<List<Child>> tracksLiveData = albumRepository.getAlbumTracks(album.getId());
tracksLiveData.observeForever(new Observer<List<Child>>() {
@OptIn(markerClass = UnstableApi.class)
@Override
public void onChanged(List<Child> songs) {
if (songs != null && !songs.isEmpty()) {

View file

@ -128,7 +128,6 @@ public class ArtistPageViewModel extends AndroidViewModel {
MappingUtil.mapDownloads(songs),
songs.stream().map(Download::new).collect(Collectors.toList())
);
} else {
}
}
});

View file

@ -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.SeedType;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
@ -34,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";
@ -223,7 +223,7 @@ public class HomeViewModel extends AndroidViewModel {
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
mediaInstantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue);
songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, mediaInstantMix::postValue);
return mediaInstantMix;
}

View file

@ -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<List<Child>> 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;
}

View file

@ -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;
@ -21,6 +22,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.SeedType;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.NetworkUtil;
@ -128,11 +130,22 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
instantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, instantMix::postValue);
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<Share> shareTrack() {
return sharingRepository.createShare(song.getId(), song.getTitle(), null);
}

View file

@ -51,6 +51,8 @@
<string name="battery_optimization_negative_button">Ignore</string>
<string name="battery_optimization_neutral_button">Don\'t ask again</string>
<string name="battery_optimization_positive_button">Disable</string>
<string name="bottom_sheet_generating_instant_mix">Generating instant mix...</string>
<string name="bottom_sheet_problem_generating_instant_mix">Could not retrieve tracks from subsonic server.</string>
<string name="connection_alert_dialog_negative_button">Cancel</string>
<string name="connection_alert_dialog_neutral_button">Enable data saver</string>
<string name="connection_alert_dialog_positive_button">OK</string>