package com.vgmlr.jamb
import android.content.ContentUris
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
class JambMediaStore(private val context: Context) {
fun queryTracks(): List<JambTrack> {
val out = mutableListOf<JambTrack>()
val collection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.BUCKET_DISPLAY_NAME,
MediaStore.Audio.Media.RELATIVE_PATH,
MediaStore.Audio.Media.SIZE
)
val selection = "${MediaStore.Audio.Media.IS_MUSIC} != 0"
val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} COLLATE NOCASE ASC"
context.contentResolver.query(collection, projection, selection, null, sortOrder)?.use { c ->
val idCol = c.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val nameCol = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
val durCol = c.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val bucketCol = c.getColumnIndexOrThrow(MediaStore.Audio.Media.BUCKET_DISPLAY_NAME)
val relCol = c.getColumnIndexOrThrow(MediaStore.Audio.Media.RELATIVE_PATH)
val sizeCol = c.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
while (c.moveToNext()) {
val id = c.getLong(idCol)
val name = c.getString(nameCol) ?: continue
val dur = c.getLong(durCol)
val folder = c.getString(bucketCol) ?: "Unknown"
val relPath = c.getString(relCol) ?: ""
val size = c.getLong(sizeCol)
val uri = ContentUris.withAppendedId(collection, id).toString()
out.add(JambTrack(id, uri, name, folder, relPath, dur, size))
}
}
return out
}
fun matchTrack(uri: Uri, master: List<JambTrack>): JambTrack? {
if (uri.authority == "media") {
val id = runCatching { ContentUris.parseId(uri) }.getOrNull()
if (id != null && id >= 0) {
master.firstOrNull { it.id == id }?.let { return it }
}
}
var name: String? = null
var size = -1L
runCatching {
context.contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE),
null, null, null
)?.use { c ->
if (c.moveToFirst()) {
val nCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val sCol = c.getColumnIndex(OpenableColumns.SIZE)
if (nCol >= 0 && !c.isNull(nCol)) name = c.getString(nCol)
if (sCol >= 0 && !c.isNull(sCol)) size = c.getLong(sCol)
}
}
}
if (name == null && uri.scheme == "file") {
name = uri.lastPathSegment
}
val display = name ?: return null
master.firstOrNull { it.displayName == display && size >= 0 && it.sizeBytes == size }
?.let { return it }
return master.firstOrNull { it.displayName == display }
}
suspend fun forceScan(uri: Uri): Uri? {
val path = resolvePath(uri) ?: return null
return scanFile(path)
}
private fun resolvePath(uri: Uri): String? {
if (uri.scheme == "file") return uri.path
if (uri.scheme == "content" &&
uri.authority == "com.android.externalstorage.documents"
) {
val docId = runCatching { DocumentsContract.getDocumentId(uri) }.getOrNull()
?: return null
val split = docId.split(":", limit = 2)
if (split.size == 2 && split[0].equals("primary", ignoreCase = true)) {
return Environment.getExternalStorageDirectory().absolutePath + "/" + split[1]
}
}
return null
}
private suspend fun scanFile(path: String): Uri? =
suspendCancellableCoroutine { cont ->
MediaScannerConnection.scanFile(context, arrayOf(path), null) { _, resultUri ->
if (cont.isActive) cont.resume(resultUri)
}
}
}