music: use saf fields in raw song

This commit is contained in:
Alexander Capehart 2024-11-19 10:16:09 -07:00
parent 5b447f7efb
commit cadd2d1231
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 76 additions and 66 deletions

View file

@ -22,6 +22,7 @@ import java.util.UUID
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song 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.fs.Path
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
import org.oxycblt.auxio.music.info.ReleaseType import org.oxycblt.auxio.music.info.ReleaseType
@ -32,23 +33,9 @@ import org.oxycblt.auxio.music.info.ReleaseType
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
data class RawSong( data class RawSong(
/** val file: DeviceFile,
* 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,
/** @see Song.durationMs */ /** @see Song.durationMs */
var durationMs: Long? = null, var durationMs: Long? = null,
/** @see Song.mimeType */
var extensionMimeType: String? = null,
/** @see Song.replayGainAdjustment */ /** @see Song.replayGainAdjustment */
var replayGainTrackAdjustment: Float? = null, var replayGainTrackAdjustment: Float? = null,
/** @see Song.replayGainAdjustment */ /** @see Song.replayGainAdjustment */

View file

@ -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.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
@ -14,7 +31,6 @@ import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import javax.inject.Inject
interface DeviceFiles { interface DeviceFiles {
fun explore(uris: Flow<Uri>): Flow<DeviceFile> fun explore(uris: Flow<Uri>): Flow<DeviceFile>
@ -24,17 +40,15 @@ interface DeviceFiles {
class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) : class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) :
DeviceFiles { DeviceFiles {
private val contentResolver = context.contentResolverSafe private val contentResolver = context.contentResolverSafe
override fun explore(uris: Flow<Uri>): Flow<DeviceFile> = override fun explore(uris: Flow<Uri>): Flow<DeviceFile> =
uris.flatMapMerge { rootUri -> uris.flatMapMerge { rootUri -> exploreImpl(contentResolver, rootUri, Components.nil()) }
exploreImpl(contentResolver, rootUri, Components.nil())
}
private fun exploreImpl( private fun exploreImpl(
contentResolver: ContentResolver, contentResolver: ContentResolver,
uri: Uri, uri: Uri,
relativePath: Components relativePath: Components
): Flow<DeviceFile> = ): Flow<DeviceFile> = flow {
flow {
contentResolver.useQuery(uri, PROJECTION) { cursor -> contentResolver.useQuery(uri, PROJECTION) { cursor ->
val childUriIndex = val childUriIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) 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) cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
val mimeTypeIndex = val mimeTypeIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) 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 // Recurse into sub-directories as another flow
val recursions = mutableListOf<Flow<DeviceFile>>() val recursions = mutableListOf<Flow<DeviceFile>>()
while (cursor.moveToNext()) { 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. // 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 // This also just makes sure the outer flow has a reason to exist
// rather than just being a glorified async. // 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, // 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 { private companion object {
val PROJECTION = arrayOf( val PROJECTION =
arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE, 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)

View file

@ -88,13 +88,14 @@ inline fun <reified R> ContentResolver.mapQuery(
selector: String? = null, selector: String? = null,
args: Array<String>? = null, args: Array<String>? = null,
crossinline transform: Cursor.() -> R crossinline transform: Cursor.() -> R
) = useQuery(uri, projection, selector, args) { ) =
useQuery(uri, projection, selector, args) {
sequence<R> { sequence<R> {
while (it.moveToNext()) { while (it.moveToNext()) {
yield(it.transform()) yield(it.transform())
} }
} }
} }
/** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */ /** 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") private val externalCoversUri = Uri.parse("content://media/external/audio/albumart")