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.
This commit is contained in:
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -192,7 +192,7 @@
|
||||
</option>
|
||||
</component>
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -23,6 +23,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
|
||||
@@ -31,6 +34,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
|
||||
|
||||
@UnstableApi
|
||||
open class BaseMediaService : MediaLibraryService() {
|
||||
@@ -67,6 +76,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() {
|
||||
@@ -117,6 +133,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) {
|
||||
@@ -126,6 +145,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)
|
||||
}
|
||||
|
||||
@@ -162,6 +191,103 @@ 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
|
||||
val mediaType = extras?.getString("type")
|
||||
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||
|
||||
var artist: String? = null
|
||||
var title: String? = null
|
||||
|
||||
for (i in 0 until metadata.length()) {
|
||||
when (val entry = metadata[i]) {
|
||||
is IcyInfo -> {
|
||||
// Common format: "Artist - Title"
|
||||
val icyTitle = entry.title ?: continue
|
||||
val parts = icyTitle.split(" - ", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
artist = parts[0].trim().ifEmpty { null } ?: artist
|
||||
title = parts[1].trim().ifEmpty { null } ?: title
|
||||
} else {
|
||||
title = icyTitle.trim().ifEmpty { null } ?: title
|
||||
}
|
||||
}
|
||||
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 -> {
|
||||
// OGG Vorbis/Opus metadata
|
||||
@Suppress("DEPRECATION")
|
||||
val value = entry.value
|
||||
when (entry.key) {
|
||||
"ARTIST" -> if (!value.isNullOrBlank()) {
|
||||
artist = value
|
||||
}
|
||||
"TITLE" -> if (!value.isNullOrBlank()) {
|
||||
title = value
|
||||
}
|
||||
"ALBUM" -> {
|
||||
// Store album if needed, but not used for radio display
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||
|
||||
// Deduplicate consecutive identical metadata
|
||||
if (artist == lastRadioArtist && title == lastRadioTitle) return
|
||||
lastRadioArtist = artist
|
||||
lastRadioTitle = title
|
||||
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
if (currentIndex == C.INDEX_UNSET) return
|
||||
|
||||
val metadataBuilder = currentItem.mediaMetadata.buildUpon()
|
||||
val newExtras = Bundle(currentItem.mediaMetadata.extras ?: Bundle())
|
||||
|
||||
artist?.let {
|
||||
metadataBuilder.setArtist(it)
|
||||
newExtras.putString("radioArtist", it)
|
||||
}
|
||||
title?.let {
|
||||
metadataBuilder.setTitle(it)
|
||||
newExtras.putString("radioTitle", it)
|
||||
}
|
||||
|
||||
// Preserve station name separately (fallback to static title if needed)
|
||||
if (!newExtras.containsKey("stationName")) {
|
||||
val stationName =
|
||||
currentItem.mediaMetadata.extras?.getString("stationName")
|
||||
?: currentItem.mediaMetadata.title?.toString()
|
||||
stationName?.let { newExtras.putString("stationName", it) }
|
||||
}
|
||||
|
||||
metadataBuilder.setExtras(newExtras)
|
||||
|
||||
val updatedItem = currentItem.buildUpon()
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.build()
|
||||
|
||||
(player as? ExoPlayer)?.let { exo ->
|
||||
exo.replaceMediaItem(currentIndex, updatedItem)
|
||||
updateWidget(exo)
|
||||
// Media3 notification will automatically update via MediaMetadata changes
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
Log.d(javaClass.toString(), "onIsPlayingChanged " + player.currentMediaItemIndex)
|
||||
if (!isPlaying) {
|
||||
@@ -174,8 +300,10 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
scheduleRadioHeaderChecks()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
stopRadioHeaderChecks()
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
@@ -279,6 +407,8 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
stopRadioHeaderChecks()
|
||||
radioHeaderCheckExecutor.shutdown()
|
||||
releasePlayers()
|
||||
mediaLibrarySession.release()
|
||||
super.onDestroy()
|
||||
@@ -397,6 +527,226 @@ 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
|
||||
|
||||
val streamUrl = extras?.getString("uri") ?: currentItem.requestMetadata.mediaUri?.toString()
|
||||
if (streamUrl.isNullOrBlank()) return
|
||||
|
||||
try {
|
||||
val url = URL(streamUrl)
|
||||
val connection = url.openConnection() as? HttpURLConnection ?: run {
|
||||
Log.d(javaClass.toString(), "Failed to create HTTP connection for: $streamUrl")
|
||||
return
|
||||
}
|
||||
|
||||
// Try HEAD request first (more efficient)
|
||||
connection.requestMethod = "HEAD"
|
||||
connection.setRequestProperty("Icy-MetaData", "1")
|
||||
connection.setRequestProperty("User-Agent", "Tempus/1.0")
|
||||
connection.connectTimeout = 5000
|
||||
connection.readTimeout = 5000
|
||||
|
||||
try {
|
||||
connection.connect()
|
||||
} catch (e: Exception) {
|
||||
Log.d(javaClass.toString(), "HEAD request failed, trying GET: ${e.message}")
|
||||
connection.disconnect()
|
||||
// Fallback to GET request with Range header (some servers don't support HEAD)
|
||||
val getConnection = url.openConnection() as? HttpURLConnection ?: return
|
||||
getConnection.requestMethod = "GET"
|
||||
getConnection.setRequestProperty("Icy-MetaData", "1")
|
||||
getConnection.setRequestProperty("User-Agent", "Tempus/1.0")
|
||||
getConnection.setRequestProperty("Range", "bytes=0-1") // Request minimal data
|
||||
getConnection.connectTimeout = 5000
|
||||
getConnection.readTimeout = 5000
|
||||
|
||||
try {
|
||||
getConnection.connect()
|
||||
val responseCode = getConnection.responseCode
|
||||
if (responseCode >= 400) {
|
||||
Log.d(javaClass.toString(), "GET request failed with code: $responseCode")
|
||||
getConnection.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for various HTTP header formats that contain metadata
|
||||
val streamTitle = getConnection.getHeaderField("icy-name")
|
||||
?: getConnection.getHeaderField("StreamTitle")
|
||||
?: getConnection.getHeaderField("stream-title")
|
||||
?: getConnection.getHeaderField("X-StreamTitle")
|
||||
|
||||
getConnection.inputStream?.close()
|
||||
getConnection.disconnect()
|
||||
|
||||
if (streamTitle.isNullOrBlank()) {
|
||||
Log.d(javaClass.toString(), "No HTTP header metadata found in GET response")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(javaClass.toString(), "Found HTTP header metadata via GET: $streamTitle")
|
||||
processStreamTitle(streamTitle, player)
|
||||
return
|
||||
} catch (e2: Exception) {
|
||||
Log.d(javaClass.toString(), "GET request also failed: ${e2.message}")
|
||||
getConnection.disconnect()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode >= 400) {
|
||||
Log.d(javaClass.toString(), "HEAD request failed with code: $responseCode")
|
||||
connection.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for various HTTP header formats that contain metadata
|
||||
// Radio Bob and similar stations send metadata in HTTP headers like:
|
||||
// - icy-name: "Artist - Song Title"
|
||||
// - StreamTitle: "Artist - Song Title"
|
||||
val streamTitle = connection.getHeaderField("icy-name")
|
||||
?: connection.getHeaderField("StreamTitle")
|
||||
?: connection.getHeaderField("stream-title")
|
||||
?: connection.getHeaderField("X-StreamTitle")
|
||||
|
||||
connection.disconnect()
|
||||
|
||||
if (streamTitle.isNullOrBlank()) {
|
||||
Log.d(javaClass.toString(), "No HTTP header metadata found for radio stream")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(javaClass.toString(), "Found HTTP header metadata via HEAD: $streamTitle")
|
||||
processStreamTitle(streamTitle, player)
|
||||
} catch (e: Exception) {
|
||||
Log.d(javaClass.toString(), "Failed to fetch radio HTTP headers: ${e.message ?: e.javaClass.simpleName}", e)
|
||||
// Silently fail - this is a fallback mechanism
|
||||
}
|
||||
}
|
||||
|
||||
private fun processStreamTitle(streamTitle: String, player: Player) {
|
||||
// Parse the stream title (could be "Artist - Title" or just "Title")
|
||||
// Radio Bob format: "Artist - Song Title"
|
||||
var artist: String? = null
|
||||
val title: String?
|
||||
|
||||
val parts = streamTitle.split(" - ", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
artist = parts[0].trim().ifEmpty { null }
|
||||
title = parts[1].trim().ifEmpty { null }
|
||||
Log.d(javaClass.toString(), "Parsed HTTP metadata - Artist: $artist, Title: $title")
|
||||
} else {
|
||||
title = streamTitle.trim().ifEmpty { null }
|
||||
Log.d(javaClass.toString(), "Parsed HTTP metadata - Title only: $title")
|
||||
}
|
||||
|
||||
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||
|
||||
// Deduplicate consecutive identical metadata
|
||||
if (artist == lastRadioArtist && title == lastRadioTitle) {
|
||||
Log.d(javaClass.toString(), "Skipping duplicate metadata")
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
val currentMediaType = currentExtras?.getString("type")
|
||||
if (currentMediaType != Constants.MEDIA_TYPE_RADIO) return@post
|
||||
|
||||
// Check if we already have metadata from embedded sources (ICY, ID3, etc.)
|
||||
// HTTP headers are used as fallback when embedded metadata is not available
|
||||
val hasEmbeddedMetadata = !currentItemNow.mediaMetadata.artist.isNullOrBlank() ||
|
||||
!currentItemNow.mediaMetadata.title.isNullOrBlank() ||
|
||||
(currentExtras != null && !currentExtras.getString("radioArtist").isNullOrBlank()) ||
|
||||
(currentExtras != null && !currentExtras.getString("radioTitle").isNullOrBlank())
|
||||
|
||||
// Only use HTTP header metadata if we don't have embedded metadata
|
||||
// This preserves the original way while adding HTTP header support as fallback
|
||||
if (!hasEmbeddedMetadata) {
|
||||
Log.d(javaClass.toString(), "Updating radio metadata from HTTP headers - Artist: $artist, Title: $title")
|
||||
lastRadioArtist = artist
|
||||
lastRadioTitle = title
|
||||
|
||||
val metadataBuilder = currentItemNow.mediaMetadata.buildUpon()
|
||||
val newExtras = if (currentExtras != null) {
|
||||
Bundle(currentExtras)
|
||||
} else {
|
||||
Bundle()
|
||||
}
|
||||
|
||||
// Set artist and title in MediaMetadata
|
||||
// The UI will read these and display "Artist - Title" in the main title label
|
||||
artist?.let {
|
||||
metadataBuilder.setArtist(it)
|
||||
newExtras.putString("radioArtist", it)
|
||||
}
|
||||
title?.let {
|
||||
metadataBuilder.setTitle(it)
|
||||
newExtras.putString("radioTitle", it)
|
||||
}
|
||||
|
||||
// Preserve station name separately (shown in artist label)
|
||||
if (!newExtras.containsKey("stationName")) {
|
||||
val stationName = currentExtras?.getString("stationName")
|
||||
?: currentItemNow.mediaMetadata.title?.toString()
|
||||
stationName?.let { newExtras.putString("stationName", it) }
|
||||
}
|
||||
|
||||
metadataBuilder.setExtras(newExtras)
|
||||
|
||||
val updatedItem = currentItemNow.buildUpon()
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.build()
|
||||
|
||||
(player as? ExoPlayer)?.let { exo ->
|
||||
// replaceMediaItem triggers onMediaMetadataChanged in UI listeners
|
||||
// This will update the player display automatically
|
||||
exo.replaceMediaItem(currentIndex, updatedItem)
|
||||
updateWidget(exo)
|
||||
Log.d(javaClass.toString(), "Radio metadata updated in player")
|
||||
}
|
||||
} else {
|
||||
Log.d(javaClass.toString(), "Skipping HTTP header metadata - embedded metadata already exists")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
@@ -587,4 +937,5 @@ open class BaseMediaService : MediaLibraryService() {
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
private const val RADIO_HEADER_CHECK_INTERVAL_SECONDS = 10L
|
||||
|
||||
|
||||
@@ -61,13 +61,47 @@ public class TrackInfoDialog extends DialogFragment {
|
||||
private void setTrackInfo() {
|
||||
genreLink = null;
|
||||
yearLink = null;
|
||||
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))
|
||||
: "");
|
||||
|
||||
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||
boolean isRadio = Objects.equals(type, Constants.MEDIA_TYPE_RADIO);
|
||||
|
||||
if (isRadio) {
|
||||
// For radio: show "Artist - Title" or just title in header, station name as artist
|
||||
String stationName = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("stationName",
|
||||
mediaMetadata.title != null ? String.valueOf(mediaMetadata.title) : "")
|
||||
: mediaMetadata.title != null ? String.valueOf(mediaMetadata.title) : "";
|
||||
|
||||
String artist = mediaMetadata.artist != null
|
||||
? String.valueOf(mediaMetadata.artist)
|
||||
: mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioArtist", "")
|
||||
: "";
|
||||
|
||||
String title = mediaMetadata.title != null
|
||||
? String.valueOf(mediaMetadata.title)
|
||||
: mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioTitle", "")
|
||||
: "";
|
||||
|
||||
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 {
|
||||
mainTitle = stationName;
|
||||
}
|
||||
|
||||
bind.trakTitleInfoTextView.setText(mainTitle);
|
||||
bind.trakArtistInfoTextView.setText(stationName);
|
||||
} else {
|
||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||
bind.trakArtistInfoTextView.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: "");
|
||||
}
|
||||
|
||||
if (mediaMetadata.extras != null) {
|
||||
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
|
||||
@@ -90,6 +124,27 @@ public class TrackInfoDialog extends DialogFragment {
|
||||
String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder));
|
||||
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);
|
||||
|
||||
@@ -213,12 +213,52 @@ 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)) {
|
||||
String stationName = mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("stationName",
|
||||
mediaMetadata.title != null ? String.valueOf(mediaMetadata.title) : "")
|
||||
: mediaMetadata.title != null ? String.valueOf(mediaMetadata.title) : "";
|
||||
|
||||
String artist = mediaMetadata.artist != null
|
||||
? String.valueOf(mediaMetadata.artist)
|
||||
: mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioArtist", "")
|
||||
: "";
|
||||
|
||||
String title = mediaMetadata.title != null
|
||||
? String.valueOf(mediaMetadata.title)
|
||||
: mediaMetadata.extras != null
|
||||
? mediaMetadata.extras.getString("radioTitle", "")
|
||||
: "";
|
||||
|
||||
String mainTitle;
|
||||
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||
mainTitle = artist + " - " + title;
|
||||
} else if (!TextUtils.isEmpty(title)) {
|
||||
mainTitle = title;
|
||||
} 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);
|
||||
|
||||
@@ -209,6 +209,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);
|
||||
|
||||
@@ -217,6 +218,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"
|
||||
|
||||
@@ -499,6 +499,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>
|
||||
|
||||
Reference in New Issue
Block a user