From f091b3d248a0b1b4ee67e282f489248f17c75495 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Mon, 13 Oct 2025 21:09:27 -0700 Subject: [PATCH 1/2] fix: handle empty date fields from subsonic json --- README.md | 2 +- .../tempo/repository/AlbumRepository.java | 13 +++++- .../tempo/subsonic/RetrofitClient.kt | 10 ++++- .../subsonic/utils/EmptyDateTypeAdapter.kt | 42 +++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt diff --git a/README.md b/README.md index 1073595b..07bdc1ac 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Please note the two variants in the release assets include release/debug and 32/ As mentioned above, I am working towards a rebrand to get into app stores with a new name an icon. -Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md) +Moved details to [CHANGELOG.md](CHANGELOG.md) Fork [**sponsorship here**](https://ko-fi.com/eddyizm). 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 6dc8d3e3..bcc358b5 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.repository; import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; +import android.util.Log; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.interfaces.DecadesCallback; @@ -31,14 +32,22 @@ public class AlbumRepository { .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) { + if (response.isSuccessful() + && response.body() != null + && response.body().getSubsonicResponse().getAlbumList2() != null + && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) { + listLiveAlbums.setValue(response.body().getSubsonicResponse().getAlbumList2().getAlbums()); + } else { + Log.e("AlbumRepository", "API Error on getAlbums. HTTP Code: " + response.code()); + listLiveAlbums.setValue(new ArrayList<>()); } } @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - + Log.e("AlbumRepository", "Network Failure on getAlbums: " + t.getMessage()); + listLiveAlbums.setValue(new ArrayList<>()); } }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt index f05238ce..3f9868bd 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt @@ -2,22 +2,28 @@ package com.cappielloantonio.tempo.subsonic import com.cappielloantonio.tempo.App import com.cappielloantonio.tempo.subsonic.utils.CacheUtil +import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter import com.google.gson.GsonBuilder import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.util.Date import java.util.concurrent.TimeUnit class RetrofitClient(subsonic: Subsonic) { var retrofit: Retrofit init { + val gson = GsonBuilder() + .registerTypeAdapter(Date::class.java, EmptyDateTypeAdapter()) + .setLenient() + .create() + retrofit = Retrofit.Builder() .baseUrl(subsonic.url) - .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create())) - .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create())) + .addConverterFactory(GsonConverterFactory.create(gson)) .client(getOkHttpClient()) .build() } diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt new file mode 100644 index 00000000..bcdd5ee8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt @@ -0,0 +1,42 @@ +package com.cappielloantonio.tempo.subsonic.utils + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import java.lang.reflect.Type +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +// This adapter handles Date objects, returning null if the JSON string is empty or unparsable. +class EmptyDateTypeAdapter : JsonDeserializer { + + // Define the date formats expected from the Subsonic server. + private val dateFormats: List = listOf( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }, + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }, + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") } + ) + + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Date? { + val jsonString = json.asString.trim() + + if (jsonString.isEmpty()) { + return null + } + + for (format in dateFormats) { + try { + return format.parse(jsonString) + } catch (e: ParseException) { + // Ignore and try the next format + } + } + + return null + } +} \ No newline at end of file From 8c7a25cbd08a3a41ca77d95ab9133ea8b77756ca Mon Sep 17 00:00:00 2001 From: eddyizm Date: Mon, 13 Oct 2025 21:49:41 -0700 Subject: [PATCH 2/2] fix: update to handle nulls in the sort function --- .../ui/adapter/AlbumCatalogueAdapter.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java index 1f360c80..69583a40 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java @@ -20,6 +20,7 @@ import com.cappielloantonio.tempo.util.MusicUtil; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.List; public class AlbumCatalogueAdapter extends RecyclerView.Adapter implements Filterable { @@ -152,12 +153,20 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter album.getName() != null ? album.getName() : "", + String.CASE_INSENSITIVE_ORDER + )); break; case Constants.ALBUM_ORDER_BY_ARTIST: - albums.sort(Comparator.comparing(AlbumID3::getArtist, Comparator.nullsLast(Comparator.naturalOrder()))); + albums.sort(Comparator.comparing( + album -> album.getArtist() != null ? album.getArtist() : "", + String.CASE_INSENSITIVE_ORDER + )); break; case Constants.ALBUM_ORDER_BY_YEAR: albums.sort(Comparator.comparing(AlbumID3::getYear)); @@ -166,15 +175,23 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter album.getCreated() != null ? album.getCreated() : new Date(0), + Comparator.nullsLast(Date::compareTo) + )); Collections.reverse(albums); break; case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED: - albums.sort(Comparator.comparing(AlbumID3::getPlayed)); + albums.sort(Comparator.comparing( + album -> album.getPlayed() != null ? album.getPlayed() : new Date(0), + Comparator.nullsLast(Date::compareTo) + )); Collections.reverse(albums); break; case Constants.ALBUM_ORDER_BY_MOST_PLAYED: - albums.sort(Comparator.comparing(AlbumID3::getPlayCount)); + albums.sort(Comparator.comparing( + album -> album.getPlayCount() != null ? album.getPlayCount() : 0L + )); Collections.reverse(albums); break; }