feat: beat annotator

This commit is contained in:
2026-05-15 09:03:20 +02:00
parent 7beea662b6
commit 848f5919c8
12 changed files with 640 additions and 42 deletions

View File

@@ -10,6 +10,8 @@ import at.lockstep.jukebox.db.TrackRow
import at.lockstep.player.data.UserPreferencesRepository
import at.lockstep.player.data.db.TrackPairingEntity
import at.lockstep.player.util.AudioUriValidator
import at.lockstep.player.playback.TrackBoundaryEvent
import at.lockstep.player.util.BeatAnnotationStorage
import at.lockstep.player.util.FolderMp3Scanner
import at.lockstep.player.util.Mp3EmbeddedMetadata
import at.lockstep.player.util.Mp3FolderCandidate
@@ -56,6 +58,19 @@ class LockstepViewModel(
null,
)
val annotationMode: StateFlow<Boolean> =
prefs.annotationMode.stateIn(
viewModelScope,
SharingStarted.Eagerly,
false,
)
fun setAnnotationMode(enabled: Boolean) {
viewModelScope.launch {
prefs.setAnnotationMode(enabled)
}
}
private val context get() = getApplication<Application>()
/**
@@ -120,6 +135,45 @@ class LockstepViewModel(
suspend fun hasPairedTracks(playlistId: String): Boolean = pairingDao.countPaired(playlistId) > 0
suspend fun getPlaylistDisplayName(playlistId: String): String =
withContext(Dispatchers.IO) {
app.playlistRepository.getPlaylists()
.find { it.id == playlistId }
?.name
?.trim()
.orEmpty()
.ifBlank { "playlist" }
}
/**
* Writes one JSON file under app files dir ([BeatAnnotationStorage]) for the track described
* by [event]. Skips when [beatTimesMs] is empty.
*/
fun persistBeatAnnotation(
playlistId: String,
playlistDisplayName: String,
event: TrackBoundaryEvent,
beatTimesMs: List<Long>,
) {
if (beatTimesMs.isEmpty()) {
return
}
viewModelScope.launch(Dispatchers.IO) {
val pairing = pairingDao.findForTrack(playlistId, event.trackId)
val docId = BeatAnnotationStorage.mp3DocumentContentId(pairing?.localUri)
val contentId = docId.ifBlank { event.trackId }
BeatAnnotationStorage.writeAnnotationsFile(
context = getApplication(),
playlistDisplayName = playlistDisplayName,
trackQueueIndex0Based = event.queueIndex,
contentId = contentId,
title = event.title,
artist = event.artist,
beatTimesMs = beatTimesMs,
)
}
}
suspend fun syncJukeboxIfToken(): String? {
val token = spotifyAccessToken.value
if (token.isNullOrBlank()) {