mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
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.
This commit is contained in:
@@ -104,7 +104,7 @@ interface DownloadDao {
|
||||
fun playlistStatuses(sourceId: Long): Flow<List<EntityDownloadStatus>>
|
||||
|
||||
@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 " +
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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?,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<QueueItem>,
|
||||
startIndex: Int,
|
||||
|
||||
@@ -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<Long> = _positionMs
|
||||
|
||||
fun currentAudioEncoding(): AudioEncoding? = player?.audioEncoding
|
||||
|
||||
private var player: PlayerHandle? = null
|
||||
private var connecting = false
|
||||
private var pendingPlay: PendingPlay? = null
|
||||
|
||||
@@ -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<QueueItem>,
|
||||
startIndex: Int,
|
||||
|
||||
@@ -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<PlaybackController>()
|
||||
val libraryRepository = koinInject<LibraryRepository>()
|
||||
val downloadRepository = koinInject<DownloadRepository>()
|
||||
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<Job?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val contextMenuHost = remember { ContextMenuHost() }
|
||||
var infoSong by remember { mutableStateOf<Song?>(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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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<AudioEncoding?>(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"
|
||||
}
|
||||
@@ -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<DownloadDeleteTarget?>(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<AudioEncoding?>(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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DownloadedSong>): DownloadTree =
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ class DownloadTreeTest {
|
||||
size: Long,
|
||||
) = DownloadedSong(
|
||||
songId = id,
|
||||
sourceId = 1,
|
||||
title = title,
|
||||
albumId = albumId,
|
||||
albumName = albumName,
|
||||
|
||||
Reference in New Issue
Block a user