feat: scan media store for pairing
This commit is contained in:
@@ -13,6 +13,7 @@ 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.MediaStoreMp3Scanner
|
||||
import at.lockstep.player.util.Mp3EmbeddedMetadata
|
||||
import at.lockstep.player.util.Mp3FolderCandidate
|
||||
import at.lockstep.player.util.TrackFileMatching
|
||||
@@ -293,25 +294,7 @@ class LockstepViewModel(
|
||||
}
|
||||
|
||||
val tracks = loadJukeboxTracksEnsuringDetail(playlistId)
|
||||
val withId = tracks.count { !it.trackId.isNullOrBlank() }
|
||||
val withTitle = tracks.count { !it.trackName.isNullOrBlank() }
|
||||
val withArtist = tracks.count { !it.artistName.isNullOrBlank() }
|
||||
Log.d(
|
||||
TAG,
|
||||
"jukebox DB rows for playlist: total=${tracks.size} withSpotifyTrackId=$withId " +
|
||||
"withTitle=$withTitle withArtist=$withArtist",
|
||||
)
|
||||
tracks.take(12).forEach { row ->
|
||||
Log.d(
|
||||
TAG,
|
||||
" jukebox row pos=${row.position} id=${row.trackId} title='${row.trackName}' " +
|
||||
"artist='${row.artistName}' durationMs=${row.durationMs}",
|
||||
)
|
||||
}
|
||||
if (tracks.size > 12) {
|
||||
Log.d(TAG, " … ${tracks.size - 12} more rows omitted from log")
|
||||
}
|
||||
|
||||
logJukeboxTracks(tracks)
|
||||
val (mp3Total, pool) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val mp3s = FolderMp3Scanner.listMp3Uris(context, treeUri)
|
||||
@@ -328,75 +311,117 @@ class LockstepViewModel(
|
||||
}.toMutableList()
|
||||
Pair(mp3s.size, p)
|
||||
}
|
||||
|
||||
if (pool.isEmpty()) {
|
||||
Log.w(TAG, "No MP3 candidates after scan — cannot pair")
|
||||
onFinished(
|
||||
FolderPairingResult(
|
||||
paired = 0,
|
||||
failed = 0,
|
||||
jukeboxRowCount = tracks.size,
|
||||
mp3Count = mp3Total,
|
||||
skippedNoSpotifyTrackId = 0,
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"pool id3 coverage: ${pool.count { it.id3Title != null || it.id3Artist != null }}/${pool.size} " +
|
||||
"files have title or artist from tags",
|
||||
)
|
||||
|
||||
var paired = 0
|
||||
var failed = 0
|
||||
var skippedNoSpotifyTrackId = 0
|
||||
for (track in tracks.sortedBy { it.position }) {
|
||||
val tid = track.trackId
|
||||
if (tid.isNullOrBlank()) {
|
||||
skippedNoSpotifyTrackId++
|
||||
failed++
|
||||
Log.d(
|
||||
TAG,
|
||||
"skip row pos=${track.position}: no Spotify track id " +
|
||||
"(title='${track.trackName}' artist='${track.artistName}')",
|
||||
)
|
||||
continue
|
||||
}
|
||||
val title = track.trackName.orEmpty()
|
||||
val artist = track.artistName.orEmpty()
|
||||
val pick = TrackFileMatching.bestMatchForFolder(title, artist, pool)
|
||||
if (pick == null) {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
val err =
|
||||
withContext(Dispatchers.IO) { AudioUriValidator.validateReadableAudio(context, pick) }
|
||||
if (err != null) {
|
||||
Log.w(TAG, "match candidate failed validation spotifyTid=$tid local=$pick err=$err")
|
||||
upsertPairing(playlistId, tid, null, err)
|
||||
failed++
|
||||
} else {
|
||||
upsertPairing(playlistId, tid, pick.toString(), null)
|
||||
paired++
|
||||
pool.removeAll { it.uri == pick }
|
||||
}
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
"pairPlaylistFromFolder done paired=$paired failed=$failed " +
|
||||
"skippedNoSpotifyTrackId=$skippedNoSpotifyTrackId jukeboxRows=${tracks.size} mp3=$mp3Total",
|
||||
)
|
||||
onFinished(
|
||||
FolderPairingResult(
|
||||
paired = paired,
|
||||
failed = failed,
|
||||
jukeboxRowCount = tracks.size,
|
||||
mp3Count = mp3Total,
|
||||
skippedNoSpotifyTrackId = skippedNoSpotifyTrackId,
|
||||
),
|
||||
)
|
||||
onFinished(matchPlaylistTracks(playlistId, tracks, pool, mp3Total))
|
||||
}
|
||||
}
|
||||
|
||||
/** Pair against all MP3s indexed on the device (Music, Downloads, etc.) — no folder picker needed. */
|
||||
fun pairPlaylistFromDeviceAudio(
|
||||
playlistId: String,
|
||||
onFinished: (FolderPairingResult) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "pairPlaylistFromDeviceAudio playlistId=$playlistId")
|
||||
val tracks = loadJukeboxTracksEnsuringDetail(playlistId)
|
||||
logJukeboxTracks(tracks)
|
||||
val (mp3Total, pool) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val candidates = MediaStoreMp3Scanner.listMp3Candidates(context)
|
||||
Pair(candidates.size, candidates.toMutableList())
|
||||
}
|
||||
onFinished(matchPlaylistTracks(playlistId, tracks, pool, mp3Total))
|
||||
}
|
||||
}
|
||||
|
||||
private fun logJukeboxTracks(tracks: List<TrackRow>) {
|
||||
val withId = tracks.count { !it.trackId.isNullOrBlank() }
|
||||
val withTitle = tracks.count { !it.trackName.isNullOrBlank() }
|
||||
val withArtist = tracks.count { !it.artistName.isNullOrBlank() }
|
||||
Log.d(
|
||||
TAG,
|
||||
"jukebox DB rows for playlist: total=${tracks.size} withSpotifyTrackId=$withId " +
|
||||
"withTitle=$withTitle withArtist=$withArtist",
|
||||
)
|
||||
tracks.take(12).forEach { row ->
|
||||
Log.d(
|
||||
TAG,
|
||||
" jukebox row pos=${row.position} id=${row.trackId} title='${row.trackName}' " +
|
||||
"artist='${row.artistName}' durationMs=${row.durationMs}",
|
||||
)
|
||||
}
|
||||
if (tracks.size > 12) {
|
||||
Log.d(TAG, " … ${tracks.size - 12} more rows omitted from log")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun matchPlaylistTracks(
|
||||
playlistId: String,
|
||||
tracks: List<TrackRow>,
|
||||
pool: MutableList<Mp3FolderCandidate>,
|
||||
mp3Total: Int,
|
||||
): FolderPairingResult {
|
||||
if (pool.isEmpty()) {
|
||||
Log.w(TAG, "No MP3 candidates after scan — cannot pair")
|
||||
return FolderPairingResult(
|
||||
paired = 0,
|
||||
failed = 0,
|
||||
jukeboxRowCount = tracks.size,
|
||||
mp3Count = mp3Total,
|
||||
skippedNoSpotifyTrackId = 0,
|
||||
)
|
||||
}
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"pool id3 coverage: ${pool.count { it.id3Title != null || it.id3Artist != null }}/${pool.size} " +
|
||||
"files have title or artist from tags",
|
||||
)
|
||||
|
||||
var paired = 0
|
||||
var failed = 0
|
||||
var skippedNoSpotifyTrackId = 0
|
||||
for (track in tracks.sortedBy { it.position }) {
|
||||
val tid = track.trackId
|
||||
if (tid.isNullOrBlank()) {
|
||||
skippedNoSpotifyTrackId++
|
||||
failed++
|
||||
Log.d(
|
||||
TAG,
|
||||
"skip row pos=${track.position}: no Spotify track id " +
|
||||
"(title='${track.trackName}' artist='${track.artistName}')",
|
||||
)
|
||||
continue
|
||||
}
|
||||
val title = track.trackName.orEmpty()
|
||||
val artist = track.artistName.orEmpty()
|
||||
val pick = TrackFileMatching.bestMatchForFolder(title, artist, pool)
|
||||
if (pick == null) {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
val err =
|
||||
withContext(Dispatchers.IO) { AudioUriValidator.validateReadableAudio(context, pick) }
|
||||
if (err != null) {
|
||||
Log.w(TAG, "match candidate failed validation spotifyTid=$tid local=$pick err=$err")
|
||||
upsertPairing(playlistId, tid, null, err)
|
||||
failed++
|
||||
} else {
|
||||
upsertPairing(playlistId, tid, pick.toString(), null)
|
||||
paired++
|
||||
pool.removeAll { it.uri == pick }
|
||||
}
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
"playlist pairing done paired=$paired failed=$failed " +
|
||||
"skippedNoSpotifyTrackId=$skippedNoSpotifyTrackId jukeboxRows=${tracks.size} mp3=$mp3Total",
|
||||
)
|
||||
return FolderPairingResult(
|
||||
paired = paired,
|
||||
failed = failed,
|
||||
jukeboxRowCount = tracks.size,
|
||||
mp3Count = mp3Total,
|
||||
skippedNoSpotifyTrackId = skippedNoSpotifyTrackId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user