Skip to content

Commit

Permalink
perf: Improved metadata handling logic
Browse files Browse the repository at this point in the history
Signed-off-by: Gabriel Fontán <gabilessto@gmail.com>
  • Loading branch information
BobbyESP committed May 11, 2024
1 parent 1092fb7 commit 72f1752
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 301 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bobbyesp.ext.joinOrNullToString
import com.bobbyesp.ext.toAudioFileMetadata
import com.bobbyesp.ext.toMinutes
import com.bobbyesp.ext.toModifiableMap
import com.bobbyesp.metadator.R
import com.bobbyesp.metadator.model.ParcelableSong
import com.bobbyesp.metadator.presentation.common.LocalNavController
Expand All @@ -65,6 +66,8 @@ import com.bobbyesp.ui.components.others.MetadataTag
import com.bobbyesp.ui.components.text.LargeCategoryTitle
import com.bobbyesp.ui.components.text.MarqueeText
import com.bobbyesp.ui.components.text.PreConfiguredOutlinedTextField
import com.bobbyesp.utilities.mediastore.AudioFileMetadata.Companion.toAudioFileMetadata
import com.bobbyesp.utilities.mediastore.AudioFileMetadata.Companion.toPropertyMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

Expand All @@ -79,7 +82,8 @@ fun ID3MetadataEditorPage(
val path = parcelableSong.localSongPath
val scope = rememberCoroutineScope()

var propertiesCopy = viewModel.propertiesCopy.value
val metadata = viewState.metadata
val modifiablePropertyMap = viewState.metadata?.propertyMap?.toModifiableMap()

var newArtworkAddress by remember {
mutableStateOf<Uri?>(null)
Expand All @@ -96,11 +100,15 @@ fun ID3MetadataEditorPage(
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartIntentSenderForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
scope.launch(Dispatchers.IO) {
viewModel.saveMetadata(
newMetadata = viewState.metadata?.copy(
propertyMap = propertiesCopy!!.toPropertyMap()
)!!, path = path!!, imageUri = newArtworkAddress
)
modifiablePropertyMap?.let {
viewModel.saveMetadata(
newMetadata = viewState.metadata.copy(
propertyMap = it.toAudioFileMetadata().toPropertyMap()
),
path = path!!,
imageUri = newArtworkAddress
)
}
}
navController.popBackStack()
}
Expand All @@ -110,7 +118,7 @@ fun ID3MetadataEditorPage(

fun saveInMediaStore(): Boolean = viewModel.saveMetadata(
newMetadata = viewState.metadata?.copy(
propertyMap = propertiesCopy!!.toPropertyMap()
propertyMap = modifiablePropertyMap!!.toAudioFileMetadata().toPropertyMap()
)!!, path = path!!, imageUri = newArtworkAddress
) {
val intent = IntentSenderRequest.Builder(it).build()
Expand Down Expand Up @@ -139,9 +147,6 @@ fun ID3MetadataEditorPage(
}, actions = {
IconButton(onClick = {
scope.launch(Dispatchers.IO) {
val results = viewModel.getSpotifyResults(
propertiesCopy?.title?.joinToString() + propertiesCopy?.artist?.joinToString()
)
//TODO: Implement the search results
}
}) {
Expand Down Expand Up @@ -191,11 +196,12 @@ fun ID3MetadataEditorPage(
}

is ID3MetadataEditorPageViewModel.Companion.ID3MetadataEditorPageState.Success -> {
//TODO: SOME FIELDS LIKE ARTIST MIGHT WANT TO BE SAVED IN DIFFERENT INDEXES OF THE ARRAY
var showMediaStoreInfoDialog by remember { mutableStateOf(false) }

val artworkUri = newArtworkAddress ?: parcelableSong.artworkPath

val songProperties = viewState.audioFileMetadata!!
val songProperties = metadata!!.propertyMap.toAudioFileMetadata()
val audioStats = viewState.audioProperties!!

val scrollState = rememberScrollState()
Expand Down Expand Up @@ -293,39 +299,35 @@ fun ID3MetadataEditorPage(
text = stringResource(id = R.string.general_tags)
)
PreConfiguredOutlinedTextField(
value = songProperties.title.joinOrNullToString(),
value = songProperties.title,
label = stringResource(id = R.string.title),
modifier = Modifier.fillMaxWidth()
) { title ->
propertiesCopy?.title = arrayOf(title)
modifiablePropertyMap?.put("TITLE", title)
}

PreConfiguredOutlinedTextField(
value = songProperties.artist.joinOrNullToString(),
value = songProperties.artist,
label = stringResource(id = R.string.artist),
modifier = Modifier.fillMaxWidth()
) { artists ->
propertiesCopy?.artist =
artists.split(",").map { it.trim() }.toTypedArray()

modifiablePropertyMap?.put("ARTIST", artists)
}

PreConfiguredOutlinedTextField(
value = songProperties.album.joinOrNullToString(),
value = songProperties.album,
label = stringResource(id = R.string.album),
modifier = Modifier.fillMaxWidth()
) { album ->
propertiesCopy?.album = arrayOf(album)
modifiablePropertyMap?.put("ALBUM", album)
}

PreConfiguredOutlinedTextField(
value = songProperties.albumArtist.joinOrNullToString(),
value = songProperties.albumArtist,
label = stringResource(id = R.string.album_artist),
modifier = Modifier.fillMaxWidth()
) { artists ->
propertiesCopy?.albumArtist =
artists.split(",").map { it.trim() }.toTypedArray()

modifiablePropertyMap?.put("ALBUMARTIST", artists)
}

Column(
Expand All @@ -335,40 +337,38 @@ fun ID3MetadataEditorPage(
modifier = Modifier.fillMaxWidth(),
) {
PreConfiguredOutlinedTextField(
value = songProperties.trackNumber.joinOrNullToString(),
value = songProperties.trackNumber,
label = stringResource(id = R.string.track_number),
modifier = Modifier.weight(0.5f)
) { trackNumber ->
propertiesCopy?.trackNumber = arrayOf(trackNumber)
modifiablePropertyMap?.put("TRACKNUMBER", trackNumber)
}
Spacer(modifier = Modifier.width(8.dp))
PreConfiguredOutlinedTextField(
value = songProperties.discNumber.joinOrNullToString(),
value = songProperties.discNumber,
label = stringResource(id = R.string.disc_number),
modifier = Modifier.weight(0.5f)
) { discNumber ->
propertiesCopy?.discNumber = arrayOf(discNumber)

modifiablePropertyMap?.put("DISCNUMBER", discNumber)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
) {
PreConfiguredOutlinedTextField(
value = songProperties.date.joinOrNullToString(),
value = songProperties.date,
label = stringResource(id = R.string.date),
modifier = Modifier.weight(0.5f)
) { date ->
propertiesCopy?.date = arrayOf(date)

modifiablePropertyMap?.put("DATE", date)
}
Spacer(modifier = Modifier.width(8.dp))
PreConfiguredOutlinedTextField(
value = songProperties.genre.joinOrNullToString(),
value = songProperties.genre,
label = stringResource(id = R.string.genre),
modifier = Modifier.weight(0.5f)
) { genre ->
propertiesCopy?.genre = arrayOf(genre)
modifiablePropertyMap?.put("GENRE", genre)
}
}
}
Expand All @@ -384,49 +384,46 @@ fun ID3MetadataEditorPage(
modifier = Modifier.fillMaxWidth(),
) {
PreConfiguredOutlinedTextField(
value = songProperties.composer.joinOrNullToString(),
value = songProperties.composer,
label = stringResource(id = R.string.composer),
modifier = Modifier.weight(0.5f)
) { composer ->
propertiesCopy?.composer = arrayOf(composer)
modifiablePropertyMap?.put("COMPOSER", composer)
}
Spacer(modifier = Modifier.width(8.dp))
PreConfiguredOutlinedTextField(
value = songProperties.lyricist.joinOrNullToString(),
value = songProperties.lyricist,
label = stringResource(id = R.string.lyricist),
modifier = Modifier.weight(0.5f)
) { lyricist ->
propertiesCopy?.lyricist = arrayOf(lyricist)
modifiablePropertyMap?.put("LYRICIST", lyricist)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
) {
PreConfiguredOutlinedTextField(
value = songProperties.conductor.joinOrNullToString(),
value = songProperties.conductor,
label = stringResource(id = R.string.conductor),
modifier = Modifier.weight(0.5f)
) { conductor ->
propertiesCopy?.conductor = arrayOf(conductor)
modifiablePropertyMap?.put("CONDUCTOR", conductor)
}
Spacer(modifier = Modifier.width(8.dp))
PreConfiguredOutlinedTextField(
value = songProperties.remixer.joinOrNullToString(),
value = songProperties.remixer,
label = stringResource(id = R.string.remixer),
modifier = Modifier.weight(0.5f)
) { remixer ->
propertiesCopy = propertiesCopy?.copy(
remixer = arrayOf(remixer)
)
modifiablePropertyMap?.put("REMIXER", remixer)
}
}
PreConfiguredOutlinedTextField(
value = songProperties.performer.joinOrNullToString(),
value = songProperties.performer,
label = stringResource(id = R.string.performer),
modifier = Modifier.fillMaxWidth()
) { performer ->
propertiesCopy?.performer =
performer.split(",").map { it.trim() }.toTypedArray()
modifiablePropertyMap?.put("PERFORMER", performer)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ import android.content.Context
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.adamratzman.spotify.models.Track
import com.bobbyesp.metadator.features.spotify.domain.repositories.SearchRepository
import com.bobbyesp.utilities.mediastore.AudioFileMetadata
import com.bobbyesp.utilities.mediastore.AudioFileMetadata.Companion.toAudioFileMetadata
import com.bobbyesp.utilities.mediastore.MediaStoreReceiver
import com.kyant.taglib.AudioProperties
import com.kyant.taglib.AudioPropertiesReadStyle
Expand All @@ -39,44 +36,43 @@ class ID3MetadataEditorPageViewModel @Inject constructor(
private val mutablePageViewState = MutableStateFlow(PageViewState())
val pageViewState = mutablePageViewState.asStateFlow()

val propertiesCopy = mutableStateOf<AudioFileMetadata?>(null)

data class PageViewState(
val metadata: Metadata? = null,
val audioFileMetadata: AudioFileMetadata? = null,
val audioProperties: AudioProperties? = null,
val state: ID3MetadataEditorPageState = ID3MetadataEditorPageState.Loading,
)

suspend fun loadTrackMetadata(path: String) {
updateState(ID3MetadataEditorPageState.Loading)
kotlin.runCatching {
runCatching {
MediaStoreReceiver.getFileDescriptorFromPath(context, path, mode = "r")?.use { songFd ->
val fd = songFd.dup()?.detachFd()
?: throw IllegalStateException("File descriptor is null")

val metadataDeferred =
val metadata =
withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
async {
TagLib.getMetadata(
fd = fd,
readStyle = AudioPropertiesReadStyle.Fast
)
}
}
}.await()
} ?: throw IllegalStateException("Metadata is null")

val metadata = metadataDeferred.await()

if (metadata == null) {
updateState(ID3MetadataEditorPageState.Error(Exception("Metadata is null")))
return
}
val fd2 = songFd.dup()?.detachFd()
?: throw IllegalStateException("File descriptor is null")
val audioProperties =
withContext(viewModelScope.coroutineContext + Dispatchers.IO) {
async {
TagLib.getAudioProperties(
fd = fd2,
readStyle = AudioPropertiesReadStyle.Fast
)
}.await()
} ?: throw IllegalStateException("Audio properties are null")

updateMetadata(metadata)
updateStateMetadata(metadata, audioProperties)

updateState(ID3MetadataEditorPageState.Success)

propertiesCopy.value = metadata.propertyMap.toAudioFileMetadata()
}
}.onFailure { error ->
Log.e(
Expand Down Expand Up @@ -148,15 +144,11 @@ class ID3MetadataEditorPageViewModel @Inject constructor(
description = "Song cover - Metadator",
pictureType = "Cover (front)"
)
val saved = TagLib.savePictures(
fileDescriptorId,
pictures = arrayOf(picture)
)

if (saved) {
Log.i("ID3MetadataEditorPageViewModel", "Saved picture")
} else {
Log.e("ID3MetadataEditorPageViewModel", "Error while trying to save picture")
viewModelScope.launch(Dispatchers.IO) {
TagLib.savePictures(
fileDescriptorId,
pictures = arrayOf(picture)
)
}
}

Expand All @@ -178,12 +170,14 @@ class ID3MetadataEditorPageViewModel @Inject constructor(
}
}

private fun updateMetadata(metadata: Metadata? = null) {
private fun updateStateMetadata(
metadata: Metadata? = null,
audioProperties: AudioProperties? = null
) {
mutablePageViewState.update {
it.copy(
metadata = metadata,
audioFileMetadata = metadata?.propertyMap?.toAudioFileMetadata(),
audioProperties = metadata?.audioProperties
audioProperties = audioProperties
)
}
}
Expand Down
43 changes: 43 additions & 0 deletions app/utilities/src/main/java/com/bobbyesp/ext/PropertyMap.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.bobbyesp.ext

import com.bobbyesp.utilities.mediastore.AudioFileMetadata

typealias PropertyMap = HashMap<String, Array<String>>

fun PropertyMap.toAudioFileMetadata(): AudioFileMetadata {
return AudioFileMetadata(
title = this["TITLE"]?.getOrNull(0),
artist = this["ARTIST"]?.getOrNull(0),
album = this["ALBUM"]?.getOrNull(0),
albumArtist = this["ALBUMARTIST"]?.getOrNull(0),
trackNumber = this["TRACKNUMBER"]?.getOrNull(0),
discNumber = this["DISCNUMBER"]?.getOrNull(0),
date = this["DATE"]?.getOrNull(0),
genre = this["GENRE"]?.getOrNull(0),
composer = this["COMPOSER"]?.getOrNull(0),
lyricist = this["LYRICIST"]?.getOrNull(0),
performer = this["PERFORMER"]?.getOrNull(0),
conductor = this["CONDUCTOR"]?.getOrNull(0),
remixer = this["REMIXER"]?.getOrNull(0),
comment = this["COMMENT"]?.getOrNull(0),
)
}

fun PropertyMap.toModifiableMap(): MutableMap<String, String?> {
return mutableMapOf(
"TITLE" to this["TITLE"]?.getOrNull(0),
"ARTIST" to this["ARTIST"]?.getOrNull(0),
"ALBUM" to this["ALBUM"]?.getOrNull(0),
"ALBUMARTIST" to this["ALBUMARTIST"]?.getOrNull(0),
"TRACKNUMBER" to this["TRACKNUMBER"]?.getOrNull(0),
"DISCNUMBER" to this["DISCNUMBER"]?.getOrNull(0),
"DATE" to this["DATE"]?.getOrNull(0),
"GENRE" to this["GENRE"]?.getOrNull(0),
"COMPOSER" to this["COMPOSER"]?.getOrNull(0),
"LYRICIST" to this["LYRICIST"]?.getOrNull(0),
"PERFORMER" to this["PERFORMER"]?.getOrNull(0),
"CONDUCTOR" to this["CONDUCTOR"]?.getOrNull(0),
"REMIXER" to this["REMIXER"]?.getOrNull(0),
"COMMENT" to this["COMMENT"]?.getOrNull(0),
)
}
Loading

0 comments on commit 72f1752

Please sign in to comment.