From 02b3ddbb9828ae85182a53b86e2e3011d409d5be Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:32:17 +0000 Subject: [PATCH] add a song info dialog and show the encoding in the manager The song context menus gain Info, which lists every field we hold for the song and, when it is downloaded, the file's format, bitrate, sample rate, channels and size read from the file itself. For the song currently playing it takes the encoding from the media3 stream instead, so a transcoded stream reports what is actually being played. The menu is shared, so this reaches the album, playlist, now playing and queue menus at once. The downloads manager shows the same format and bitrate beside the size on each song row, read lazily for the rows on screen rather than for the whole tree. --- .../com/subtracks/data/db/DownloadDao.kt | 2 +- .../data/media/AudioEncodingReader.kt | 32 ++++++ .../com/subtracks/data/model/AudioEncoding.kt | 22 ++++ .../com/subtracks/data/model/Download.kt | 1 + .../subtracks/data/repo/DownloadRepository.kt | 13 ++- .../subtracks/playback/Media3PlayerHandle.kt | 21 ++++ .../subtracks/playback/PlaybackController.kt | 3 + .../com/subtracks/playback/PlayerHandle.kt | 5 + .../main/kotlin/com/subtracks/ui/AppRoot.kt | 23 +++- .../ui/components/ItemContextMenu.kt | 5 + .../subtracks/ui/components/SongInfoDialog.kt | 100 ++++++++++++++++++ .../subtracks/ui/downloads/DownloadsScreen.kt | 41 +++++-- .../ui/downloads/DownloadsViewModel.kt | 5 + .../subtracks/data/model/AudioEncodingTest.kt | 25 +++++ .../ui/downloads/DownloadTreeTest.kt | 1 + 15 files changed, 286 insertions(+), 13 deletions(-) create mode 100644 app/src/main/kotlin/com/subtracks/data/media/AudioEncodingReader.kt create mode 100644 app/src/main/kotlin/com/subtracks/data/model/AudioEncoding.kt create mode 100644 app/src/main/kotlin/com/subtracks/ui/components/SongInfoDialog.kt create mode 100644 app/src/test/kotlin/com/subtracks/data/model/AudioEncodingTest.kt diff --git a/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt b/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt index 7cc8e7a3..5ed2b482 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/DownloadDao.kt @@ -104,7 +104,7 @@ interface DownloadDao { fun playlistStatuses(sourceId: Long): Flow> @Query( - "SELECT d.songId AS songId, s.title AS title, s.albumId AS albumId, al.name AS albumName, " + + "SELECT d.sourceId AS sourceId, d.songId AS songId, s.title AS title, s.albumId AS albumId, al.name AS albumName, " + "al.artistId AS artistId, ar.name AS artistName, d.status AS status, d.total AS size " + "FROM song_downloads d " + "JOIN songs s ON s.sourceId = d.sourceId AND s.id = d.songId " + diff --git a/app/src/main/kotlin/com/subtracks/data/media/AudioEncodingReader.kt b/app/src/main/kotlin/com/subtracks/data/media/AudioEncodingReader.kt new file mode 100644 index 00000000..6b97c222 --- /dev/null +++ b/app/src/main/kotlin/com/subtracks/data/media/AudioEncodingReader.kt @@ -0,0 +1,32 @@ +package com.subtracks.data.media + +import android.media.MediaExtractor +import android.media.MediaFormat +import com.subtracks.data.model.AudioEncoding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +suspend fun readAudioEncoding(file: File): AudioEncoding? = + withContext(Dispatchers.IO) { + runCatching { + val extractor = MediaExtractor() + try { + extractor.setDataSource(file.absolutePath) + if (extractor.trackCount == 0) return@runCatching null + val format = extractor.getTrackFormat(0) + AudioEncoding( + mimeType = format.stringOrNull(MediaFormat.KEY_MIME), + bitrate = format.intOrNull(MediaFormat.KEY_BIT_RATE), + sampleRate = format.intOrNull(MediaFormat.KEY_SAMPLE_RATE), + channels = format.intOrNull(MediaFormat.KEY_CHANNEL_COUNT), + ) + } finally { + extractor.release() + } + }.getOrNull() + } + +private fun MediaFormat.stringOrNull(key: String): String? = if (containsKey(key)) getString(key) else null + +private fun MediaFormat.intOrNull(key: String): Int? = if (containsKey(key)) getInteger(key) else null diff --git a/app/src/main/kotlin/com/subtracks/data/model/AudioEncoding.kt b/app/src/main/kotlin/com/subtracks/data/model/AudioEncoding.kt new file mode 100644 index 00000000..28480b1f --- /dev/null +++ b/app/src/main/kotlin/com/subtracks/data/model/AudioEncoding.kt @@ -0,0 +1,22 @@ +package com.subtracks.data.model + +data class AudioEncoding( + val mimeType: String?, + val bitrate: Int? = null, + val sampleRate: Int? = null, + val channels: Int? = null, +) { + val format: String? + get() = + when (mimeType?.lowercase()) { + null -> null + "audio/mpeg" -> "MP3" + "audio/mp4", "audio/m4a", "audio/x-m4a", "audio/aac" -> "AAC" + "audio/flac", "audio/x-flac" -> "FLAC" + "audio/ogg" -> "Ogg" + "audio/opus" -> "Opus" + "audio/webm" -> "WebM" + "audio/wav", "audio/x-wav" -> "WAV" + else -> mimeType.substringAfterLast('/').uppercase() + } +} diff --git a/app/src/main/kotlin/com/subtracks/data/model/Download.kt b/app/src/main/kotlin/com/subtracks/data/model/Download.kt index 4d98bc58..cdc03319 100644 --- a/app/src/main/kotlin/com/subtracks/data/model/Download.kt +++ b/app/src/main/kotlin/com/subtracks/data/model/Download.kt @@ -63,6 +63,7 @@ data class EntityDownloadStatus( data class DownloadedSong( val songId: String, + val sourceId: Long, val title: String, val albumId: String?, val albumName: String?, diff --git a/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt index 970e7fda..e68ce93f 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/DownloadRepository.kt @@ -93,12 +93,17 @@ class DownloadRepository( if (started) loaded.await() } - fun localUri(songId: String): String? = + fun localFile(songId: String): File? = statesFlow.value[songId] ?.takeIf { it.status == DownloadStatus.Completed } - ?.let { file(it.sourceId, it.songId) } - ?.takeIf { it.exists() } - ?.let { Uri.fromFile(it).toString() } + ?.let { localFile(it.sourceId, it.songId) } + + fun localFile( + sourceId: Long, + songId: String, + ): File? = file(sourceId, songId).takeIf { it.exists() } + + fun localUri(songId: String): String? = localFile(songId)?.let { Uri.fromFile(it).toString() } suspend fun download( sourceId: Long, diff --git a/app/src/main/kotlin/com/subtracks/playback/Media3PlayerHandle.kt b/app/src/main/kotlin/com/subtracks/playback/Media3PlayerHandle.kt index 649175ba..258d2b58 100644 --- a/app/src/main/kotlin/com/subtracks/playback/Media3PlayerHandle.kt +++ b/app/src/main/kotlin/com/subtracks/playback/Media3PlayerHandle.kt @@ -3,12 +3,15 @@ package com.subtracks.playback import android.os.Bundle import android.util.Log import androidx.media3.common.C +import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.ClippingConfiguration import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaController +import com.subtracks.data.model.AudioEncoding import com.subtracks.data.repo.DownloadRepository import com.subtracks.data.repo.SourceRepository @@ -38,6 +41,24 @@ class Media3PlayerHandle( override val currentItem: QueueItem? get() = controller.currentMediaItem?.let(::toQueueItem) + @get:UnstableApi + override val audioEncoding: AudioEncoding? + get() = + controller.currentTracks.groups + .firstOrNull { it.type == C.TRACK_TYPE_AUDIO && it.isSelected } + ?.let { group -> + (0 until group.length) + .firstOrNull { group.isTrackSelected(it) } + ?.let(group::getTrackFormat) + }?.let { format -> + AudioEncoding( + mimeType = format.sampleMimeType, + bitrate = format.bitrate.takeIf { it != Format.NO_VALUE }, + sampleRate = format.sampleRate.takeIf { it != Format.NO_VALUE }, + channels = format.channelCount.takeIf { it != Format.NO_VALUE }, + ) + } + override fun setWindow( items: List, startIndex: Int, diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt index 8daf7ad0..21325ffd 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt @@ -1,5 +1,6 @@ package com.subtracks.playback +import com.subtracks.data.model.AudioEncoding import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.QueueEntry import com.subtracks.data.model.QueueKind @@ -118,6 +119,8 @@ class PlaybackController( private val _positionMs = MutableStateFlow(0L) val positionMs: StateFlow = _positionMs + fun currentAudioEncoding(): AudioEncoding? = player?.audioEncoding + private var player: PlayerHandle? = null private var connecting = false private var pendingPlay: PendingPlay? = null diff --git a/app/src/main/kotlin/com/subtracks/playback/PlayerHandle.kt b/app/src/main/kotlin/com/subtracks/playback/PlayerHandle.kt index a3ef4df1..b397f7b4 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlayerHandle.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlayerHandle.kt @@ -1,5 +1,7 @@ package com.subtracks.playback +import com.subtracks.data.model.AudioEncoding + interface PlayerHandle { val itemCount: Int val currentIndex: Int @@ -12,6 +14,9 @@ interface PlayerHandle { val currentPositionMs: Long val currentItem: QueueItem? + /** The encoding of the audio now being rendered, when the player knows it. */ + val audioEncoding: AudioEncoding? get() = null + fun setWindow( items: List, startIndex: Int, diff --git a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt index 8532130f..523289ca 100644 --- a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt +++ b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt @@ -58,12 +58,15 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import com.subtracks.data.model.Song +import com.subtracks.data.repo.DownloadRepository import com.subtracks.data.repo.LibraryRepository import com.subtracks.data.repo.SourceRepository import com.subtracks.playback.PlaybackController import com.subtracks.ui.components.ContextMenuHost import com.subtracks.ui.components.ItemContextMenu import com.subtracks.ui.components.LoadingState +import com.subtracks.ui.components.SongInfoDialog import com.subtracks.ui.components.statusBarScrim import com.subtracks.ui.downloads.DownloadsRoute import com.subtracks.ui.library.AlbumDetailRoute @@ -175,7 +178,9 @@ private fun MainNavigation() { val tabBarVisible = currentRoute == Routes.LIBRARY val playbackController = koinInject() val libraryRepository = koinInject() + val downloadRepository = koinInject() val playback by playbackController.state.collectAsStateWithLifecycle() + val downloads by downloadRepository.states().collectAsStateWithLifecycle() var showingQueue by rememberSaveable { mutableStateOf(false) } var nowPlayingOpen by rememberSaveable { mutableStateOf(false) } var nowPlayingProgress by remember { mutableFloatStateOf(0f) } @@ -184,6 +189,7 @@ private fun MainNavigation() { var settleJob by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() val contextMenuHost = remember { ContextMenuHost() } + var infoSong by remember { mutableStateOf(null) } fun settleNowPlaying(open: Boolean) { if (open) { @@ -502,7 +508,22 @@ private fun MainNavigation() { } contextMenuHost.target?.let { target -> - ItemContextMenu(target = target, actions = contextMenuHost.actions, onDismiss = { contextMenuHost.dismiss() }) + ItemContextMenu( + target = target, + actions = contextMenuHost.actions, + onDismiss = { contextMenuHost.dismiss() }, + onInfo = { infoSong = it }, + ) + } + + infoSong?.let { song -> + SongInfoDialog( + song = song, + download = downloads[song.id], + localFile = downloadRepository.localFile(song.id), + streamEncoding = playbackController.currentAudioEncoding().takeIf { playback.item?.id == song.id }, + onDismiss = { infoSong = null }, + ) } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt b/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt index 2949e22d..02838c6d 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.rounded.Album import androidx.compose.material.icons.rounded.Cancel import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.SkipNext import androidx.compose.material.icons.rounded.Star @@ -164,6 +165,7 @@ fun ItemContextMenu( target: MenuTarget, actions: ItemActions, onDismiss: () -> Unit, + onInfo: ((SongModel) -> Unit)? = null, ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scope = rememberCoroutineScope() @@ -219,6 +221,9 @@ fun ItemContextMenu( } } } + onInfo?.let { info -> + MenuItem(Icons.Rounded.Info, "Info") { dismiss { info(target.song) } } + } } is MenuTarget.Album -> { diff --git a/app/src/main/kotlin/com/subtracks/ui/components/SongInfoDialog.kt b/app/src/main/kotlin/com/subtracks/ui/components/SongInfoDialog.kt new file mode 100644 index 00000000..09727c0e --- /dev/null +++ b/app/src/main/kotlin/com/subtracks/ui/components/SongInfoDialog.kt @@ -0,0 +1,100 @@ +package com.subtracks.ui.components + +import android.text.format.DateUtils +import android.text.format.Formatter +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.subtracks.data.media.readAudioEncoding +import com.subtracks.data.model.AudioEncoding +import com.subtracks.data.model.DownloadStatus +import com.subtracks.data.model.Song +import com.subtracks.data.model.SongDownload +import java.io.File + +@Composable +fun SongInfoDialog( + song: Song, + download: SongDownload?, + localFile: File?, + streamEncoding: AudioEncoding?, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + var encoding by remember(song.id, localFile) { mutableStateOf(null) } + LaunchedEffect(song.id, localFile) { + encoding = localFile?.let { readAudioEncoding(it) } + } + val shown = if (download?.status == DownloadStatus.Completed) encoding else streamEncoding + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Info") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + InfoRow("Title", song.title) + InfoRow("Artist", song.artist) + InfoRow("Album", song.album) + InfoRow("Duration", song.duration?.let(DateUtils::formatElapsedTime)) + InfoRow("Track", song.track?.let { track -> song.disc?.let { "$it.$track" } ?: track.toString() }) + InfoRow("Genre", song.genre) + InfoRow("Starred", if (song.starred != null) "Yes" else null) + if (download != null) { + InfoRow("Download", downloadStatus(download.status)) + InfoRow("Size", localFile?.length()?.takeIf { it > 0 }?.let { Formatter.formatFileSize(context, it) }) + download.error?.let { InfoRow("Error", it) } + } + shown?.let { enc -> + InfoRow("Format", enc.format) + InfoRow("Bitrate", enc.bitrate?.takeIf { it > 0 }?.let { "${it / 1000} kbps" }) + InfoRow("Sample rate", enc.sampleRate?.takeIf { it > 0 }?.let { "$it Hz" }) + InfoRow("Channels", enc.channels?.takeIf { it > 0 }?.toString()) + } + } + }, + confirmButton = { TextButton(onClick = onDismiss) { Text("Close") } }, + ) +} + +@Composable +private fun InfoRow( + label: String, + value: String?, +) { + if (value.isNullOrBlank()) return + Row(Modifier.fillMaxWidth().padding(vertical = 2.dp)) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(end = 16.dp), + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f), + ) + } +} + +private fun downloadStatus(status: DownloadStatus): String = + when (status) { + DownloadStatus.Completed -> "Downloaded" + DownloadStatus.Queued -> "Queued" + DownloadStatus.Running -> "Downloading" + DownloadStatus.Failed -> "Failed" + } diff --git a/app/src/main/kotlin/com/subtracks/ui/downloads/DownloadsScreen.kt b/app/src/main/kotlin/com/subtracks/ui/downloads/DownloadsScreen.kt index 6feb4bc3..924c7abd 100644 --- a/app/src/main/kotlin/com/subtracks/ui/downloads/DownloadsScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/downloads/DownloadsScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -37,6 +38,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.subtracks.data.model.AudioEncoding import com.subtracks.data.model.DownloadList import com.subtracks.data.model.DownloadStatus import com.subtracks.data.model.DownloadedSong @@ -68,6 +70,7 @@ fun DownloadsRoute( var pending by remember { mutableStateOf(null) } DownloadsScreen( tree = tree, + loadEncoding = viewModel::encoding, onBack = onBack, onDelete = { target -> when (target) { @@ -105,6 +108,7 @@ fun DownloadsRoute( @Composable fun DownloadsScreen( tree: DownloadTree, + loadEncoding: suspend (DownloadedSong) -> AudioEncoding?, onBack: () -> Unit, onDelete: (DownloadDeleteTarget) -> Unit, modifier: Modifier = Modifier, @@ -177,7 +181,7 @@ fun DownloadsScreen( } if (albumKey in expanded) { items(album.songs, key = { "$albumKey:${it.songId}" }) { song -> - SongRow(song) { onDelete(DownloadDeleteTarget.Songs(listOf(song.songId))) } + SongRow(song, loadEncoding) { onDelete(DownloadDeleteTarget.Songs(listOf(song.songId))) } } } } @@ -239,11 +243,16 @@ private fun NodeRow( @Composable private fun SongRow( song: DownloadedSong, + loadEncoding: suspend (DownloadedSong) -> AudioEncoding?, onDelete: () -> Unit, ) { + var encoding by remember(song.songId) { mutableStateOf(null) } + LaunchedEffect(song.songId, song.status) { + encoding = if (song.status == DownloadStatus.Completed) loadEncoding(song) else null + } ListItem( headlineContent = { Text(song.title, maxLines = 1, overflow = TextOverflow.Ellipsis) }, - supportingContent = { Text(statusLabel(song), maxLines = 1, overflow = TextOverflow.Ellipsis) }, + supportingContent = { Text(songSubtitle(song, encoding), maxLines = 1, overflow = TextOverflow.Ellipsis) }, trailingContent = { IconButton(onClick = onDelete) { Icon(Icons.Rounded.Delete, contentDescription = "Delete download") @@ -255,13 +264,31 @@ private fun SongRow( } @Composable -private fun statusLabel(song: DownloadedSong): String { +private fun songSubtitle( + song: DownloadedSong, + encoding: AudioEncoding?, +): String { val size = size(song.size) return when (song.status) { - DownloadStatus.Completed -> size - DownloadStatus.Queued -> "Queued · $size" - DownloadStatus.Running -> "Downloading · $size" - DownloadStatus.Failed -> "Failed · $size" + DownloadStatus.Completed -> { + listOfNotNull( + encoding?.format, + encoding?.bitrate?.takeIf { it > 0 }?.let { "${it / 1000} kbps" }, + size, + ).joinToString(" · ") + } + + DownloadStatus.Queued -> { + "Queued · $size" + } + + DownloadStatus.Running -> { + "Downloading · $size" + } + + DownloadStatus.Failed -> { + "Failed · $size" + } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/downloads/DownloadsViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/downloads/DownloadsViewModel.kt index 224fca11..3816d8e0 100644 --- a/app/src/main/kotlin/com/subtracks/ui/downloads/DownloadsViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/downloads/DownloadsViewModel.kt @@ -2,6 +2,8 @@ package com.subtracks.ui.downloads import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.subtracks.data.media.readAudioEncoding +import com.subtracks.data.model.AudioEncoding import com.subtracks.data.model.DownloadList import com.subtracks.data.model.DownloadedSong import com.subtracks.data.repo.DownloadRepository @@ -76,6 +78,9 @@ class DownloadsViewModel( playbackController.refreshMediaItems() } } + + suspend fun encoding(song: DownloadedSong): AudioEncoding? = + downloadRepository.localFile(song.sourceId, song.songId)?.let { readAudioEncoding(it) } } internal fun buildTree(songs: List): DownloadTree = diff --git a/app/src/test/kotlin/com/subtracks/data/model/AudioEncodingTest.kt b/app/src/test/kotlin/com/subtracks/data/model/AudioEncodingTest.kt new file mode 100644 index 00000000..f0beb8fa --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/data/model/AudioEncodingTest.kt @@ -0,0 +1,25 @@ +package com.subtracks.data.model + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class AudioEncodingTest { + @Test + fun commonTypesGetReadableNames() { + assertEquals("MP3", AudioEncoding("audio/mpeg").format) + assertEquals("FLAC", AudioEncoding("audio/flac").format) + assertEquals("AAC", AudioEncoding("audio/mp4").format) + assertEquals("Opus", AudioEncoding("audio/opus").format) + } + + @Test + fun anUnknownTypeFallsBackToItsSubtype() { + assertEquals("X-CUSTOM", AudioEncoding("audio/x-custom").format) + } + + @Test + fun noTypeMeansNoFormat() { + assertNull(AudioEncoding(null).format) + } +} diff --git a/app/src/test/kotlin/com/subtracks/ui/downloads/DownloadTreeTest.kt b/app/src/test/kotlin/com/subtracks/ui/downloads/DownloadTreeTest.kt index f4bc5023..3dfb5e6c 100644 --- a/app/src/test/kotlin/com/subtracks/ui/downloads/DownloadTreeTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/downloads/DownloadTreeTest.kt @@ -54,6 +54,7 @@ class DownloadTreeTest { size: Long, ) = DownloadedSong( songId = id, + sourceId = 1, title = title, albumId = albumId, albumName = albumName,