optimizing a bit and better format for notification

This commit is contained in:
TrackArcher
2026-01-13 10:56:21 +01:00
parent 67a23b385e
commit 41a9cdf2e2
3 changed files with 121 additions and 205 deletions

View File

@@ -195,96 +195,89 @@ open class BaseMediaService : MediaLibraryService() {
// 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
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 -> {
// 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
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
}
"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
}
"ARTIST" -> if (!value.isNullOrBlank()) artist = value
"TITLE" -> if (!value.isNullOrBlank()) title = value
}
}
}
}
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
// Deduplicate consecutive identical metadata
if (artist == lastRadioArtist && title == lastRadioTitle) 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(currentItem.mediaMetadata.extras ?: Bundle())
val newExtras = Bundle(extras ?: Bundle())
artist?.let {
metadataBuilder.setArtist(it)
newExtras.putString("radioArtist", it)
}
title?.let {
metadataBuilder.setTitle(it)
newExtras.putString("radioTitle", it)
// 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)
}
// 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) }
// 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.setExtras(newExtras)
val updatedItem = currentItem.buildUpon()
.setMediaMetadata(metadataBuilder.build())
.build()
metadataBuilder.setTitle(formattedTitle)
if (stationName.isNotBlank()) {
metadataBuilder.setArtist(stationName)
}
(player as? ExoPlayer)?.let { exo ->
exo.replaceMediaItem(currentIndex, updatedItem)
exo.replaceMediaItem(currentIndex, currentItem.buildUpon()
.setMediaMetadata(metadataBuilder.setExtras(newExtras).build())
.build())
updateWidget(exo)
// Media3 notification will automatically update via MediaMetadata changes
}
}
@@ -560,124 +553,60 @@ open class BaseMediaService : MediaLibraryService() {
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 ?: run {
Log.d(javaClass.toString(), "Failed to create HTTP connection for: $streamUrl")
return
}
val connection = url.openConnection() as? HttpURLConnection ?: return
// Try HEAD request first (more efficient)
// 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 = 5000
connection.readTimeout = 5000
connection.connectTimeout = 3000 // Reduced timeout
connection.readTimeout = 3000
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
}
}
connection.connect()
val responseCode = connection.responseCode
if (responseCode >= 400) {
Log.d(javaClass.toString(), "HEAD request failed with code: $responseCode")
if (connection.responseCode >= 400) {
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"
// Check for metadata in HTTP headers
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
if (!streamTitle.isNullOrBlank()) {
processStreamTitle(streamTitle, player)
}
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
// Silently fail - this is a fallback mechanism, ICY metadata is primary
}
}
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?
// Parse "Artist - Title" format
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")
}
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
// Deduplicate consecutive identical metadata
if (artist == lastRadioArtist && title == lastRadioTitle) {
Log.d(javaClass.toString(), "Skipping duplicate metadata")
return
}
lastRadioArtist = artist
lastRadioTitle = title
// Update on main thread
widgetUpdateHandler.post {
@@ -686,63 +615,49 @@ open class BaseMediaService : MediaLibraryService() {
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
if (currentExtras?.getString("type") != 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
// 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
// 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()
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()
(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")
.build())
updateWidget(exo)
}
}
}
@@ -937,5 +852,5 @@ open class BaseMediaService : MediaLibraryService() {
}
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
private const val RADIO_HEADER_CHECK_INTERVAL_SECONDS = 10L
private const val RADIO_HEADER_CHECK_INTERVAL_SECONDS = 30L // Reduced frequency - only fallback when ICY fails

View File

@@ -66,29 +66,29 @@ public class TrackInfoDialog extends DialogFragment {
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
// 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.title != null ? String.valueOf(mediaMetadata.title) : "")
: mediaMetadata.title != null ? String.valueOf(mediaMetadata.title) : "";
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
String artist = mediaMetadata.artist != null
? String.valueOf(mediaMetadata.artist)
: mediaMetadata.extras != null
String artist = mediaMetadata.extras != null
? mediaMetadata.extras.getString("radioArtist", "")
: "";
String title = mediaMetadata.title != null
? String.valueOf(mediaMetadata.title)
: mediaMetadata.extras != null
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;
}

View File

@@ -216,28 +216,29 @@ public class PlayerControllerFragment extends Fragment {
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.title != null ? String.valueOf(mediaMetadata.title) : "")
: mediaMetadata.title != null ? String.valueOf(mediaMetadata.title) : "";
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
String artist = mediaMetadata.artist != null
? String.valueOf(mediaMetadata.artist)
: mediaMetadata.extras != null
String artist = mediaMetadata.extras != null
? mediaMetadata.extras.getString("radioArtist", "")
: "";
String title = mediaMetadata.title != null
? String.valueOf(mediaMetadata.title)
: mediaMetadata.extras != null
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;
}