From f5f039fd62cb906ae6f3e3d57721b61fb68f3953 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sat, 23 May 2026 16:52:00 -0700 Subject: [PATCH 1/2] chore: updated change log --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 682f2dcf6..c2444d810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog -## Pending changes (Pre-Release) +## [4.17.0](https://github.com/eddyizm/tempus/releases/tag/v4.17.0) (2026-05-23) +## What's Changed * feat: handle crashes gracefully by @tvillega in https://github.com/eddyizm/tempus/pull/611 * feat: add Starred bundle for Android Auto by @MaFo-28 in https://github.com/eddyizm/tempus/pull/614 * fix: quick actions visibility state not checked by @tvillega in https://github.com/eddyizm/tempus/pull/621 @@ -21,6 +22,12 @@ * fix: npe if playlist playback happens before data fetching is done by @tvillega in https://github.com/eddyizm/tempus/pull/690 * Revert Issue600 - Slow loading of long playlists (#627) by @eddyizm in https://github.com/eddyizm/tempus/pull/703 +## New Contributors +* @pLum0 made their first contribution in https://github.com/eddyizm/tempus/pull/631 +* @REDGROUL made their first contribution in https://github.com/eddyizm/tempus/pull/635 +* @OlivierGenez made their first contribution in https://github.com/eddyizm/tempus/pull/651 + +**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.16.0...v4.17.0 ## [4.16.0](https://github.com/eddyizm/tempus/releases/tag/v4.16.0) (2026-05-06) ## What's Changed From 4a12e835fd3c66b8a5a92bd1d205f445703b1e4d Mon Sep 17 00:00:00 2001 From: willem Date: Fri, 29 May 2026 15:15:45 +0200 Subject: [PATCH 2/2] feat: use Navidrome coverArt API for internet radio artwork Load radio station images from the Subsonic coverArt field (ra-* IDs via getCoverArt) instead of encoding homepage URLs. Updates the radio list, player, offline cache, and Android Auto mapping from PR #352. Co-authored-by: Cursor --- .../tempo/database/AppDatabase.java | 3 +- .../tempo/model/InternetRadioStationCache.kt | 4 +++ .../tempo/model/SessionMediaItem.kt | 12 +------- .../provider/AlbumArtContentProvider.java | 12 +------- .../subsonic/models/InternetRadioStation.kt | 4 ++- .../adapter/InternetRadioStationAdapter.java | 7 +---- .../fragment/PlayerBottomSheetFragment.java | 7 ++++- .../ui/fragment/PlayerCoverFragment.java | 7 ++++- .../tempo/util/MappingUtil.java | 29 ++++++++----------- 9 files changed, 36 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java index 63e571274..afd1871ee 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java +++ b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java @@ -35,7 +35,7 @@ @UnstableApi @Database( - version = 17, + version = 18, entities = { Queue.class, Server.class, @@ -57,6 +57,7 @@ @AutoMigration(from = 14, to = 15), @AutoMigration(from = 15, to = 16), @AutoMigration(from = 16, to = 17), + @AutoMigration(from = 17, to = 18), } ) @TypeConverters({DateConverters.class, StringListConverter.class}) diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/InternetRadioStationCache.kt b/app/src/main/java/com/cappielloantonio/tempo/model/InternetRadioStationCache.kt index 333ea0087..3b6a9b381 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/model/InternetRadioStationCache.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/model/InternetRadioStationCache.kt @@ -18,12 +18,15 @@ class InternetRadioStationCache( var streamUrl: String? = null, @ColumnInfo(name = "home_page_url") var homePageUrl: String? = null, + @ColumnInfo(name = "cover_art") + var coverArtId: String? = null, ) { constructor(station: InternetRadioStation) : this( id = station.id ?: "", name = station.name, streamUrl = station.streamUrl, homePageUrl = station.homePageUrl, + coverArtId = station.coverArtId, ) fun toInternetRadioStation(): InternetRadioStation { @@ -32,6 +35,7 @@ class InternetRadioStationCache( name = name, streamUrl = streamUrl, homePageUrl = homePageUrl, + coverArtId = coverArtId, ) } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt index e01e7e734..e1fe1f4d7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt @@ -1,6 +1,5 @@ package com.cappielloantonio.tempo.model -import android.content.ContentResolver import android.net.Uri import android.os.Bundle import androidx.annotation.Keep @@ -13,7 +12,6 @@ import androidx.media3.common.util.UnstableApi import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.cappielloantonio.tempo.glide.CustomGlideRequest import com.cappielloantonio.tempo.provider.AlbumArtContentProvider import androidx.room.Embedded import com.cappielloantonio.tempo.subsonic.models.Child @@ -202,15 +200,7 @@ class SessionMediaItem() { title = internetRadioStation.name streamUrl = internetRadioStation.streamUrl type = Constants.MEDIA_TYPE_RADIO - - val homePageUrl = internetRadioStation.homePageUrl - if (homePageUrl != null && homePageUrl.isNotEmpty() && MusicUtil.isImageUrl(homePageUrl)) { - val encodedUrl = android.util.Base64.encodeToString( - homePageUrl.toByteArray(java.nio.charset.StandardCharsets.UTF_8), - android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP - ) - coverArtId = "ir_$encodedUrl" - } + coverArtId = internetRadioStation.coverArtId } fun getMediaItem(): MediaItem { diff --git a/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java b/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java index 5063b3a48..f455adafe 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java +++ b/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java @@ -8,8 +8,6 @@ import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; -import android.util.Base64; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -54,15 +52,7 @@ public static Uri contentUri(String artworkId) { public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { Context context = getContext(); String albumId = uri.getLastPathSegment(); - Uri artworkUri; - - if (albumId != null && albumId.startsWith("ir_")) { - String encodedUrl = albumId.substring("ir_".length()); - String decodedUrl = new String(Base64.decode(encodedUrl, Base64.URL_SAFE | Base64.NO_WRAP)); - artworkUri = Uri.parse(decodedUrl); - } else { - artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize())); - } + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize())); try { // use pipe to communicate between background thread and caller of openFile() diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt index dd82d247f..1bf30750c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt @@ -13,4 +13,6 @@ class InternetRadioStation( var streamUrl: String? = null, @SerializedName("homePageUrl", alternate = ["homepageUrl"]) var homePageUrl: String? = null, -) : Parcelable \ No newline at end of file + @SerializedName("coverArt") + var coverArtId: String? = null, +) : Parcelable diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/InternetRadioStationAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/InternetRadioStationAdapter.java index a9db373b1..239a2e5ec 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/InternetRadioStationAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/InternetRadioStationAdapter.java @@ -42,13 +42,8 @@ public void onBindViewHolder(ViewHolder holder, int position) { holder.item.internetRadioStationTitleTextView.setText(internetRadioStation.getName()); holder.item.internetRadioStationSubtitleTextView.setText(internetRadioStation.getStreamUrl()); - String imageId = internetRadioStation.getHomePageUrl(); - if (imageId == null || imageId.isEmpty()) { - imageId = internetRadioStation.getStreamUrl(); - } - CustomGlideRequest.Builder - .from(holder.itemView.getContext(), imageId, CustomGlideRequest.ResourceType.Radio) + .from(holder.itemView.getContext(), internetRadioStation.getCoverArtId(), CustomGlideRequest.ResourceType.Radio) .build() .into(holder.item.internetRadioStationCoverImageView); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java index c02d7404a..4b338f753 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java @@ -218,8 +218,13 @@ private void setMetadata(MediaMetadata mediaMetadata) { : View.GONE); } + String coverArtId = mediaMetadata.extras.getString("coverArtId"); + CustomGlideRequest.ResourceType resourceType = Objects.equals(type, Constants.MEDIA_TYPE_RADIO) + ? CustomGlideRequest.ResourceType.Radio + : CustomGlideRequest.ResourceType.Song; + CustomGlideRequest.Builder - .from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song) + .from(requireContext(), coverArtId, resourceType) .build() .into(bind.playerHeaderLayout.playerHeaderMediaCoverImage); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java index 2d73847ea..887e0f022 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java @@ -195,8 +195,13 @@ public void onMediaMetadataChanged(@NonNull MediaMetadata mediaMetadata) { } private void setCover(MediaMetadata mediaMetadata) { + String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null; + CustomGlideRequest.ResourceType resourceType = Constants.MEDIA_TYPE_RADIO.equals(type) + ? CustomGlideRequest.ResourceType.Radio + : CustomGlideRequest.ResourceType.Song; + CustomGlideRequest.Builder - .from(requireContext(), mediaMetadata.extras != null ? mediaMetadata.extras.getString("coverArtId") : null, CustomGlideRequest.ResourceType.Song) + .from(requireContext(), mediaMetadata.extras != null ? mediaMetadata.extras.getString("coverArtId") : null, resourceType) .build() .into(bind.nowPlayingSongCoverImageView); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java index ef57b784b..6de1eef2b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java @@ -4,7 +4,6 @@ import android.net.Uri; import android.os.Bundle; import android.util.Log; -import android.util.Base64; import androidx.annotation.OptIn; import androidx.lifecycle.LifecycleOwner; @@ -27,7 +26,6 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.nio.charset.StandardCharsets; @OptIn(markerClass = UnstableApi.class) public class MappingUtil { @@ -249,30 +247,27 @@ public static MediaItem mapDownload(Child media) { public static MediaItem mapInternetRadioStation(InternetRadioStation internetRadioStation) { Uri uri = Uri.parse(internetRadioStation.getStreamUrl()); - Uri artworkUri = null; - String homePageUrl = internetRadioStation.getHomePageUrl(); - String coverArtId = null; - - if (homePageUrl != null && !homePageUrl.isEmpty() && MusicUtil.isImageUrl(homePageUrl)) { - String encodedUrl = Base64.encodeToString(homePageUrl.getBytes(StandardCharsets.UTF_8), - Base64.URL_SAFE | Base64.NO_WRAP); - coverArtId = "ir_" + encodedUrl; - artworkUri = AlbumArtContentProvider.contentUri(coverArtId); - } + String coverArtId = internetRadioStation.getCoverArtId(); + Uri artworkUri = (coverArtId != null && !coverArtId.isEmpty()) + ? AlbumArtContentProvider.contentUri(coverArtId) + : null; + + // `MediaItem.setMediaId()` requires a non-null id; different Subsonic servers may return null here. + String radioId = internetRadioStation.getId(); + if (radioId == null || radioId.isEmpty()) radioId = internetRadioStation.getStreamUrl(); + if (radioId == null || radioId.isEmpty()) radioId = internetRadioStation.getName(); + if (radioId == null) radioId = "radio"; Bundle bundle = new Bundle(); - bundle.putString("id", internetRadioStation.getId()); + bundle.putString("id", radioId); bundle.putString("title", internetRadioStation.getName()); bundle.putString("stationName", internetRadioStation.getName()); bundle.putString("uri", uri.toString()); bundle.putString("type", Constants.MEDIA_TYPE_RADIO); bundle.putString("coverArtId", coverArtId); - if (homePageUrl != null) { - bundle.putString("homepageUrl", homePageUrl); - } return new MediaItem.Builder() - .setMediaId(internetRadioStation.getId()) + .setMediaId(radioId) .setMediaMetadata( new MediaMetadata.Builder() .setTitle(internetRadioStation.getName())