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.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 */
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue