From f08695657878abee46c72176882d4b53ebb1e919 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Sun, 26 Apr 2026 13:15:17 +0200 Subject: [PATCH] feat: enhance track label formatting for audio, subtitles, and video --- .../purefin/player/manager/TrackMapper.kt | 131 ++++++++++++++++-- 1 file changed, 122 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/hu/bbara/purefin/player/manager/TrackMapper.kt b/core/src/main/java/hu/bbara/purefin/player/manager/TrackMapper.kt index 3941afc0..a8604475 100644 --- a/core/src/main/java/hu/bbara/purefin/player/manager/TrackMapper.kt +++ b/core/src/main/java/hu/bbara/purefin/player/manager/TrackMapper.kt @@ -7,6 +7,7 @@ import androidx.media3.common.Tracks import androidx.media3.common.util.UnstableApi import hu.bbara.purefin.player.model.TrackOption import hu.bbara.purefin.player.model.TrackType +import java.util.Locale import javax.inject.Inject data class TrackSelectionState( @@ -35,10 +36,7 @@ class TrackMapper @Inject constructor() { repeat(group.length) { trackIndex -> val format = group.getTrackFormat(trackIndex) val id = "a_${groupIndex}_${trackIndex}" - val label = format.label - ?: format.language - ?: "${format.channelCount}ch" - ?: "Audio ${trackIndex}" + val label = formatAudioLabel(format, trackIndex) val option = TrackOption( id = id, label = label, @@ -60,10 +58,8 @@ class TrackMapper @Inject constructor() { repeat(group.length) { trackIndex -> val format = group.getTrackFormat(trackIndex) val id = "t_${groupIndex}_${trackIndex}" - val label = format.label - ?: format.language - ?: "Subtitle ${trackIndex}" val isForced = (format.selectionFlags and C.SELECTION_FLAG_FORCED) != 0 + val label = formatTextLabel(format, trackIndex, isForced) val option = TrackOption( id = id, label = label, @@ -86,8 +82,7 @@ class TrackMapper @Inject constructor() { repeat(group.length) { trackIndex -> val format = group.getTrackFormat(trackIndex) val id = "v_${groupIndex}_${trackIndex}" - val res = if (format.height != Format.NO_VALUE) "${format.height}p" else null - val label = res ?: format.label ?: "Video ${trackIndex}" + val label = formatVideoLabel(format, trackIndex) val option = TrackOption( id = id, label = label, @@ -134,4 +129,122 @@ class TrackMapper @Inject constructor() { selectedVideoTrackId = selectedVideo ) } + + private fun formatAudioLabel(format: Format, trackIndex: Int): String { + val details = listOfNotNull( + displayLanguage(format.language), + audioCodec(format), + channelLabel(format.channelCount) + ).joinToString(" ") + + return labelWithDetails( + title = format.label, + details = details, + fallback = "Audio ${trackIndex + 1}" + ) + } + + private fun formatTextLabel(format: Format, trackIndex: Int, isForced: Boolean): String { + val details = listOfNotNull( + displayLanguage(format.language), + subtitleCodec(format), + "Forced".takeIf { isForced } + ).joinToString(" ") + + return labelWithDetails( + title = format.label, + details = details, + fallback = "Subtitle ${trackIndex + 1}" + ) + } + + private fun formatVideoLabel(format: Format, trackIndex: Int): String { + val details = if (format.height != Format.NO_VALUE && format.height > 0) { + "${format.height}p" + } else { + null + } + + return labelWithDetails( + title = format.label, + details = details, + fallback = "Video ${trackIndex + 1}" + ) + } + + private fun labelWithDetails(title: String?, details: String?, fallback: String): String { + val cleanTitle = title?.trim()?.takeIf { it.isNotEmpty() } + val cleanDetails = details?.trim()?.takeIf { it.isNotEmpty() } + + return when { + cleanTitle != null && cleanDetails != null && cleanTitle != cleanDetails -> + "$cleanTitle - $cleanDetails" + + cleanTitle != null -> cleanTitle + cleanDetails != null -> cleanDetails + else -> fallback + } + } + + private fun displayLanguage(code: String?): String? { + val cleanCode = code + ?.trim() + ?.takeIf { it.isNotEmpty() && !it.equals("und", ignoreCase = true) } + ?: return null + val locale = Locale.forLanguageTag(cleanCode.replace('_', '-')) + val language = locale.getDisplayLanguage(Locale.getDefault()) + + return language.takeIf { it.isNotEmpty() && it != cleanCode } ?: cleanCode + } + + private fun audioCodec(format: Format): String? { + val value = format.codecText() + + return when { + value.contains("truehd") -> "TrueHD" + value.contains("eac3") || value.contains("e-ac-3") || value.contains("ec-3") -> + "Dolby Digital Plus" + + value.contains("ac3") || value.contains("ac-3") -> "Dolby Digital" + value.contains("dts") || value.contains("dca") -> "DTS" + value.contains("alac") -> "ALAC" + value.contains("flac") -> "FLAC" + value.contains("opus") -> "Opus" + value.contains("vorbis") || value.contains("ogg") -> "Vorbis" + value.contains("mp3") || value.contains("mpeg") -> "MP3" + value.contains("aac") || value.contains("mp4a") -> "AAC" + value.contains("pcm") || value.contains("raw") || value.contains("lpcm") -> "PCM" + else -> null + } + } + + private fun subtitleCodec(format: Format): String? { + val value = format.codecText() + + return when { + value.contains("subrip") || value.contains("srt") -> "SRT" + value.contains("ssa") || value.contains("ass") -> "ASS" + value.contains("pgs") || value.contains("pgssub") -> "PGS" + value.contains("dvb") -> "DVB" + value.contains("dvd") || value.contains("vobsub") -> "DVD" + value.contains("vtt") || value.contains("webvtt") -> "VTT" + else -> null + } + } + + private fun Format.codecText(): String = listOfNotNull( + sampleMimeType, + containerMimeType, + codecs + ).joinToString(" ").lowercase(Locale.US) + + private fun channelLabel(channelCount: Int): String? = + when { + channelCount == Format.NO_VALUE || channelCount <= 0 -> null + channelCount == 1 -> "Mono" + channelCount == 2 -> "Stereo" + channelCount == 6 -> "5.1" + channelCount == 8 -> "7.1" + else -> "$channelCount ch" + } }