detail: heavily improve format handling
Heavily improve the way Auxio handles shows formats in the song properties view. This is composed of the following: - Using ExoPlayer to find a format-specific mime type before having to fall back to other solutions - Keeping around the format and extension mime types so that each is picked in the best circumstances - Using MediaFormat to also retrieve a format-specific mime type in the case that ExoPlayer parsing is disabled - Adding special names for the most common formats
This commit is contained in:
parent
8e56459f8b
commit
5f0518d983
7 changed files with 149 additions and 15 deletions
|
@ -33,6 +33,7 @@ import org.oxycblt.auxio.detail.recycler.SortHeader
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MimeType
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
|
@ -51,7 +52,12 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class DetailViewModel : ViewModel(), MusicStore.Callback {
|
||||
data class DetailSong(val song: Song, val bitrateKbps: Int?, val sampleRate: Int?)
|
||||
data class DetailSong(
|
||||
val song: Song,
|
||||
val bitrateKbps: Int?,
|
||||
val sampleRate: Int?,
|
||||
val resolvedMimeType: MimeType
|
||||
)
|
||||
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
@ -157,7 +163,7 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
} catch (e: Exception) {
|
||||
logW("Unable to extract song attributes.")
|
||||
logW(e.stackTraceToString())
|
||||
return@withContext DetailSong(song, null, null)
|
||||
return@withContext DetailSong(song, null, null, song.mimeType)
|
||||
}
|
||||
|
||||
val format = extractor.getTrackFormat(0)
|
||||
|
@ -176,7 +182,24 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
null
|
||||
}
|
||||
|
||||
DetailSong(song, bitrate, sampleRate)
|
||||
val resolvedMimeType =
|
||||
if (song.mimeType.fromFormat != null) {
|
||||
// ExoPlayer was already able to populate the format.
|
||||
song.mimeType
|
||||
} else {
|
||||
val formatMimeType =
|
||||
try {
|
||||
format.getString(MediaFormat.KEY_MIME)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
// Ensure that we don't include the functionally useless
|
||||
// "audio/raw" mime type
|
||||
MimeType(song.mimeType.fromExtension, formatMimeType)
|
||||
}
|
||||
|
||||
DetailSong(song, bitrate, sampleRate, resolvedMimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -188,7 +211,7 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
|
||||
// To create a good user experience regarding disc numbers, we intersperse
|
||||
// items that show the disc number throughout the album's songs. In the case
|
||||
// that the album does not have distinct disc numbers, we omit the header.
|
||||
// that the album does not have distinct disc numbers, we omit the header.
|
||||
val songs = albumSort.songs(album.songs)
|
||||
val byDisc = songs.groupBy { it.disc ?: 1 }
|
||||
if (byDisc.size > 1) {
|
||||
|
|
|
@ -62,9 +62,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
binding.detailContainer.isGone = false
|
||||
binding.detailFileName.setText(song.song.path.name)
|
||||
binding.detailRelativeDir.setText(song.song.path.parent.resolveName(requireContext()))
|
||||
binding.detailFormat.setText(
|
||||
mimeTypes.getExtensionFromMimeType(song.song.mimeType)?.uppercase()
|
||||
?: getString(R.string.def_format))
|
||||
binding.detailFormat.setText(song.resolvedMimeType.resolveName(requireContext()))
|
||||
binding.detailSize.setText(Formatter.formatFileSize(requireContext(), song.song.size))
|
||||
binding.detailDuration.setText(song.song.durationSecs.formatDuration(true))
|
||||
|
||||
|
@ -85,6 +83,41 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getMimeName(mime: String): String? {
|
||||
return when (mime) {
|
||||
// Since Auxio only feasibly loads music, we can match for general mime types
|
||||
// and assume that they are audio-based.
|
||||
|
||||
// Classic formats
|
||||
"audio/mpeg",
|
||||
"audio/mp3" -> "MPEG-1 Layer 3"
|
||||
"audio/ogg",
|
||||
"application/ogg" -> "OGG"
|
||||
"audio/vorbis" -> "OGG Vorbis"
|
||||
"audio/opus" -> "OGG Opus"
|
||||
"audio/flac" -> "(OGG) FLAC"
|
||||
|
||||
// Modern formats
|
||||
"audio/mp4",
|
||||
"audio/mp4a-latm",
|
||||
"audio/mpeg4-generic",
|
||||
"audio/aac",
|
||||
"audio/3gpp",
|
||||
"audio/3gpp2", -> "Advanced Audio Coding (AAC)"
|
||||
"audio/x-matroska" -> "Matroska Audio (MKA)"
|
||||
|
||||
// Windows formats
|
||||
"audio/wav",
|
||||
"audio/x-wav",
|
||||
"audio/wave",
|
||||
"audio/vnd.wave" -> "Microsoft WAV"
|
||||
"audio/x-ms-wma" -> "Windows Media Audio (WMA)"
|
||||
|
||||
// Don't know, fall back to an extension
|
||||
else -> mimeTypes.getExtensionFromMimeType(mime)?.uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(song: Song): SongDetailDialog {
|
||||
val instance = SongDetailDialog()
|
||||
|
|
|
@ -64,7 +64,7 @@ data class Song(
|
|||
/** The URI linking to this song's file. */
|
||||
val uri: Uri,
|
||||
/** The mime type of this song. */
|
||||
val mimeType: String,
|
||||
val mimeType: MimeType,
|
||||
/** The size of this song (in bytes) */
|
||||
val size: Long,
|
||||
/** The total duration of this song, in millis. */
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.MimeTypeMap
|
||||
import org.oxycblt.auxio.R
|
||||
|
||||
/**
|
||||
|
@ -65,3 +66,57 @@ sealed class Dir {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a mime type as it is loaded by Auxio. [fromExtension] is based on the file extension
|
||||
* should always exist, while [fromFormat] is based on the file itself and may not be available.
|
||||
*/
|
||||
data class MimeType(val fromExtension: String, val fromFormat: String?) {
|
||||
fun resolveName(context: Context): String {
|
||||
// Prefer the format mime type first, as it actually is derived from the file
|
||||
// and not the extension. Just make sure to ignore audio/raw, as that could feasibly
|
||||
// correspond to multiple formats.
|
||||
val readableMime =
|
||||
if (fromFormat != null && fromFormat != "audio/raw") {
|
||||
fromFormat
|
||||
} else {
|
||||
fromExtension
|
||||
}
|
||||
|
||||
// We have special names for the most common formats.
|
||||
val readableStringRes =
|
||||
when (readableMime) {
|
||||
// Classic formats
|
||||
"audio/mpeg",
|
||||
"audio/mp3" -> R.string.cdc_mp3
|
||||
"audio/vorbis" -> R.string.cdc_ogg_vorbis
|
||||
"audio/opus" -> R.string.cdc_ogg_opus
|
||||
"audio/flac" -> R.string.cdc_flac
|
||||
|
||||
// MP4, 3GPP, M4A, etc. are all based on AAC
|
||||
"audio/mp4",
|
||||
"audio/mp4a-latm",
|
||||
"audio/mpeg4-generic",
|
||||
"audio/aac",
|
||||
"audio/3gpp",
|
||||
"audio/3gpp2", -> R.string.cdc_aac
|
||||
|
||||
// Windows formats
|
||||
"audio/wav",
|
||||
"audio/x-wav",
|
||||
"audio/wave",
|
||||
"audio/vnd.wave" -> R.string.cdc_wav
|
||||
"audio/x-ms-wma" -> R.string.cdc_wma
|
||||
|
||||
else -> -1
|
||||
}
|
||||
|
||||
return if (readableStringRes > -1) {
|
||||
context.getString(readableStringRes)
|
||||
} else {
|
||||
// Fall back to the extension if we can't find a special name for this format.
|
||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase()
|
||||
?: context.getString(R.string.def_codec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,15 +140,22 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
|
|||
return null
|
||||
}
|
||||
|
||||
val metadata =
|
||||
val format =
|
||||
try {
|
||||
future.get()[0].getFormat(0).metadata
|
||||
future.get()[0].getFormat(0)
|
||||
} catch (e: Exception) {
|
||||
logW("Unable to extract metadata for ${audio.title}")
|
||||
logW(e.stackTraceToString())
|
||||
null
|
||||
}
|
||||
|
||||
if (format == null) {
|
||||
logD("Nothing could be extracted for ${audio.title}")
|
||||
return audio.toSong()
|
||||
}
|
||||
|
||||
format.sampleMimeType?.let { audio.formatMimeType = it }
|
||||
val metadata = format.metadata
|
||||
if (metadata != null) {
|
||||
completeAudio(metadata)
|
||||
} else {
|
||||
|
|
|
@ -28,6 +28,7 @@ import androidx.core.database.getStringOrNull
|
|||
import java.io.File
|
||||
import org.oxycblt.auxio.music.Dir
|
||||
import org.oxycblt.auxio.music.Indexer
|
||||
import org.oxycblt.auxio.music.MimeType
|
||||
import org.oxycblt.auxio.music.Path
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.albumCoverUri
|
||||
|
@ -216,7 +217,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
audio.id = cursor.getLong(idIndex)
|
||||
audio.title = cursor.getString(titleIndex)
|
||||
|
||||
audio.mimeType = cursor.getString(mimeTypeIndex)
|
||||
audio.extensionMimeType = cursor.getString(mimeTypeIndex)
|
||||
audio.size = cursor.getLong(sizeIndex)
|
||||
|
||||
// Try to use the DISPLAY_NAME field to obtain a (probably sane) file name
|
||||
|
@ -266,7 +267,8 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
var title: String? = null,
|
||||
var displayName: String? = null,
|
||||
var dir: Dir? = null,
|
||||
var mimeType: String? = null,
|
||||
var extensionMimeType: String? = null,
|
||||
var formatMimeType: String? = null,
|
||||
var size: Long? = null,
|
||||
var duration: Long? = null,
|
||||
var track: Int? = null,
|
||||
|
@ -288,7 +290,11 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
name = requireNotNull(displayName) { "Malformed audio: No display name" },
|
||||
parent = requireNotNull(dir) { "Malformed audio: No parent directory" }),
|
||||
uri = requireNotNull(id) { "Malformed audio: No id" }.audioUri,
|
||||
mimeType = requireNotNull(mimeType) { "Malformed audio: No mime type" },
|
||||
mimeType =
|
||||
MimeType(
|
||||
fromExtension =
|
||||
requireNotNull(extensionMimeType) { "Malformed audio: No mime type" },
|
||||
fromFormat = formatMimeType),
|
||||
size = requireNotNull(size) { "Malformed audio: No size" },
|
||||
durationMs = requireNotNull(duration) { "Malformed audio: No duration" },
|
||||
track = track,
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<string name="lbl_go_album">Go to album</string>
|
||||
<string name="lbl_song_detail">View properties</string>
|
||||
|
||||
<string name="lbl_props">File properties</string>
|
||||
<string name="lbl_props">Song properties</string>
|
||||
<string name="lbl_file_name">File name</string>
|
||||
<string name="lbl_relative_path">Relative path</string>
|
||||
<string name="lbl_format">Format</string>
|
||||
|
@ -169,12 +169,22 @@
|
|||
<string name="def_date">No Date</string>
|
||||
<string name="def_track">No Track Number</string>
|
||||
<string name="def_playback">No music playing</string>
|
||||
<string name="def_format">Unknown Format</string>
|
||||
<string name="def_codec">Unknown Format</string>
|
||||
<string name="def_bitrate">No Bitrate</string>
|
||||
<string name="def_sample_rate">No Sample Rate</string>
|
||||
<string name="def_widget_song">Song Name</string>
|
||||
<string name="def_widget_artist">Artist Name</string>
|
||||
|
||||
<!-- Codec Namespace | Format names -->
|
||||
<string name="cdc_mp3">MPEG-1 Layer 3 (MP3)</string>
|
||||
<string name="cdc_ogg">OGG</string>
|
||||
<string name="cdc_ogg_vorbis">OGG Vorbis</string>
|
||||
<string name="cdc_ogg_opus">OGG Opus</string>
|
||||
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
|
||||
<string name="cdc_aac">Advanced Audio Coding (AAC)</string>
|
||||
<string name="cdc_wav">Microsoft WAV</string>
|
||||
<string name="cdc_wma">Windows Media Audio (WMA)</string>
|
||||
|
||||
<!-- Color Label namespace | Accent names -->
|
||||
<string name="clr_red">Red</string>
|
||||
<string name="clr_pink">Pink</string>
|
||||
|
|
Loading…
Reference in a new issue