diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7f9b852..abe73a72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
# Changelog
-## Pending release...
+## [4.6.3](https://github.com/eddyizm/tempo/releases/tag/v4.6.3) (2026-01-10)
+* fix: give user feedback when trying to add podcast/radio on unsupport… by @eddyizm in https://github.com/eddyizm/tempus/pull/328
+* docs: Clarify Android Auto enablement by @Forage in https://github.com/eddyizm/tempus/pull/336
+* fix: instant mix gets a big refactor, with cascading fallbacks to produce a larger queue by @eddyizm in https://github.com/eddyizm/tempus/pull/330
+* chore(i18n): add missing keys, update Chinese translation and alphabetize by @hongwei1203 in https://github.com/eddyizm/tempus/pull/332
+* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/339
+* feat: Ability to toggle visibility of artist biography by @kmarius in https://github.com/eddyizm/tempus/pull/338
## [4.6.0](https://github.com/eddyizm/tempo/releases/tag/v4.6.0) (2025-12-22)
* chore: Update description_empty_title in English and Polish by @tyren234 in https://github.com/eddyizm/tempus/pull/307
diff --git a/README.md b/README.md
index ca6dd30a..c7fb9618 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
**Tempus** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device.
-Tempus does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Last.fm to personalize your music experience.
+Tempus does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Listenbrainz.org and Last.fm to personalize your music experience (These must be supported by your backend).
The project is a fork of [Tempo](#credits).
@@ -59,16 +59,19 @@ Please note the two variants in the release assets include release/debug and 32/
- **Streaming and Offline Mode**: Stream music directly from your Subsonic server. Offline mode is currently under active development and may have limitations when using multiple servers.
- **Playlist Management**: Create, edit, and manage playlists to curate your perfect music collection.
- **Gapless Playback**: Experience uninterrupted playback with gapless listening mode.
-- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state.
+- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state.*
- **Scrobbling Integration**: Optionally integrate Tempus with Last.fm or Listenbrainz.org to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempus, expanding your audio entertainment options.
+- **Instant Mix**: Full refactor of instant mix function which leverages subsonics similar songs by artist/album and randomSongs endpoints to server a larger play queue more reliably.
- **Transcoding Support**: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server.
-- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.
+- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.*
- **Multiple Libraries**: Tempus handles multi-library setups gracefully. They are displayed as Library folders.
- **Equalizer**: Option to use in app equalizer.
- **Widget**: New widget to keeping the basic controls on your screen at all times.
- **Available in 11 languages**: Currently in Chinese, French, German, Italian, Korean, Polish, Portuguese, Russion, Spanish and Turkish
+ **Github version only*
+
## Screenshot
diff --git a/USAGE.md b/USAGE.md
index d3a946db..009b85da 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -30,7 +30,7 @@ This app works with any service that implements the Subsonic API, including:
- [Gonic](https://github.com/sentriz/gonic)
- [Ampache](https://github.com/ampache/ampache)
- [NextCloud Music](https://apps.nextcloud.com/apps/music)
-
+- [Airsonic Advanced](https://github.com/kagemomiji/airsonic-advanced)
@@ -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.
+
+
+
+
+
+
+2. Go to the "Developer settings" by the menu at the top right.
+
+
+
+
+3. Scroll down to the bottom and check "Unknown sources".
+
+
+
### Server Settings
diff --git a/app/build.gradle b/app/build.gradle
index c74d9cc2..90b76011 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,8 +10,8 @@ android {
minSdkVersion 24
targetSdk 35
- versionCode 11
- versionName '4.6.0'
+ versionCode 12
+ versionName '4.6.3'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions {
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..e07a4395 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java
@@ -2,15 +2,16 @@ package com.cappielloantonio.tempo.repository;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
+
import android.util.Log;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.interfaces.DecadesCallback;
-import com.cappielloantonio.tempo.interfaces.MediaCallback;
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,40 +205,12 @@ 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<>();
-
- 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);
- }
- }
-
- callback.onLoadMedia(songs);
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- callback.onLoadMedia(new ArrayList<>());
- }
- });
+ public MutableLiveData> getInstantMix(AlbumID3 album, int count) {
+ // Delegate to the centralized SongRepository
+ return new SongRepository().getInstantMix(album.getId(), SeedType.ALBUM, count);
}
+
public MutableLiveData> getDecades() {
MutableLiveData> decades = new MutableLiveData<>();
@@ -248,7 +221,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);
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..a0d351e7 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java
@@ -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,26 +289,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(), SeedType.ARTIST, count);
}
public MutableLiveData> getRandomSong(ArtistID3 artist, int count) {
diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java
index edfbbcc6..4237d763 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java
@@ -66,88 +66,33 @@ public class PodcastRepository {
return liveNewestPodcastEpisodes;
}
- public void refreshPodcasts() {
- App.getSubsonicClientInstance(false)
+ public Call refreshPodcasts() {
+ return App.getSubsonicClientInstance(false)
.getPodcastClient()
- .refreshPodcasts()
- .enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
-
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
- });
+ .refreshPodcasts();
}
- public void createPodcastChannel(String url) {
- App.getSubsonicClientInstance(false)
+ public Call createPodcastChannel(String url) {
+ return App.getSubsonicClientInstance(false)
.getPodcastClient()
- .createPodcastChannel(url)
- .enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
-
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
- });
+ .createPodcastChannel(url);
}
- public void deletePodcastChannel(String channelId) {
- App.getSubsonicClientInstance(false)
+ public Call deletePodcastChannel(String channelId) {
+ return App.getSubsonicClientInstance(false)
.getPodcastClient()
- .deletePodcastChannel(channelId)
- .enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
-
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
- });
+ .deletePodcastChannel(channelId);
}
- public void deletePodcastEpisode(String episodeId) {
- App.getSubsonicClientInstance(false)
+ public Call deletePodcastEpisode(String episodeId) {
+ return App.getSubsonicClientInstance(false)
.getPodcastClient()
- .deletePodcastEpisode(episodeId)
- .enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
-
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
- });
+ .deletePodcastEpisode(episodeId);
}
- public void downloadPodcastEpisode(String episodeId) {
- App.getSubsonicClientInstance(false)
+ public Call downloadPodcastEpisode(String episodeId) {
+ return App.getSubsonicClientInstance(false)
.getPodcastClient()
- .downloadPodcastEpisode(episodeId)
- .enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
-
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
- });
+ .downloadPodcastEpisode(episodeId);
}
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java
index 9ad8a111..dea17558 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java
@@ -38,54 +38,22 @@ public class RadioRepository {
return radioStation;
}
- public void createInternetRadioStation(String name, String streamURL, String homepageURL) {
- App.getSubsonicClientInstance(false)
+ public Call createInternetRadioStation(String name, String streamURL, String homepageURL) {
+ return App.getSubsonicClientInstance(false)
.getInternetRadioClient()
- .createInternetRadioStation(streamURL, name, homepageURL)
- .enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
-
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
- });
+ .createInternetRadioStation(streamURL, name, homepageURL);
}
- public void updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) {
- App.getSubsonicClientInstance(false)
+ public Call updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) {
+ return App.getSubsonicClientInstance(false)
.getInternetRadioClient()
- .updateInternetRadioStation(id, streamURL, name, homepageURL)
- .enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
-
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
- });
+ .updateInternetRadioStation(id, streamURL, name, homepageURL);
}
- public void deleteInternetRadioStation(String id) {
- App.getSubsonicClientInstance(false)
+ public Call deleteInternetRadioStation(String id) {
+ return App.getSubsonicClientInstance(false)
.getInternetRadioClient()
- .deleteInternetRadioStation(id)
- .enqueue(new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
-
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
- });
+ .deleteInternetRadioStation(id);
}
+
}
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..25990236 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java
@@ -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 songs);
+ }
public MutableLiveData> getStarredSongs(boolean random, int size) {
MutableLiveData> starredSongs = new MutableLiveData<>(Collections.emptyList());
@@ -42,219 +53,332 @@ public class SongRepository {
}
@Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
- }
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {}
});
return starredSongs;
}
- public MutableLiveData> getInstantMix(String id, int count) {
- MutableLiveData> instantMix = new MutableLiveData<>();
+ /**
+ * Used by ViewModels. Updates the LiveData list incrementally as songs are found.
+ */
+ public MutableLiveData> getInstantMix(String id, SeedType type, int count) {
+ 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());
+ performSmartMix(id, type, count, songs -> {
+ List 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 call, @NonNull Throwable t) {
- instantMix.setValue(null);
- }
- });
+ instantMix.postValue(current);
+ });
+ } else {
+ 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, 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 accumulatedSongs = new ArrayList<>();
+ private final Set 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 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() {
+ @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()) {
+ int fromAlbum = Math.min(count, albumSongs.size());
+ List 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 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() {
@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 similar = extractSongs(response, "similarSongs2");
+ if (!similar.isEmpty()) {
+ List 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 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));
+ int remaining = count - 1;
+ if (remaining > 0) {
+ fetchSimilarOnly(trackId, remaining, 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()) {
+ 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 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) {
+ 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<>());
+ }
+ });
+ }
+
+ 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
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..ab591104 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.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 mediaBrowserListenableFuture, List media, int startIndex) {
if (mediaBrowserListenableFuture != null) {
+
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
final MediaBrowser browser = mediaBrowserListenableFuture.get();
final List 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 mediaBrowserListenableFuture, Child media) {
@@ -442,7 +448,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, 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/ui/dialog/RadioEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java
index b4aba968..487bb916 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java
@@ -3,11 +3,13 @@ package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
+import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogRadioEditorBinding;
import com.cappielloantonio.tempo.interfaces.RadioCallback;
@@ -21,7 +23,6 @@ import java.util.Objects;
public class RadioEditorDialog extends DialogFragment {
private DialogRadioEditorBinding bind;
private RadioEditorViewModel radioEditorViewModel;
-
private final RadioCallback radioCallback;
private String radioName;
@@ -36,25 +37,26 @@ public class RadioEditorDialog extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogRadioEditorBinding.inflate(getLayoutInflater());
-
radioEditorViewModel = new ViewModelProvider(requireActivity()).get(RadioEditorViewModel.class);
+ setupObservers();
+
return new MaterialAlertDialogBuilder(requireContext())
.setView(bind.getRoot())
.setTitle(R.string.radio_editor_dialog_title)
.setPositiveButton(R.string.radio_editor_dialog_positive_button, (dialog, id) -> {
if (validateInput()) {
if (radioEditorViewModel.getRadioToEdit() == null) {
- radioEditorViewModel.createRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL);
+ radioEditorViewModel.createRadio(radioName, radioStreamURL,
+ radioHomepageURL.isEmpty() ? null : radioHomepageURL);
} else {
- radioEditorViewModel.updateRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL);
+ radioEditorViewModel.updateRadio(radioName, radioStreamURL,
+ radioHomepageURL.isEmpty() ? null : radioHomepageURL);
}
- dismissDialog();
}
})
.setNeutralButton(R.string.radio_editor_dialog_neutral_button, (dialog, id) -> {
radioEditorViewModel.deleteRadio();
- dismissDialog();
})
.setNegativeButton(R.string.radio_editor_dialog_negative_button, (dialog, id) -> {
dialog.cancel();
@@ -62,6 +64,24 @@ public class RadioEditorDialog extends DialogFragment {
.create();
}
+ private void setupObservers() {
+ radioEditorViewModel.getIsSuccess().observe(this, isSuccess -> {
+ if (isSuccess != null && isSuccess) {
+ Toast.makeText(requireContext(),
+ radioEditorViewModel.getRadioToEdit() == null ?
+ App.getContext().getString(R.string.radio_editor_dialog_added) : App.getContext().getString(R.string.radio_editor_dialog_updated),
+ Toast.LENGTH_SHORT).show();
+ dismissDialog();
+ }
+ });
+ radioEditorViewModel.getErrorMessage().observe(this, error -> {
+ if (error != null && !error.isEmpty()) {
+ Toast.makeText(requireContext(), error, Toast.LENGTH_LONG).show();
+ radioEditorViewModel.clearError();
+ }
+ });
+ }
+
@Override
public void onStart() {
super.onStart();
@@ -77,7 +97,6 @@ public class RadioEditorDialog extends DialogFragment {
private void setParameterInfo() {
if (getArguments() != null && getArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT) != null) {
InternetRadioStation toEdit = requireArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT);
-
radioEditorViewModel.setRadioToEdit(toEdit);
bind.internetRadioStationNameTextView.setText(toEdit.getName());
@@ -90,22 +109,21 @@ public class RadioEditorDialog extends DialogFragment {
radioName = Objects.requireNonNull(bind.internetRadioStationNameTextView.getText()).toString().trim();
radioStreamURL = Objects.requireNonNull(bind.internetRadioStationStreamUrlTextView.getText()).toString().trim();
radioHomepageURL = Objects.requireNonNull(bind.internetRadioStationHomepageUrlTextView.getText()).toString().trim();
-
if (TextUtils.isEmpty(radioName)) {
bind.internetRadioStationNameTextView.setError(getString(R.string.error_required));
return false;
}
-
if (TextUtils.isEmpty(radioStreamURL)) {
bind.internetRadioStationStreamUrlTextView.setError(getString(R.string.error_required));
return false;
}
-
return true;
}
private void dismissDialog() {
- radioCallback.onDismiss();
+ if (radioCallback != null) {
+ radioCallback.onDismiss();
+ }
Objects.requireNonNull(getDialog()).dismiss();
}
-}
+}
\ No newline at end of file
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..06bcd984 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,12 +9,14 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Button;
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,12 +34,14 @@ 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;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
+import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.google.common.util.concurrent.ListenableFuture;
@@ -118,6 +122,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(artistPageViewModel.getArtist().getStarred() != null);
favoriteToggle.setOnClickListener(v -> artistPageViewModel.setFavorite(requireContext()));
+
+ Button bioToggle = view.findViewById(R.id.button_toggle_bio);
+ bioToggle.setOnClickListener(v ->
+ Toast.makeText(getActivity(), R.string.artist_no_artist_info_toast, Toast.LENGTH_SHORT).show());
}
private void initAppBar() {
@@ -135,13 +143,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
if (artistInfo == null) {
if (bind != null) bind.artistPageBioSector.setVisibility(View.GONE);
} else {
- String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography());
-
- if (bind != null)
- bind.artistPageBioSector.setVisibility(!normalizedBio.trim().isEmpty() ? View.VISIBLE : View.GONE);
- if (bind != null)
- bind.bioMoreTextViewClickable.setVisibility(artistInfo.getLastFmUrl() != null ? View.VISIBLE : View.GONE);
-
if (getContext() != null && bind != null) {
ArtistID3 currentArtist = artistPageViewModel.getArtist();
String primaryId = currentArtist.getCoverArtId() != null && !currentArtist.getCoverArtId().trim().isEmpty()
@@ -191,34 +192,67 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
.into(bind.artistBackdropImageView);
}
- if (bind != null) bind.bioTextView.setText(normalizedBio);
+ if (bind != null) {
+ String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography()).trim();
+ String lastFmUrl = artistInfo.getLastFmUrl();
- if (bind != null) bind.bioMoreTextViewClickable.setOnClickListener(v -> {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(Uri.parse(artistInfo.getLastFmUrl()));
- startActivity(intent);
- });
+ if (normalizedBio.isEmpty()) {
+ bind.bioTextView.setVisibility(View.GONE);
+ } else {
+ bind.bioTextView.setText(normalizedBio);
+ }
- if (bind != null) bind.artistPageBioSector.setVisibility(View.VISIBLE);
+ if (lastFmUrl == null) {
+ bind.bioMoreTextViewClickable.setVisibility(View.GONE);
+ } else {
+ bind.bioMoreTextViewClickable.setOnClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(artistInfo.getLastFmUrl()));
+ startActivity(intent);
+ });
+ bind.bioMoreTextViewClickable.setVisibility(View.VISIBLE);
+ }
+
+ if (!normalizedBio.isEmpty() || lastFmUrl != null) {
+ View view = bind.getRoot();
+
+ Button bioToggle = view.findViewById(R.id.button_toggle_bio);
+ bioToggle.setOnClickListener(v -> {
+ if (bind != null) {
+ boolean displayBio = Preferences.getArtistDisplayBiography();
+ Preferences.setArtistDisplayBiography(!displayBio);
+ bind.artistPageBioSector.setVisibility(displayBio ? View.GONE : View.VISIBLE);
+ }
+ });
+
+ boolean displayBio = Preferences.getArtistDisplayBiography();
+ bind.artistPageBioSector.setVisibility(displayBio ? View.VISIBLE : View.GONE);
+ }
+ }
}
});
}
+
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>() {
+ @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 Observer>() {
+ @Override
+ public void onChanged(List songs) {
+ if (songs != null && !songs.isEmpty()) {
+ MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
+ activity.setBottomSheetInPeek(true);
+ artistPageViewModel.getArtistInstantMix().removeObserver(this);
+ }
}
}));
}
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 a6167eed..dd810b8e 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
@@ -24,7 +24,6 @@ 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.repository.AlbumRepository;
import com.cappielloantonio.tempo.service.MediaManager;
@@ -43,7 +42,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 +59,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
private List currentAlbumTracks = Collections.emptyList();
private List currentAlbumMediaItems = Collections.emptyList();
+ private boolean isFirstBatch = true;
+
private ListenableFuture mediaBrowserListenableFuture;
+ private static final String TAG = "AlbumBottomSheetDialog";
@Nullable
@Override
@@ -114,33 +115,41 @@ 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();
- }
+ MainActivity activity = (MainActivity) getActivity();
+ if (activity == null) return;
- @Override
- public void onLoadMedia(List> media) {
- MusicUtil.ratingFilter((ArrayList) media);
+ ListenableFuture activityBrowserFuture = activity.getMediaBrowserListenableFuture();
+ if (activityBrowserFuture == null) return;
- if (!media.isEmpty()) {
- MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0);
- ((MainActivity) requireActivity()).setBottomSheetInPeek(true);
+ isFirstBatch = true;
+ Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
+
+ albumBottomSheetViewModel.getAlbumInstantMix(activity, album).observe(activity, media -> {
+ if (media == null || media.isEmpty()) return;
+ if (getActivity() == null) return;
+
+ MusicUtil.ratingFilter(media);
+
+ if (isFirstBatch) {
+ isFirstBatch = false;
+
+ MediaManager.startQueue(activityBrowserFuture, media, 0);
+ activity.setBottomSheetInPeek(true);
+
+ if (isAdded()) {
+ dismissBottomSheet();
}
-
- dismissBottomSheet();
+ } else {
+ MediaManager.enqueue(activityBrowserFuture, media, true);
}
});
});
+
TextView playRandom = view.findViewById(R.id.play_random_text_view);
playRandom.setOnClickListener(v -> {
AlbumRepository albumRepository = new AlbumRepository();
@@ -186,18 +195,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 +298,5 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
private void refreshShares() {
homeViewModel.refreshShares(requireActivity());
}
+
}
\ 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 9ec9b549..95d63332 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
@@ -29,6 +29,7 @@ import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.common.util.concurrent.ListenableFuture;
+
@UnstableApi
public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
private static final String TAG = "AlbumBottomSheetDialog";
@@ -38,6 +39,8 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
private ListenableFuture mediaBrowserListenableFuture;
+ private boolean isFirstBatch = true;
+
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@@ -86,20 +89,31 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
playRadio.setOnClickListener(v -> {
- ArtistRepository artistRepository = new ArtistRepository();
+ MainActivity activity = (MainActivity) getActivity();
+ if (activity == null) return;
- artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> {
- // navidrome may return null for this
- if (songs == null)
- return;
- MusicUtil.ratingFilter(songs);
+ ListenableFuture activityBrowserFuture = activity.getMediaBrowserListenableFuture();
+ if (activityBrowserFuture == null) return;
- if (!songs.isEmpty()) {
- MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
- ((MainActivity) requireActivity()).setBottomSheetInPeek(true);
+ isFirstBatch = true;
+ Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
+
+ artistBottomSheetViewModel.getArtistInstantMix(activity, artist).observe(activity, media -> {
+ if (media == null || media.isEmpty()) return;
+ if (getActivity() == null) return;
+
+ MusicUtil.ratingFilter(media);
+
+ if (isFirstBatch) {
+ isFirstBatch = false;
+ MediaManager.startQueue(activityBrowserFuture, media, 0);
+ activity.setBottomSheetInPeek(true);
+ if (isAdded()) {
+ dismissBottomSheet();
+ }
+ } else {
+ MediaManager.enqueue(activityBrowserFuture, media, true);
}
-
- dismissBottomSheet();
});
});
@@ -108,16 +122,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 +147,5 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
private void releaseMediaBrowser() {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
-}
\ No newline at end of file
+
+}
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..bf421c24 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,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;
@@ -44,8 +45,6 @@ import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.common.util.concurrent.ListenableFuture;
-import android.content.Intent;
-import androidx.media3.common.MediaItem;
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
import java.util.ArrayList;
@@ -67,7 +66,9 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private AssetLinkUtil.AssetLink currentAlbumLink;
private AssetLinkUtil.AssetLink currentArtistLink;
+ private boolean isFirstBatch = true;
private ListenableFuture mediaBrowserListenableFuture;
+ private static final String TAG = "SongBottomSheetDialog";
@Nullable
@Override
@@ -143,20 +144,34 @@ 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);
+ MainActivity activity = (MainActivity) getActivity();
+ if (activity == null) return;
- songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> {
- MusicUtil.ratingFilter(songs);
+ ListenableFuture activityBrowserFuture = activity.getMediaBrowserListenableFuture();
+ if (activityBrowserFuture == null) {
+ Log.e(TAG, "MediaBrowser Future is null in MainActivity");
+ return;
+ }
- if (songs == null) {
- dismissBottomSheet();
- return;
- }
+ isFirstBatch = true;
+ Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
- if (!songs.isEmpty()) {
- MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
- dismissBottomSheet();
+ songBottomSheetViewModel.getInstantMix(activity, song).observe(activity, media -> {
+
+ if (media == null || media.isEmpty()) return;
+ if (getActivity() == null) return;
+
+ MusicUtil.ratingFilter(media);
+
+ if (isFirstBatch) {
+ isFirstBatch = false;
+ MediaManager.startQueue(activityBrowserFuture, media, 0);
+ activity.setBottomSheetInPeek(true);
+ if (isAdded()) {
+ dismissBottomSheet();
+ }
+ } else {
+ MediaManager.enqueue(activityBrowserFuture, media, true);
}
});
});
@@ -327,16 +342,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
chip.setVisibility(View.VISIBLE);
chip.setOnClickListener(v -> {
- if (assetLink != null) {
- ((MainActivity) requireActivity()).openAssetLink(assetLink);
- }
+ ((MainActivity) requireActivity()).openAssetLink(assetLink);
});
chip.setOnLongClickListener(v -> {
- if (assetLink != null) {
- AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
- Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
- }
+ AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
+ Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
return true;
});
@@ -397,4 +408,5 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private void refreshShares() {
homeViewModel.refreshShares(requireActivity());
}
+
}
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..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,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
diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt
index c40821ac..542adc8f 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt
@@ -82,6 +82,7 @@ object Preferences {
private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
private const val SORT_SEARCH_CHRONOLOGICALLY= "sort_search_chronologically"
+ private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
@JvmStatic
fun getServer(): String? {
@@ -680,4 +681,14 @@ object Preferences {
fun isSearchSortingChronologicallyEnabled(): Boolean {
return App.getInstance().preferences.getBoolean(SORT_SEARCH_CHRONOLOGICALLY, false)
}
+
+ @JvmStatic
+ fun getArtistDisplayBiography(): Boolean {
+ return App.getInstance().preferences.getBoolean(ARTIST_DISPLAY_BIOGRAPHY, true)
+ }
+
+ @JvmStatic
+ fun setArtistDisplayBiography(displayBiographyEnabled: Boolean) {
+ App.getInstance().preferences.edit().putBoolean(ARTIST_DISPLAY_BIOGRAPHY, displayBiographyEnabled).apply()
+ }
}
\ No newline at end of file
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..3b7b0999 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,13 @@ import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LifecycleOwner;
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;
@@ -24,6 +27,7 @@ import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.NetworkUtil;
import com.cappielloantonio.tempo.util.Preferences;
+import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@@ -33,8 +37,8 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
private final ArtistRepository artistRepository;
private final FavoriteRepository favoriteRepository;
private final SharingRepository sharingRepository;
-
private AlbumID3 album;
+ private final MutableLiveData> instantMix = new MutableLiveData<>(null);
public AlbumBottomSheetViewModel(@NonNull Application application) {
super(application);
@@ -116,6 +120,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()) {
@@ -129,4 +134,12 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
});
}
}
+
+ public LiveData> getAlbumInstantMix(LifecycleOwner owner, AlbumID3 album) {
+ instantMix.setValue(Collections.emptyList());
+
+ albumRepository.getInstantMix(album, 20).observe(owner, instantMix::postValue);
+
+ return instantMix;
+ }
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistBottomSheetViewModel.java
index 2c008d80..0df56246 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistBottomSheetViewModel.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistBottomSheetViewModel.java
@@ -4,7 +4,12 @@ import android.app.Application;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.interfaces.StarCallback;
@@ -17,6 +22,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences;
+import java.util.Collections;
import java.util.Date;
import java.util.stream.Collectors;
import java.util.List;
@@ -24,6 +30,7 @@ import java.util.List;
public class ArtistBottomSheetViewModel extends AndroidViewModel {
private final ArtistRepository artistRepository;
private final FavoriteRepository favoriteRepository;
+ private final MutableLiveData> instantMix = new MutableLiveData<>(null);
private ArtistID3 artist;
@@ -95,6 +102,7 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
Log.d("ArtistSync", "Starting artist sync for: " + artist.getName());
artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() {
+ @OptIn(markerClass = UnstableApi.class)
@Override
public void onSongsCollected(List songs) {
Log.d("ArtistSync", "Callback triggered with songs: " + (songs != null ? songs.size() : 0));
@@ -114,5 +122,12 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
Log.d("ArtistSync", "Artist sync preference is disabled");
}
}
- ///
+
+ public LiveData> getArtistInstantMix(LifecycleOwner owner, ArtistID3 artist) {
+ instantMix.setValue(Collections.emptyList());
+
+ artistRepository.getInstantMix(artist, 20).observe(owner, instantMix::postValue);
+
+ return instantMix;
+ }
}
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 {
}
}
});
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..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,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> 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;
}
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..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(), 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/PodcastChannelBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java
index fe2b6ac9..423e3dd3 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java
@@ -1,14 +1,24 @@
package com.cappielloantonio.tempo.viewmodel;
import android.app.Application;
+import android.util.Log;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import com.cappielloantonio.tempo.repository.PodcastRepository;
+import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.PodcastChannel;
+import java.io.IOException;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
public class PodcastChannelBottomSheetViewModel extends AndroidViewModel {
+ private static final String TAG = "PodcastChannelBottomSheetViewModel";
private final PodcastRepository podcastRepository;
private PodcastChannel podcastChannel;
@@ -28,6 +38,59 @@ public class PodcastChannelBottomSheetViewModel extends AndroidViewModel {
}
public void deletePodcastChannel() {
- if (podcastChannel != null) podcastRepository.deletePodcastChannel(podcastChannel.getId());
+ if (podcastChannel != null && podcastChannel.getId() != null) {
+ podcastRepository.deletePodcastChannel(podcastChannel.getId())
+ .enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.code() == 501) {
+ Toast.makeText(getApplication(),
+ "Podcasts are not supported by this server",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ if (response.isSuccessful() && response.body() != null) {
+ ApiResponse apiResponse = response.body();
+
+ String status = apiResponse.subsonicResponse.getStatus();
+
+ if ("ok".equals(status)) {
+ Toast.makeText(getApplication(),
+ "Podcast channel deleted",
+ Toast.LENGTH_SHORT).show();
+ //TODO refresh the UI after deleting
+ //podcastRepository.refreshPodcasts();
+ }
+ } else {
+ handleHttpError(response);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ Toast.makeText(getApplication(),
+ "Network error: " + t.getMessage(),
+ Toast.LENGTH_LONG).show();
+ }
+ });
+ }
}
+
+ private void handleHttpError(Response response) {
+ String errorMsg = "HTTP error: " + response.code();
+ if (response.errorBody() != null) {
+ try {
+ String serverMsg = response.errorBody().string();
+ if (!serverMsg.isEmpty()) {
+ errorMsg += " - " + serverMsg;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading error body", e);
+ }
+ }
+
+ Toast.makeText(getApplication(), errorMsg, Toast.LENGTH_LONG).show();
+ }
+
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java
index 759da750..573affda 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java
@@ -1,27 +1,99 @@
package com.cappielloantonio.tempo.viewmodel;
import android.app.Application;
+import android.util.Log;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.repository.PodcastRepository;
-import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
+import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
+
+import java.io.IOException;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
public class PodcastChannelEditorViewModel extends AndroidViewModel {
- private static final String TAG = "RadioEditorViewModel";
+ private static final String TAG = "PodcastChannelEditorViewModel";
private final PodcastRepository podcastRepository;
- private InternetRadioStation toEdit;
+ private final MutableLiveData isSuccess = new MutableLiveData<>(false);
+ private final MutableLiveData errorMessage = new MutableLiveData<>();
public PodcastChannelEditorViewModel(@NonNull Application application) {
super(application);
-
podcastRepository = new PodcastRepository();
}
- public void createChannel(String url) {
- podcastRepository.createPodcastChannel(url);
+ public LiveData getIsSuccess() {
+ return isSuccess;
}
-}
+
+ public LiveData getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void clearError() {
+ errorMessage.setValue(null);
+ }
+
+ public void createChannel(String url) {
+ errorMessage.setValue(null);
+ isSuccess.setValue(false);
+
+ podcastRepository.createPodcastChannel(url)
+ .enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.code() == 501) {
+ showError(getApplication().getString(R.string.podcast_channel_not_supported_snackbar));
+ return;
+ }
+
+ if (response.isSuccessful() && response.body() != null) {
+ ApiResponse apiResponse = response.body();
+
+ String status = apiResponse.subsonicResponse.getStatus();
+ if ("ok".equals(status)) {
+ isSuccess.setValue(true);
+ }
+ } else {
+ handleHttpError(response);
+ }
+ }
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ showError("Network error: " + t.getMessage());
+ Log.e(TAG, "Network error", t);
+ }
+ });
+ }
+
+ private void handleHttpError(Response response) {
+ String errorMsg = "HTTP error: " + response.code();
+ if (response.errorBody() != null) {
+ try {
+ String serverMsg = response.errorBody().string();
+ if (!serverMsg.isEmpty()) {
+ errorMsg += " - " + serverMsg;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading error body", e);
+ }
+ }
+ showError(errorMsg);
+ }
+
+ private void showError(String message) {
+ Toast.makeText(getApplication(), message, Toast.LENGTH_LONG).show();
+ errorMessage.setValue(message);
+ Log.e(TAG, "Error shown: " + message);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java
index c15ea93f..871b0906 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java
@@ -1,26 +1,47 @@
package com.cappielloantonio.tempo.viewmodel;
import android.app.Application;
+import android.util.Log;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.repository.RadioRepository;
+import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
+import java.io.IOException;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
public class RadioEditorViewModel extends AndroidViewModel {
private static final String TAG = "RadioEditorViewModel";
private final RadioRepository radioRepository;
-
private InternetRadioStation toEdit;
+
+ private final MutableLiveData isSuccess = new MutableLiveData<>(false);
+ private final MutableLiveData errorMessage = new MutableLiveData<>();
public RadioEditorViewModel(@NonNull Application application) {
super(application);
-
radioRepository = new RadioRepository();
}
+
+ public LiveData getIsSuccess() { return isSuccess; }
+ public LiveData getErrorMessage() { return errorMessage; }
+
+ public void clearError() {
+ errorMessage.setValue(null);
+ }
+
public InternetRadioStation getRadioToEdit() {
return toEdit;
}
@@ -30,14 +51,120 @@ public class RadioEditorViewModel extends AndroidViewModel {
}
public void createRadio(String name, String streamURL, String homepageURL) {
- radioRepository.createInternetRadioStation(name, streamURL, homepageURL);
+ errorMessage.setValue(null);
+ isSuccess.setValue(false);
+
+ radioRepository.createInternetRadioStation(name, streamURL, homepageURL)
+ .enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ // Handle HTTP 501 (Not Implemented) from Navidrome
+ if (response.code() == 501) {
+ showError(getApplication().getString(R.string.radio_dialog_not_supported_snackbar));
+ return;
+ }
+ if (response.isSuccessful() && response.body() != null) {
+ ApiResponse apiResponse = response.body();
+ String status = apiResponse.subsonicResponse.getStatus();
+ if ("ok".equals(status)) {
+ isSuccess.setValue(true);
+ } else if ("failed".equals(status)) {
+ handleFailedResponse(apiResponse);
+ }
+ } else {
+ errorMessage.setValue("HTTP error: " + response.code());
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ errorMessage.setValue("Network error: " + t.getMessage());
+ }
+ });
}
public void updateRadio(String name, String streamURL, String homepageURL) {
- if (toEdit != null) radioRepository.updateInternetRadioStation(toEdit.getId(), name, streamURL, homepageURL);
+ if (toEdit != null && toEdit.getId() != null) {
+ errorMessage.setValue(null);
+ isSuccess.setValue(false);
+
+ radioRepository.updateInternetRadioStation(toEdit.getId(), name, streamURL, homepageURL)
+ .enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful() && response.body() != null) {
+ ApiResponse apiResponse = response.body();
+ if (apiResponse.subsonicResponse != null) {
+ String status = apiResponse.subsonicResponse.getStatus();
+ if ("ok".equals(status)) {
+ isSuccess.setValue(true);
+ } else if ("failed".equals(status)) {
+ handleFailedResponse(apiResponse);
+ }
+ }
+ } else {
+ errorMessage.setValue("HTTP error: " + response.code());
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ errorMessage.setValue("Network error: " + t.getMessage());
+ }
+ });
+ }
}
public void deleteRadio() {
- if (toEdit != null) radioRepository.deleteInternetRadioStation(toEdit.getId());
+ if (toEdit != null && toEdit.getId() != null) {
+ errorMessage.setValue(null);
+ isSuccess.setValue(false);
+
+ radioRepository.deleteInternetRadioStation(toEdit.getId())
+ .enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful() && response.body() != null) {
+ ApiResponse apiResponse = response.body();
+
+ String status = apiResponse.subsonicResponse.getStatus();
+
+ if ("ok".equals(status)) {
+ isSuccess.setValue(true);
+ } else if ("failed".equals(status)) {
+ handleFailedResponse(apiResponse);
+ }
+ } else {
+ errorMessage.setValue("HTTP error: " + response.code());
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ errorMessage.setValue("Network error: " + t.getMessage());
+ }
+ });
+ }
}
-}
+
+ private void showError(String message) {
+ Toast.makeText(getApplication(), message, Toast.LENGTH_LONG).show();
+ errorMessage.setValue(message);
+ }
+
+ private void handleFailedResponse(ApiResponse apiResponse) {
+ String errorMsg = "Unknown server error";
+
+ if (apiResponse.subsonicResponse.getError() != null) {
+ errorMsg = apiResponse.subsonicResponse.getError().getMessage();
+
+ if ("Not implemented".equals(errorMsg)) {
+ errorMsg = getApplication().getString((R.string.radio_dialog_not_supported_snackbar));
+ }
+ }
+
+ Toast.makeText(getApplication(), errorMsg, Toast.LENGTH_LONG).show();
+
+ errorMessage.setValue(errorMsg);
+ }
+}
\ No newline at end of file
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..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;
@@ -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> 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 shareTrack() {
return sharingRepository.createShare(song.getId(), song.getTitle(), null);
}
diff --git a/app/src/main/res/layout/fragment_artist_page.xml b/app/src/main/res/layout/fragment_artist_page.xml
index 5cebfaf5..6712dd2e 100644
--- a/app/src/main/res/layout/fragment_artist_page.xml
+++ b/app/src/main/res/layout/fragment_artist_page.xml
@@ -124,6 +124,19 @@
android:textOff=""
android:textOn="" />
+