mirror of
https://github.com/antebudimir/tempus.git
synced 2026-04-15 16:27:26 +00:00
feat: radio metadata (#352)
* feat: support dynamic metadata for internet radio stations - Implemented `onMetadata` in `BaseMediaService` to extract "Artist - Title" info from ICY, ID3, and Vorbis streams. - Added a fallback mechanism to periodically check HTTP headers (e.g., `icy-name`, `StreamTitle`) for radio metadata. - Updated `PlayerControllerFragment` and `TrackInfoDialog` to display the station name alongside dynamic track information. - Enhanced `TrackInfoDialog` layout to include a dedicated "Station" field for radio tracks. - Modified `MappingUtil` to preserve station names in media metadata extras. * fix crashing issue * radio bob metadata works now. fix crashing issue * Fixing unchecked operation warnings in SongHorizontalAdapter.java. * optimizing a bit and better format for notification * removed xml files affecting build and enviroment * removed xml files affecting build and enviroment * fix ui internet radio bottomview * Revert "fix ui internet radio bottomview" This reverts commit c237ed451ff436f4be964084b144d94f1ad37668. * rebased to upstream/development and fixed metadata to show up for radio after the rebase * misc.xml restored * Apply suggestion from @eddyizm --------- Co-authored-by: eddyizm <wtfisup@hotmail.com> Co-authored-by: eddyizm <eddyizm@gmail.com>
This commit is contained in:
parent
dbd32baa12
commit
661346ca3a
9 changed files with 479 additions and 27 deletions
|
|
@ -24,6 +24,9 @@ import androidx.media3.exoplayer.source.MediaSource
|
|||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import androidx.media3.extractor.metadata.icy.IcyInfo
|
||||
import androidx.media3.extractor.metadata.id3.TextInformationFrame
|
||||
import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
|
|
@ -32,6 +35,12 @@ import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
|||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val TAG = "BaseMediaService"
|
||||
|
||||
|
|
@ -70,6 +79,13 @@ open class BaseMediaService : MediaLibraryService() {
|
|||
}
|
||||
}
|
||||
|
||||
private val radioHeaderCheckExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
|
||||
private var radioHeaderCheckScheduled = false
|
||||
private var radioHeaderCheckFuture: ScheduledFuture<*>? = null
|
||||
private val radioHeaderCheckRunnable = Runnable {
|
||||
checkRadioHttpHeaders()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
open fun playerInitHook() {
|
||||
|
|
@ -120,6 +136,9 @@ open class BaseMediaService : MediaLibraryService() {
|
|||
updateWidget(player)
|
||||
}
|
||||
|
||||
private var lastRadioArtist: String? = null
|
||||
private var lastRadioTitle: String? = null
|
||||
|
||||
fun initializePlayerListener(player: Player) {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
|
|
@ -129,6 +148,16 @@ open class BaseMediaService : MediaLibraryService() {
|
|||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
|
||||
// Restart header checks for radio streams when media item changes
|
||||
val mediaType = mediaItem.mediaMetadata.extras?.getString("type")
|
||||
if (mediaType == Constants.MEDIA_TYPE_RADIO && player.isPlaying) {
|
||||
stopRadioHeaderChecks()
|
||||
scheduleRadioHeaderChecks()
|
||||
} else if (mediaType != Constants.MEDIA_TYPE_RADIO) {
|
||||
stopRadioHeaderChecks()
|
||||
}
|
||||
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
|
|
@ -170,6 +199,96 @@ open class BaseMediaService : MediaLibraryService() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onMetadata(metadata: Metadata) {
|
||||
// Handle streaming metadata (ICY, ID3) for radio / streaming content
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val extras = currentItem.mediaMetadata.extras
|
||||
if (extras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
var artist: String? = null
|
||||
var title: String? = null
|
||||
|
||||
// Extract metadata from ICY/ID3/Vorbis
|
||||
for (i in 0 until metadata.length()) {
|
||||
when (val entry = metadata[i]) {
|
||||
is IcyInfo -> {
|
||||
entry.title?.let { icyTitle ->
|
||||
val parts = icyTitle.split(" - ", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
artist = parts[0].trim().ifEmpty { null }
|
||||
title = parts[1].trim().ifEmpty { null }
|
||||
} else {
|
||||
title = icyTitle.trim().ifEmpty { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
is TextInformationFrame -> {
|
||||
@Suppress("DEPRECATION")
|
||||
val value = entry.value
|
||||
when (entry.id) {
|
||||
"TPE1" -> if (!value.isNullOrBlank()) artist = value
|
||||
"TIT2" -> if (!value.isNullOrBlank()) title = value
|
||||
}
|
||||
}
|
||||
is VorbisComment -> {
|
||||
@Suppress("DEPRECATION")
|
||||
val value = entry.value
|
||||
when (entry.key) {
|
||||
"ARTIST" -> if (!value.isNullOrBlank()) artist = value
|
||||
"TITLE" -> if (!value.isNullOrBlank()) title = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||
if (artist == lastRadioArtist && title == lastRadioTitle) return // Deduplicate
|
||||
|
||||
lastRadioArtist = artist
|
||||
lastRadioTitle = title
|
||||
|
||||
// Stop HTTP header checks since we have embedded metadata
|
||||
stopRadioHeaderChecks()
|
||||
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
if (currentIndex == C.INDEX_UNSET) return
|
||||
|
||||
val metadataBuilder = currentItem.mediaMetadata.buildUpon()
|
||||
val newExtras = Bundle(extras ?: Bundle())
|
||||
|
||||
// Store individual values in extras for UI
|
||||
artist?.let { newExtras.putString("radioArtist", it) }
|
||||
title?.let { newExtras.putString("radioTitle", it) }
|
||||
|
||||
// Get station name (preserve if already set)
|
||||
val stationName = extras?.getString("stationName")
|
||||
?: currentItem.mediaMetadata.title?.toString()
|
||||
?: ""
|
||||
if (stationName.isNotBlank()) {
|
||||
newExtras.putString("stationName", stationName)
|
||||
}
|
||||
|
||||
// Format for notification/player: Title = "Artist - Song", Artist = "Station Name"
|
||||
val formattedTitle = when {
|
||||
!artist.isNullOrBlank() && !title.isNullOrBlank() -> "$artist - $title"
|
||||
!title.isNullOrBlank() -> title
|
||||
!artist.isNullOrBlank() -> artist
|
||||
else -> stationName
|
||||
}
|
||||
|
||||
metadataBuilder.setTitle(formattedTitle)
|
||||
if (stationName.isNotBlank()) {
|
||||
metadataBuilder.setArtist(stationName)
|
||||
}
|
||||
|
||||
(player as? ExoPlayer)?.let { exo ->
|
||||
exo.replaceMediaItem(currentIndex, currentItem.buildUpon()
|
||||
.setMediaMetadata(metadataBuilder.setExtras(newExtras).build())
|
||||
.build())
|
||||
updateWidget(exo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
Log.d(TAG, "onIsPlayingChanged " + player.currentMediaItemIndex)
|
||||
if (!isPlaying) {
|
||||
|
|
@ -182,8 +301,10 @@ open class BaseMediaService : MediaLibraryService() {
|
|||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
scheduleRadioHeaderChecks()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
stopRadioHeaderChecks()
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
|
|
@ -287,6 +408,8 @@ open class BaseMediaService : MediaLibraryService() {
|
|||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
stopRadioHeaderChecks()
|
||||
radioHeaderCheckExecutor.shutdown()
|
||||
releasePlayers()
|
||||
mediaLibrarySession.release()
|
||||
super.onDestroy()
|
||||
|
|
@ -405,6 +528,148 @@ open class BaseMediaService : MediaLibraryService() {
|
|||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun scheduleRadioHeaderChecks() {
|
||||
val player = mediaLibrarySession.player
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val mediaType = currentItem.mediaMetadata.extras?.getString("type")
|
||||
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
if (radioHeaderCheckScheduled) return
|
||||
|
||||
// Check immediately, then periodically
|
||||
checkRadioHttpHeaders()
|
||||
radioHeaderCheckFuture = radioHeaderCheckExecutor.scheduleWithFixedDelay(
|
||||
radioHeaderCheckRunnable,
|
||||
RADIO_HEADER_CHECK_INTERVAL_SECONDS,
|
||||
RADIO_HEADER_CHECK_INTERVAL_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
radioHeaderCheckScheduled = true
|
||||
}
|
||||
|
||||
private fun stopRadioHeaderChecks() {
|
||||
if (!radioHeaderCheckScheduled) return
|
||||
radioHeaderCheckFuture?.cancel(false)
|
||||
radioHeaderCheckFuture = null
|
||||
radioHeaderCheckScheduled = false
|
||||
}
|
||||
|
||||
private fun checkRadioHttpHeaders() {
|
||||
val player = mediaLibrarySession.player
|
||||
val currentItem = player.currentMediaItem ?: return
|
||||
val extras = currentItem.mediaMetadata.extras
|
||||
val mediaType = extras?.getString("type")
|
||||
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
// Skip if we already have embedded metadata (ICY/ID3) - HTTP headers are only fallback
|
||||
val hasEmbeddedMetadata = !currentItem.mediaMetadata.artist.isNullOrBlank() ||
|
||||
!currentItem.mediaMetadata.title.isNullOrBlank() ||
|
||||
(extras != null && !extras.getString("radioArtist").isNullOrBlank()) ||
|
||||
(extras != null && !extras.getString("radioTitle").isNullOrBlank())
|
||||
if (hasEmbeddedMetadata) return
|
||||
|
||||
val streamUrl = extras?.getString("uri") ?: currentItem.requestMetadata.mediaUri?.toString()
|
||||
if (streamUrl.isNullOrBlank()) return
|
||||
|
||||
try {
|
||||
val url = URL(streamUrl)
|
||||
val connection = url.openConnection() as? HttpURLConnection ?: return
|
||||
|
||||
// Only try HEAD request (lightweight) - skip GET fallback as it's unreliable
|
||||
connection.requestMethod = "HEAD"
|
||||
connection.setRequestProperty("Icy-MetaData", "1")
|
||||
connection.setRequestProperty("User-Agent", "Tempus/1.0")
|
||||
connection.connectTimeout = 3000 // Reduced timeout
|
||||
connection.readTimeout = 3000
|
||||
|
||||
connection.connect()
|
||||
|
||||
if (connection.responseCode >= 400) {
|
||||
connection.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for metadata in HTTP headers
|
||||
val streamTitle = connection.getHeaderField("icy-name")
|
||||
?: connection.getHeaderField("StreamTitle")
|
||||
?: connection.getHeaderField("stream-title")
|
||||
|
||||
connection.disconnect()
|
||||
|
||||
if (!streamTitle.isNullOrBlank()) {
|
||||
processStreamTitle(streamTitle, player)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Silently fail - this is a fallback mechanism, ICY metadata is primary
|
||||
}
|
||||
}
|
||||
|
||||
private fun processStreamTitle(streamTitle: String, player: Player) {
|
||||
// Parse "Artist - Title" format
|
||||
val parts = streamTitle.split(" - ", limit = 2)
|
||||
val artist = if (parts.size == 2) parts[0].trim().ifEmpty { null } else null
|
||||
val title = if (parts.size == 2) parts[1].trim().ifEmpty { null } else streamTitle.trim().ifEmpty { null }
|
||||
|
||||
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||
if (artist == lastRadioArtist && title == lastRadioTitle) return // Deduplicate
|
||||
|
||||
lastRadioArtist = artist
|
||||
lastRadioTitle = title
|
||||
|
||||
// Update on main thread
|
||||
widgetUpdateHandler.post {
|
||||
val currentItemNow = player.currentMediaItem ?: return@post
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
if (currentIndex == C.INDEX_UNSET) return@post
|
||||
|
||||
val currentExtras = currentItemNow.mediaMetadata.extras
|
||||
if (currentExtras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return@post
|
||||
|
||||
// Double-check we still don't have embedded metadata (might have arrived since check)
|
||||
val hasEmbeddedMetadata = !currentItemNow.mediaMetadata.artist.isNullOrBlank() ||
|
||||
!currentItemNow.mediaMetadata.title.isNullOrBlank() ||
|
||||
(currentExtras != null && !currentExtras.getString("radioArtist").isNullOrBlank()) ||
|
||||
(currentExtras != null && !currentExtras.getString("radioTitle").isNullOrBlank())
|
||||
if (hasEmbeddedMetadata) return@post
|
||||
|
||||
val metadataBuilder = currentItemNow.mediaMetadata.buildUpon()
|
||||
val newExtras = Bundle(currentExtras ?: Bundle())
|
||||
|
||||
// Store individual values in extras for UI
|
||||
artist?.let { newExtras.putString("radioArtist", it) }
|
||||
title?.let { newExtras.putString("radioTitle", it) }
|
||||
|
||||
// Get station name (preserve if already set)
|
||||
val stationName = currentExtras?.getString("stationName")
|
||||
?: currentItemNow.mediaMetadata.title?.toString()
|
||||
?: ""
|
||||
if (stationName.isNotBlank()) {
|
||||
newExtras.putString("stationName", stationName)
|
||||
}
|
||||
|
||||
// Format for notification/player: Title = "Artist - Song", Artist = "Station Name"
|
||||
val formattedTitle = when {
|
||||
!artist.isNullOrBlank() && !title.isNullOrBlank() -> "$artist - $title"
|
||||
!title.isNullOrBlank() -> title
|
||||
!artist.isNullOrBlank() -> artist
|
||||
else -> stationName
|
||||
}
|
||||
|
||||
metadataBuilder.setTitle(formattedTitle)
|
||||
if (stationName.isNotBlank()) {
|
||||
metadataBuilder.setArtist(stationName)
|
||||
}
|
||||
metadataBuilder.setExtras(newExtras)
|
||||
|
||||
(player as? ExoPlayer)?.let { exo ->
|
||||
exo.replaceMediaItem(currentIndex, currentItemNow.buildUpon()
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.build())
|
||||
updateWidget(exo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
|
|
@ -595,4 +860,5 @@ open class BaseMediaService : MediaLibraryService() {
|
|||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
private const val RADIO_HEADER_CHECK_INTERVAL_SECONDS = 30L // Reduced frequency - only fallback when ICY fails
|
||||
|
||||
|
|
|
|||
|
|
@ -61,13 +61,47 @@ public class TrackInfoDialog extends DialogFragment {
|
|||
private void setTrackInfo() {
|
||||
genreLink = null;
|
||||
yearLink = null;
|
||||
|
||||
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||
boolean isRadio = Objects.equals(type, Constants.MEDIA_TYPE_RADIO);
|
||||
|
||||
if (isRadio) {
|
||||
// For radio: always read from extras first (radioArtist, radioTitle, stationName)
|
||||
// MediaMetadata.title/artist are formatted for notification
|
||||
String stationName = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
|
||||
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
|
||||
|
||||
String artist = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioArtist", "")
|
||||
: "";
|
||||
|
||||
String title = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioTitle", "")
|
||||
: "";
|
||||
|
||||
// Format: "Artist - Song" or fallback to title or station name
|
||||
String mainTitle;
|
||||
if (!android.text.TextUtils.isEmpty(artist) && !android.text.TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!android.text.TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!android.text.TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
bind.trakTitleInfoTextView.setText(mainTitle);
|
||||
bind.trakArtistInfoTextView.setText(stationName);
|
||||
} else {
|
||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||
bind.trakArtistInfoTextView.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
}
|
||||
|
||||
if (mediaMetadata.extras != null) {
|
||||
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
|
||||
|
|
@ -91,6 +125,27 @@ public class TrackInfoDialog extends DialogFragment {
|
|||
String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder));
|
||||
int yearValue = mediaMetadata.extras.getInt("year", 0);
|
||||
|
||||
// Handle radio-specific metadata
|
||||
if (isRadio) {
|
||||
String stationName = mediaMetadata.extras.getString("stationName", getString(R.string.label_placeholder));
|
||||
String radioArtist = mediaMetadata.extras.getString("radioArtist", "");
|
||||
String radioTitle = mediaMetadata.extras.getString("radioTitle", "");
|
||||
|
||||
// Show station name in station section
|
||||
bind.stationInfoSector.setVisibility(android.view.View.VISIBLE);
|
||||
bind.stationValueSector.setText(stationName);
|
||||
|
||||
// Use radio metadata for title/artist if available
|
||||
if (!android.text.TextUtils.isEmpty(radioTitle)) {
|
||||
titleValue = radioTitle;
|
||||
}
|
||||
if (!android.text.TextUtils.isEmpty(radioArtist)) {
|
||||
artistValue = radioArtist;
|
||||
}
|
||||
} else {
|
||||
bind.stationInfoSector.setVisibility(android.view.View.GONE);
|
||||
}
|
||||
|
||||
if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
|
||||
genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
|||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -173,25 +174,54 @@ public class PlayerBottomSheetFragment extends Fragment {
|
|||
playerBottomSheetViewModel.setLiveArtist(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("artistId"));
|
||||
playerBottomSheetViewModel.setLiveDescription(mediaMetadata.extras.getString("description", null));
|
||||
|
||||
String type = mediaMetadata.extras.getString("type");
|
||||
|
||||
if (Objects.equals(type, Constants.MEDIA_TYPE_RADIO)) {
|
||||
// For radio: keep header consistent with full player
|
||||
String stationName = mediaMetadata.extras.getString(
|
||||
"stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : ""
|
||||
);
|
||||
|
||||
String artist = mediaMetadata.extras.getString("radioArtist", "");
|
||||
String title = mediaMetadata.extras.getString("radioTitle", "");
|
||||
|
||||
String mainTitle;
|
||||
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mainTitle);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(stationName);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(!TextUtils.isEmpty(mainTitle) ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(!TextUtils.isEmpty(stationName) ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
// Default (music, podcast, etc.)
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
: ""
|
||||
);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(
|
||||
mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), "")
|
||||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
}
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)
|
||||
.build()
|
||||
.into(bind.playerHeaderLayout.playerHeaderMediaCoverImage);
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE);
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(
|
||||
(mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), ""))
|
||||
|| (Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null)
|
||||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -215,12 +215,53 @@ public class PlayerControllerFragment extends Fragment {
|
|||
}
|
||||
|
||||
private void setMetadata(MediaMetadata mediaMetadata) {
|
||||
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||
|
||||
if (Objects.equals(type, Constants.MEDIA_TYPE_RADIO)) {
|
||||
// For radio: always read from extras first (radioArtist, radioTitle, stationName)
|
||||
// MediaMetadata.title/artist are formatted for notification
|
||||
String stationName = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("stationName",
|
||||
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
|
||||
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
|
||||
|
||||
String artist = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioArtist", "")
|
||||
: "";
|
||||
|
||||
String title = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioTitle", "")
|
||||
: "";
|
||||
|
||||
// Format: "Artist - Song" or fallback to title or station name
|
||||
String mainTitle;
|
||||
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} else if (!TextUtils.isEmpty(artist)) {
|
||||
mainTitle = artist;
|
||||
} else {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
playerMediaTitleLabel.setText(mainTitle);
|
||||
playerArtistNameLabel.setText(stationName);
|
||||
|
||||
playerMediaTitleLabel.setSelected(true);
|
||||
playerArtistNameLabel.setSelected(true);
|
||||
|
||||
playerMediaTitleLabel.setVisibility(!TextUtils.isEmpty(mainTitle) ? View.VISIBLE : View.GONE);
|
||||
playerArtistNameLabel.setVisibility(!TextUtils.isEmpty(stationName) ? View.VISIBLE : View.GONE);
|
||||
|
||||
updateAssetLinkChips(mediaMetadata);
|
||||
return;
|
||||
}
|
||||
|
||||
playerMediaTitleLabel.setText(String.valueOf(mediaMetadata.title));
|
||||
playerArtistNameLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? String.valueOf(mediaMetadata.artist)
|
||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
|
||||
playerMediaTitleLabel.setSelected(true);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import java.net.CookieHandler;
|
|||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@UnstableApi
|
||||
|
|
@ -78,12 +80,33 @@ public final class DownloadUtil {
|
|||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getHttpDataSourceFactoryForRadio() {
|
||||
CookieManager cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
CookieHandler.setDefault(cookieManager);
|
||||
|
||||
// Create a factory with ICY metadata support for radio streams
|
||||
Map<String, String> defaultRequestProperties = new HashMap<>();
|
||||
defaultRequestProperties.put("Icy-MetaData", "1");
|
||||
defaultRequestProperties.put("User-Agent", "Tempus/1.0");
|
||||
|
||||
return new DefaultHttpDataSource
|
||||
.Factory()
|
||||
.setAllowCrossProtocolRedirects(true)
|
||||
.setDefaultRequestProperties(defaultRequestProperties);
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) {
|
||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getUpstreamDataSourceFactoryForRadio(Context context) {
|
||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactoryForRadio());
|
||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) {
|
||||
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
||||
.setCache(getStreamingCache(context))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.cappielloantonio.tempo.util
|
|||
import android.content.Context
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSource
|
||||
|
|
@ -21,10 +20,15 @@ class DynamicMediaSourceFactory(
|
|||
) : MediaSource.Factory {
|
||||
|
||||
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
||||
val mediaId = mediaItem.mediaId
|
||||
// Detect radio streams in a backwards-compatible way.
|
||||
// Older Tempus versions tagged radio items via MediaMetadata extras
|
||||
// (`type == MEDIA_TYPE_RADIO`), while newer upstream changes use an
|
||||
// "ir-" mediaId prefix. Support BOTH so radio works after rebases.
|
||||
val mediaType = mediaItem.mediaMetadata.extras?.getString("type", "")
|
||||
val isRadio = mediaType == Constants.MEDIA_TYPE_RADIO || mediaItem.mediaId.startsWith("ir-")
|
||||
|
||||
val streamingCacheSize = Preferences.getStreamingCacheSize()
|
||||
val bypassCache = mediaId.startsWith("ir-")
|
||||
val bypassCache = isRadio
|
||||
|
||||
val useUpstream = when {
|
||||
streamingCacheSize.toInt() == 0 -> true
|
||||
|
|
@ -33,7 +37,10 @@ class DynamicMediaSourceFactory(
|
|||
else -> true
|
||||
}
|
||||
|
||||
val dataSourceFactory: DataSource.Factory = if (useUpstream) {
|
||||
val dataSourceFactory: DataSource.Factory = if (bypassCache) {
|
||||
// For radio streams, use a DataSourceFactory with ICY metadata support
|
||||
DownloadUtil.getUpstreamDataSourceFactoryForRadio(context)
|
||||
} else if (useUpstream) {
|
||||
DownloadUtil.getUpstreamDataSourceFactory(context)
|
||||
} else {
|
||||
DownloadUtil.getCacheDataSourceFactory(context)
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ public class MappingUtil {
|
|||
Bundle bundle = new Bundle();
|
||||
bundle.putString("id", internetRadioStation.getId());
|
||||
bundle.putString("title", internetRadioStation.getName());
|
||||
bundle.putString("stationName", internetRadioStation.getName());
|
||||
bundle.putString("uri", uri.toString());
|
||||
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
|
||||
|
||||
|
|
@ -219,6 +220,7 @@ public class MappingUtil {
|
|||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setTitle(internetRadioStation.getName())
|
||||
.setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
|
|
|
|||
|
|
@ -131,6 +131,33 @@
|
|||
android:text="@string/label_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
style="@style/Divider"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/station_info_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/station_key_sector"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="4"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/track_info_station" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/station_value_sector"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="7"
|
||||
android:text="@string/label_placeholder" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
style="@style/Divider"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
|
|
|||
|
|
@ -517,6 +517,7 @@
|
|||
<string name="track_info_summary_transcoding_codec">The application will request the server to transcode the file. The requested codec by the user is %1$s, while the bitrate will be the same as the source file. The potential transcoding of the file into the chosen format is dependent on the server, as it may or may not support the operation.</string>
|
||||
<string name="track_info_title">Title</string>
|
||||
<string name="track_info_track_number">Track number</string>
|
||||
<string name="track_info_station">Station</string>
|
||||
<string name="track_info_transcoded_content_type">Transcoded content type</string>
|
||||
<string name="track_info_transcoded_suffix">Transcoded suffix</string>
|
||||
<string name="track_info_year">Year</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue