music: use saf fields in raw song
This commit is contained in:
parent
5b447f7efb
commit
cadd2d1231
3 changed files with 76 additions and 66 deletions
|
@ -22,6 +22,7 @@ import java.util.UUID
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.fs.DeviceFile
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
|
@ -32,23 +33,9 @@ import org.oxycblt.auxio.music.info.ReleaseType
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
data class RawSong(
|
||||
/**
|
||||
* The ID of the [SongImpl]'s audio file, obtained from MediaStore. Note that this ID is highly
|
||||
* unstable and should only be used for accessing the audio file.
|
||||
*/
|
||||
var mediaStoreId: Long? = null,
|
||||
/** @see Song.dateAdded */
|
||||
var dateAdded: Long? = null,
|
||||
/** The latest date the [SongImpl]'s audio file was modified, as a unix epoch timestamp. */
|
||||
var dateModified: Long? = null,
|
||||
/** @see Song.path */
|
||||
var path: Path? = null,
|
||||
/** @see Song.size */
|
||||
var size: Long? = null,
|
||||
val file: DeviceFile,
|
||||
/** @see Song.durationMs */
|
||||
var durationMs: Long? = null,
|
||||
/** @see Song.mimeType */
|
||||
var extensionMimeType: String? = null,
|
||||
/** @see Song.replayGainAdjustment */
|
||||
var replayGainTrackAdjustment: Float? = null,
|
||||
/** @see Song.replayGainAdjustment */
|
||||
|
|
|
@ -1,12 +1,29 @@
|
|||
package org.oxycblt.auxio.music.fs
|
||||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* DeviceFiles.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.fs
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
|
@ -14,7 +31,6 @@ import kotlinx.coroutines.flow.emitAll
|
|||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
|
||||
interface DeviceFiles {
|
||||
fun explore(uris: Flow<Uri>): Flow<DeviceFile>
|
||||
|
@ -24,17 +40,15 @@ interface DeviceFiles {
|
|||
class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
||||
DeviceFiles {
|
||||
private val contentResolver = context.contentResolverSafe
|
||||
|
||||
override fun explore(uris: Flow<Uri>): Flow<DeviceFile> =
|
||||
uris.flatMapMerge { rootUri ->
|
||||
exploreImpl(contentResolver, rootUri, Components.nil())
|
||||
}
|
||||
uris.flatMapMerge { rootUri -> exploreImpl(contentResolver, rootUri, Components.nil()) }
|
||||
|
||||
private fun exploreImpl(
|
||||
contentResolver: ContentResolver,
|
||||
uri: Uri,
|
||||
relativePath: Components
|
||||
): Flow<DeviceFile> =
|
||||
flow {
|
||||
): Flow<DeviceFile> = flow {
|
||||
contentResolver.useQuery(uri, PROJECTION) { cursor ->
|
||||
val childUriIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
|
@ -42,6 +56,9 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
|
|||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val mimeTypeIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE)
|
||||
val lastModifiedIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
// Recurse into sub-directories as another flow
|
||||
val recursions = mutableListOf<Flow<DeviceFile>>()
|
||||
while (cursor.moveToNext()) {
|
||||
|
@ -58,7 +75,9 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
|
|||
// Immediately emit all files given that it's just an O(1) op.
|
||||
// This also just makes sure the outer flow has a reason to exist
|
||||
// rather than just being a glorified async.
|
||||
emit(DeviceFile(childUri, mimeType, path))
|
||||
val lastModified = cursor.getLong(lastModifiedIndex)
|
||||
val size = cursor.getLong(sizeIndex)
|
||||
emit(DeviceFile(childUri, mimeType, path, lastModified))
|
||||
}
|
||||
}
|
||||
// Hypothetically, we could just emitAll as we recurse into a new directory,
|
||||
|
@ -71,12 +90,15 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
|
|||
}
|
||||
|
||||
private companion object {
|
||||
val PROJECTION = arrayOf(
|
||||
val PROJECTION =
|
||||
arrayOf(
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceFile(val uri: Uri, val mimeType: String, val path: Components)
|
||||
data class DeviceFile(val uri: Uri, val mimeType: String, val path: Components, val size: Long, val lastModified: Long)
|
||||
|
|
|
@ -88,13 +88,14 @@ inline fun <reified R> ContentResolver.mapQuery(
|
|||
selector: String? = null,
|
||||
args: Array<String>? = null,
|
||||
crossinline transform: Cursor.() -> R
|
||||
) = useQuery(uri, projection, selector, args) {
|
||||
) =
|
||||
useQuery(uri, projection, selector, args) {
|
||||
sequence<R> {
|
||||
while (it.moveToNext()) {
|
||||
yield(it.transform())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */
|
||||
private val externalCoversUri = Uri.parse("content://media/external/audio/albumart")
|
||||
|
|
Loading…
Reference in a new issue