all: eliminate refactor errors
This commit is contained in:
parent
f76eafc9d4
commit
556c5d5e0a
19 changed files with 300 additions and 396 deletions
|
@ -30,12 +30,12 @@ import kotlinx.parcelize.Parcelize
|
|||
import org.oxycblt.auxio.image.extractor.Cover
|
||||
import org.oxycblt.auxio.image.extractor.ParentCover
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.concatLocalized
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
|
|
|
@ -21,27 +21,20 @@ package org.oxycblt.auxio.music
|
|||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.util.LinkedList
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.yield
|
||||
import org.oxycblt.auxio.music.cache.CacheRepository
|
||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||
import org.oxycblt.auxio.music.device.RawSong
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
||||
import org.oxycblt.auxio.music.stack.Indexer
|
||||
import org.oxycblt.auxio.music.user.MutableUserLibrary
|
||||
import org.oxycblt.auxio.music.user.UserLibrary
|
||||
import org.oxycblt.auxio.util.DEFAULT_TIMEOUT
|
||||
import org.oxycblt.auxio.util.forEachWithTimeout
|
||||
import timber.log.Timber as L
|
||||
|
||||
/**
|
||||
|
@ -220,9 +213,7 @@ interface MusicRepository {
|
|||
class MusicRepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val cacheRepository: CacheRepository,
|
||||
private val mediaStoreExtractor: MediaStoreExtractor,
|
||||
private val tagExtractor: TagExtractor,
|
||||
private val indexer: Indexer,
|
||||
private val deviceLibraryFactory: DeviceLibrary.Factory,
|
||||
private val userLibraryFactory: UserLibrary.Factory,
|
||||
private val musicSettings: MusicSettings
|
||||
|
@ -355,9 +346,6 @@ constructor(
|
|||
}
|
||||
|
||||
private suspend fun indexImpl(context: Context, scope: CoroutineScope, withCache: Boolean) {
|
||||
// TODO: Find a way to break up this monster of a method, preferably as another class.
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
// Make sure we have permissions before going forward. Theoretically this would be better
|
||||
// done at the UI level, but that intertwines logic and display too much.
|
||||
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
|
||||
|
@ -367,8 +355,6 @@ constructor(
|
|||
}
|
||||
|
||||
// Obtain configuration information
|
||||
val constraints =
|
||||
MediaStoreExtractor.Constraints(musicSettings.excludeNonMusic, musicSettings.musicDirs)
|
||||
val separators = Separators.from(musicSettings.separators)
|
||||
val nameFactory =
|
||||
if (musicSettings.intelligentSorting) {
|
||||
|
@ -377,174 +363,7 @@ constructor(
|
|||
Name.Known.SimpleFactory
|
||||
}
|
||||
|
||||
// Begin with querying MediaStore and the music cache. The former is needed for Auxio
|
||||
// to figure out what songs are (probably) on the device, and the latter will be needed
|
||||
// for discovery (described later). These have no shared state, so they are done in
|
||||
// parallel.
|
||||
L.d("Starting MediaStore query")
|
||||
emitIndexingProgress(IndexingProgress.Indeterminate)
|
||||
|
||||
val mediaStoreQueryJob =
|
||||
scope.async {
|
||||
val query =
|
||||
try {
|
||||
mediaStoreExtractor.query(constraints)
|
||||
} catch (e: Exception) {
|
||||
// Normally, errors in an async call immediately bubble up to the Looper
|
||||
// and crash the app. Thus, we have to wrap any error into a Result
|
||||
// and then manually forward it to the try block that indexImpl is
|
||||
// called from.
|
||||
return@async Result.failure(e)
|
||||
}
|
||||
Result.success(query)
|
||||
}
|
||||
// Since this main thread is a co-routine, we can do operations in parallel in a way
|
||||
// identical to calling async.
|
||||
val cache =
|
||||
if (withCache) {
|
||||
L.d("Reading cache")
|
||||
cacheRepository.readCache()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
L.d("Awaiting MediaStore query")
|
||||
val query = mediaStoreQueryJob.await().getOrThrow()
|
||||
|
||||
// We now have all the information required to start the "discovery" process. This
|
||||
// is the point at which Auxio starts scanning each file given from MediaStore and
|
||||
// transforming it into a music library. MediaStore normally
|
||||
L.d("Starting discovery")
|
||||
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED) // Not fully populated w/metadata
|
||||
val completeSongs = Channel<RawSong>(Channel.UNLIMITED) // Populated with quality metadata
|
||||
val processedSongs = Channel<RawSong>(Channel.UNLIMITED) // Transformed into SongImpl
|
||||
|
||||
// MediaStoreExtractor discovers all music on the device, and forwards them to either
|
||||
// DeviceLibrary if cached metadata exists for it, or TagExtractor if cached metadata
|
||||
// does not exist. In the latter situation, it also applies it's own (inferior) metadata.
|
||||
L.d("Starting MediaStore discovery")
|
||||
val mediaStoreJob =
|
||||
scope.async {
|
||||
try {
|
||||
mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
|
||||
} catch (e: Exception) {
|
||||
// To prevent a deadlock, we want to close the channel with an exception
|
||||
// to cascade to and cancel all other routines before finally bubbling up
|
||||
// to the main extractor loop.
|
||||
L.e("MediaStore extraction failed: $e")
|
||||
incompleteSongs.close(
|
||||
Exception("MediaStore extraction failed: ${e.stackTraceToString()}"))
|
||||
return@async
|
||||
}
|
||||
incompleteSongs.close()
|
||||
}
|
||||
|
||||
// TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date
|
||||
// metadata for them, and then forwards it to DeviceLibrary.
|
||||
L.d("Starting tag extraction")
|
||||
val tagJob =
|
||||
scope.async {
|
||||
try {
|
||||
tagExtractor.consume(incompleteSongs, completeSongs)
|
||||
} catch (e: Exception) {
|
||||
L.e("Tag extraction failed: $e")
|
||||
completeSongs.close(
|
||||
Exception("Tag extraction failed: ${e.stackTraceToString()}"))
|
||||
return@async
|
||||
}
|
||||
completeSongs.close()
|
||||
}
|
||||
|
||||
// DeviceLibrary constructs music parent instances as song information is provided,
|
||||
// and then forwards them to the primary loading loop.
|
||||
L.d("Starting DeviceLibrary creation")
|
||||
val deviceLibraryJob =
|
||||
scope.async(Dispatchers.Default) {
|
||||
val deviceLibrary =
|
||||
try {
|
||||
deviceLibraryFactory.create(
|
||||
completeSongs, processedSongs, separators, nameFactory)
|
||||
} catch (e: Exception) {
|
||||
L.e("DeviceLibrary creation failed: $e")
|
||||
processedSongs.close(
|
||||
Exception("DeviceLibrary creation failed: ${e.stackTraceToString()}"))
|
||||
return@async Result.failure(e)
|
||||
}
|
||||
processedSongs.close()
|
||||
Result.success(deviceLibrary)
|
||||
}
|
||||
|
||||
// We could keep track of a total here, but we also need to collate this RawSong information
|
||||
// for when we write the cache later on in the finalization step.
|
||||
val rawSongs = LinkedList<RawSong>()
|
||||
// Use a longer timeout so that dependent components can timeout and throw errors that
|
||||
// provide more context than if we timed out here.
|
||||
processedSongs.forEachWithTimeout(DEFAULT_TIMEOUT * 2) {
|
||||
rawSongs.add(it)
|
||||
// Since discovery takes up the bulk of the music loading process, we switch to
|
||||
// indicating a defined amount of loaded songs in comparison to the projected amount
|
||||
// of songs that were queried.
|
||||
emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
|
||||
}
|
||||
|
||||
withTimeout(DEFAULT_TIMEOUT) {
|
||||
mediaStoreJob.await()
|
||||
tagJob.await()
|
||||
}
|
||||
|
||||
// Deliberately done after the involved initialization step to make it less likely
|
||||
// that the short-circuit occurs so quickly as to break the UI.
|
||||
// TODO: Do not error, instead just wipe the entire library.
|
||||
if (rawSongs.isEmpty()) {
|
||||
L.e("Music library was empty")
|
||||
throw NoMusicException()
|
||||
}
|
||||
|
||||
// Now that the library is effectively loaded, we can start the finalization step, which
|
||||
// involves writing new cache information and creating more music data that is derived
|
||||
// from the library (e.g playlists)
|
||||
L.d("Discovered ${rawSongs.size} songs, starting finalization")
|
||||
|
||||
// We have no idea how long the cache will take, and the playlist construction
|
||||
// will be too fast to indicate, so switch back to an indeterminate state.
|
||||
emitIndexingProgress(IndexingProgress.Indeterminate)
|
||||
|
||||
// The UserLibrary job is split into a query and construction step, a la MediaStore.
|
||||
// This way, we can start working on playlists even as DeviceLibrary might still be
|
||||
// working on parent information.
|
||||
L.d("Starting UserLibrary query")
|
||||
val userLibraryQueryJob =
|
||||
scope.async {
|
||||
val rawPlaylists =
|
||||
try {
|
||||
userLibraryFactory.query()
|
||||
} catch (e: Exception) {
|
||||
return@async Result.failure(e)
|
||||
}
|
||||
Result.success(rawPlaylists)
|
||||
}
|
||||
|
||||
// The cache might not exist, or we might have encountered a song not present in it.
|
||||
// Both situations require us to rewrite the cache in bulk. This is also done parallel
|
||||
// since the playlist read will probably take some time.
|
||||
// TODO: Read/write from the cache incrementally instead of in bulk?
|
||||
if (cache == null || cache.invalidated) {
|
||||
L.d("Writing cache [why=${cache?.invalidated}]")
|
||||
cacheRepository.writeCache(rawSongs)
|
||||
}
|
||||
|
||||
// Create UserLibrary once we finally get the required components for it.
|
||||
L.d("Awaiting UserLibrary query")
|
||||
val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
|
||||
L.d("Awaiting DeviceLibrary creation")
|
||||
val deviceLibrary = deviceLibraryJob.await().getOrThrow()
|
||||
L.d("Starting UserLibrary creation")
|
||||
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary, nameFactory)
|
||||
|
||||
// Loading process is functionally done, indicate such
|
||||
L.d(
|
||||
"Successfully indexed music library [device=$deviceLibrary " +
|
||||
"user=$userLibrary time=${System.currentTimeMillis() - start}]")
|
||||
emitIndexingCompletion(null)
|
||||
val (deviceLibrary, userLibrary) = indexer.run(listOf(), separators, nameFactory)
|
||||
|
||||
val deviceLibraryChanged: Boolean
|
||||
val userLibraryChanged: Boolean
|
||||
|
|
|
@ -20,10 +20,8 @@ package org.oxycblt.auxio.music.device
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
|
@ -32,13 +30,9 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.fs.contentResolverSafe
|
||||
import org.oxycblt.auxio.music.stack.fs.useQuery
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
||||
import org.oxycblt.auxio.util.forEachWithTimeout
|
||||
import org.oxycblt.auxio.util.sendWithTimeout
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
import timber.log.Timber as L
|
||||
|
||||
|
@ -341,7 +335,7 @@ class DeviceLibraryImpl(
|
|||
) : DeviceLibrary {
|
||||
// Use a mapping to make finding information based on it's UID much faster.
|
||||
private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } }
|
||||
// private val songPathMap = buildMap { songs.forEach { put(it.path, it) } }
|
||||
// private val songPathMap = buildMap { songs.forEach { put(it.path, it) } }
|
||||
private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } }
|
||||
private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } }
|
||||
private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } }
|
||||
|
@ -366,14 +360,15 @@ class DeviceLibraryImpl(
|
|||
override fun findSongByPath(path: Path) = null
|
||||
|
||||
override fun findSongForUri(context: Context, uri: Uri) = null
|
||||
// context.contentResolverSafe.useQuery(
|
||||
// uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
|
||||
// cursor.moveToFirst()
|
||||
// // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
|
||||
// // song. Do what we can to hopefully find the song the user wanted to open.
|
||||
// val displayName =
|
||||
// cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||
// val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
|
||||
// songs.find { it.path.name == displayName && it.size == size }
|
||||
// }
|
||||
// context.contentResolverSafe.useQuery(
|
||||
// uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
|
||||
// cursor.moveToFirst()
|
||||
// // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
|
||||
// // song. Do what we can to hopefully find the song the user wanted to open.
|
||||
// val displayName =
|
||||
//
|
||||
// cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||
// val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
|
||||
// songs.find { it.path.name == displayName && it.size == size }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -28,16 +28,13 @@ import org.oxycblt.auxio.music.Genre
|
|||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
||||
import org.oxycblt.auxio.music.stack.fs.toAlbumCoverUri
|
||||
import org.oxycblt.auxio.music.stack.fs.toAudioUri
|
||||
import org.oxycblt.auxio.music.stack.fs.toSongCoverUri
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
||||
import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames
|
||||
import org.oxycblt.auxio.music.stack.fs.MimeType
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.positiveOrNull
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
|
@ -76,31 +73,28 @@ class SongImpl(
|
|||
}
|
||||
override val name =
|
||||
nameFactory.parse(
|
||||
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" },
|
||||
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.file.path}: No title" },
|
||||
rawSong.sortName)
|
||||
|
||||
override val track = rawSong.track
|
||||
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
|
||||
override val date = rawSong.date
|
||||
override val uri =
|
||||
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }.toAudioUri()
|
||||
override val path = requireNotNull(rawSong.path) { "Invalid raw ${rawSong.path}: No path" }
|
||||
override val mimeType =
|
||||
MimeType(
|
||||
fromExtension =
|
||||
requireNotNull(rawSong.extensionMimeType) {
|
||||
"Invalid raw ${rawSong.path}: No mime type"
|
||||
},
|
||||
fromFormat = null)
|
||||
override val size = requireNotNull(rawSong.size) { "Invalid raw ${rawSong.path}: No size" }
|
||||
override val uri = rawSong.file.uri
|
||||
override val path = rawSong.file.path
|
||||
override val mimeType = MimeType(fromExtension = rawSong.file.mimeType, fromFormat = null)
|
||||
override val size =
|
||||
requireNotNull(rawSong.file.size) { "Invalid raw ${rawSong.file.path}: No size" }
|
||||
override val durationMs =
|
||||
requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.path}: No duration" }
|
||||
requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.file.path}: No duration" }
|
||||
override val replayGainAdjustment =
|
||||
ReplayGainAdjustment(
|
||||
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)
|
||||
|
||||
// TODO: See what we want to do with date added now that we can't get it anymore.
|
||||
override val dateAdded =
|
||||
requireNotNull(rawSong.dateAdded) { "Invalid raw ${rawSong.path}: No date added" }
|
||||
requireNotNull(rawSong.file.lastModified) {
|
||||
"Invalid raw ${rawSong.file.path}: No date added"
|
||||
}
|
||||
|
||||
private var _album: AlbumImpl? = null
|
||||
override val album: Album
|
||||
|
@ -114,18 +108,8 @@ class SongImpl(
|
|||
override val genres: List<Genre>
|
||||
get() = _genres
|
||||
|
||||
override val cover =
|
||||
rawSong.coverPerceptualHash?.let {
|
||||
// We were able to confirm that the song had a parsable cover and can be used on
|
||||
// a per-song basis. Otherwise, just fall back to a per-album cover instead, as
|
||||
// it implies either a cover.jpg pattern is used (likely) or ExoPlayer does not
|
||||
// support the cover metadata of a given spec (unlikely).
|
||||
Cover.Embedded(
|
||||
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }
|
||||
.toSongCoverUri(),
|
||||
uri,
|
||||
it)
|
||||
} ?: Cover.External(requireNotNull(rawSong.albumMediaStoreId).toAlbumCoverUri())
|
||||
// TODO: Rebuild cover system
|
||||
override val cover = Cover.External(rawSong.file.uri)
|
||||
|
||||
/**
|
||||
* The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an
|
||||
|
@ -184,14 +168,10 @@ class SongImpl(
|
|||
|
||||
rawAlbum =
|
||||
RawAlbum(
|
||||
mediaStoreId =
|
||||
requireNotNull(rawSong.albumMediaStoreId) {
|
||||
"Invalid raw ${rawSong.path}: No album id"
|
||||
},
|
||||
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name =
|
||||
requireNotNull(rawSong.albumName) {
|
||||
"Invalid raw ${rawSong.path}: No album name"
|
||||
"Invalid raw ${rawSong.file.path}: No album name"
|
||||
},
|
||||
sortName = rawSong.albumSortName,
|
||||
releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)),
|
||||
|
|
|
@ -22,9 +22,9 @@ 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.stack.fs.DeviceFile
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.stack.fs.DeviceFile
|
||||
|
||||
/**
|
||||
* Raw information about a [SongImpl] obtained from the filesystem/Extractor instances.
|
||||
|
@ -53,10 +53,6 @@ data class RawSong(
|
|||
var subtitle: String? = null,
|
||||
/** @see Song.date */
|
||||
var date: Date? = null,
|
||||
/** @see Song.cover */
|
||||
var coverPerceptualHash: String? = null,
|
||||
/** @see RawAlbum.mediaStoreId */
|
||||
var albumMediaStoreId: Long? = null,
|
||||
/** @see RawAlbum.musicBrainzId */
|
||||
var albumMusicBrainzId: String? = null,
|
||||
/** @see RawAlbum.name */
|
||||
|
@ -87,11 +83,6 @@ data class RawSong(
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
data class RawAlbum(
|
||||
/**
|
||||
* The ID of the [AlbumImpl]'s grouping, obtained from MediaStore. Note that this ID is highly
|
||||
* unstable and should only be used for accessing the system-provided cover art.
|
||||
*/
|
||||
val mediaStoreId: Long,
|
||||
/** @see Music.uid */
|
||||
override val musicBrainzId: UUID?,
|
||||
/** @see Music.name */
|
||||
|
|
|
@ -27,12 +27,12 @@ import java.io.InputStreamReader
|
|||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.stack.fs.Components
|
||||
import org.oxycblt.auxio.music.stack.fs.Path
|
||||
import org.oxycblt.auxio.music.stack.fs.Volume
|
||||
import org.oxycblt.auxio.music.stack.fs.VolumeManager
|
||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
import timber.log.Timber as L
|
||||
|
||||
|
|
|
@ -119,7 +119,6 @@ constructor(@ApplicationContext private val context: Context) : AudioProperties.
|
|||
return AudioProperties(
|
||||
bitrate,
|
||||
sampleRate,
|
||||
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType)
|
||||
)
|
||||
MimeType(fromExtension = song.mimeType.fromExtension, fromFormat = formatMimeType))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,19 +22,10 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor
|
||||
import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractorImpl
|
||||
import org.oxycblt.auxio.music.stack.extractor.TagInterpreter2
|
||||
import org.oxycblt.auxio.music.stack.extractor.TagInterpreter2Impl
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface MetadataModule {
|
||||
@Binds fun tagInterpreter(interpreter: TagInterpreterImpl): TagInterpreter
|
||||
|
||||
@Binds fun tagInterpreter2(interpreter: TagInterpreter2Impl): TagInterpreter2
|
||||
|
||||
@Binds fun exoPlayerTagExtractor(extractor: ExoPlayerTagExtractorImpl): ExoPlayerTagExtractor
|
||||
|
||||
@Binds fun audioPropertiesFactory(factory: AudioPropertiesFactoryImpl): AudioProperties.Factory
|
||||
@Binds
|
||||
fun audioPropertiesFactory(interpreter: AudioPropertiesFactoryImpl): AudioProperties.Factory
|
||||
}
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Separators.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.metadata
|
||||
|
||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
||||
|
|
|
@ -1,6 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Indexer.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.stack
|
||||
|
||||
import android.net.Uri
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -14,7 +33,6 @@ import kotlinx.coroutines.flow.flowOn
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import org.oxycblt.auxio.music.device.DeviceLibrary
|
||||
import org.oxycblt.auxio.music.device.RawSong
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
|
@ -24,44 +42,51 @@ import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor
|
|||
import org.oxycblt.auxio.music.stack.extractor.TagResult
|
||||
import org.oxycblt.auxio.music.stack.fs.DeviceFile
|
||||
import org.oxycblt.auxio.music.stack.fs.DeviceFiles
|
||||
import org.oxycblt.auxio.music.user.MutableUserLibrary
|
||||
import org.oxycblt.auxio.music.user.UserLibrary
|
||||
import javax.inject.Inject
|
||||
|
||||
interface Indexer {
|
||||
suspend fun run(uris: List<Uri>, separators: Separators, nameFactory: Name.Known.Factory): LibraryResult
|
||||
suspend fun run(
|
||||
uris: List<Uri>,
|
||||
separators: Separators,
|
||||
nameFactory: Name.Known.Factory
|
||||
): LibraryResult
|
||||
}
|
||||
|
||||
data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: UserLibrary)
|
||||
data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: MutableUserLibrary)
|
||||
|
||||
class IndexerImpl @Inject constructor(
|
||||
class IndexerImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val deviceFiles: DeviceFiles,
|
||||
private val tagCache: TagCache,
|
||||
private val tagExtractor: ExoPlayerTagExtractor,
|
||||
private val deviceLibraryFactory: DeviceLibrary.Factory,
|
||||
private val userLibraryFactory: UserLibrary.Factory
|
||||
) : Indexer {
|
||||
override suspend fun run(uris: List<Uri>, separators: Separators, nameFactory: Name.Known.Factory) = coroutineScope {
|
||||
val deviceFiles = deviceFiles.explore(uris.asFlow())
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer()
|
||||
val tagRead = tagCache.read(deviceFiles)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer()
|
||||
override suspend fun run(
|
||||
uris: List<Uri>,
|
||||
separators: Separators,
|
||||
nameFactory: Name.Known.Factory
|
||||
) = coroutineScope {
|
||||
val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer()
|
||||
val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer()
|
||||
val (cacheFiles, cacheSongs) = tagRead.split()
|
||||
val tagExtractor =
|
||||
tagExtractor.process(cacheFiles)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.buffer()
|
||||
val tagExtractor = tagExtractor.process(cacheFiles).flowOn(Dispatchers.IO).buffer()
|
||||
val (_, extractorSongs) = tagExtractor.split()
|
||||
val sharedExtractorSongs = extractorSongs.shareIn(
|
||||
val sharedExtractorSongs =
|
||||
extractorSongs.shareIn(
|
||||
CoroutineScope(Dispatchers.Main),
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
replay = Int.MAX_VALUE
|
||||
)
|
||||
val tagWrite = async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) }
|
||||
replay = Int.MAX_VALUE)
|
||||
val tagWrite =
|
||||
async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) }
|
||||
val rawPlaylists = async(Dispatchers.IO) { userLibraryFactory.query() }
|
||||
val deviceLibrary = deviceLibraryFactory.create(merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory)
|
||||
val userLibrary = userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory)
|
||||
val deviceLibrary =
|
||||
deviceLibraryFactory.create(
|
||||
merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory)
|
||||
val userLibrary =
|
||||
userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory)
|
||||
tagWrite.await()
|
||||
LibraryResult(deviceLibrary, userLibrary)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* StackModule.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.stack
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface StackModule {
|
||||
@Binds fun indexer(impl: IndexerImpl): Indexer
|
||||
}
|
|
@ -40,8 +40,7 @@ class TagDatabaseModule {
|
|||
@Singleton
|
||||
@Provides
|
||||
fun database(@ApplicationContext context: Context) =
|
||||
Room.databaseBuilder(
|
||||
context.applicationContext, TagDatabase::class.java, "music_cache.db")
|
||||
Room.databaseBuilder(context.applicationContext, TagDatabase::class.java, "music_cache.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
|
|
|
@ -1,20 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* TagCache.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.stack.cache
|
||||
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import org.oxycblt.auxio.music.device.RawSong
|
||||
import org.oxycblt.auxio.music.stack.fs.DeviceFile
|
||||
import org.oxycblt.auxio.music.stack.extractor.TagResult
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.stack.fs.DeviceFile
|
||||
|
||||
interface TagCache {
|
||||
fun read(files: Flow<DeviceFile>): Flow<TagResult>
|
||||
|
||||
suspend fun write(rawSongs: Flow<RawSong>)
|
||||
}
|
||||
|
||||
class TagCacheImpl @Inject constructor(
|
||||
private val tagDao: TagDao
|
||||
) : TagCache {
|
||||
class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
|
||||
override fun read(files: Flow<DeviceFile>) =
|
||||
files.transform<DeviceFile, TagResult> { file ->
|
||||
val tags = tagDao.selectTags(file.uri.toString(), file.lastModified)
|
||||
|
@ -28,8 +45,6 @@ class TagCacheImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun write(rawSongs: Flow<RawSong>) {
|
||||
rawSongs.collect { rawSong ->
|
||||
tagDao.updateTags(Tags.fromRaw(rawSong))
|
||||
}
|
||||
rawSongs.collect { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) }
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* CacheDatabase.kt is part of Auxio.
|
||||
* TagDatabase.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
|
||||
|
@ -43,16 +43,15 @@ interface TagDao {
|
|||
@Query("SELECT * FROM Tags WHERE uri = :uri AND dateModified = :dateModified")
|
||||
suspend fun selectTags(uri: String, dateModified: Long): Tags?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun updateTags(tags: Tags)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateTags(tags: Tags)
|
||||
}
|
||||
|
||||
@Entity
|
||||
@TypeConverters(Tags.Converters::class)
|
||||
data class Tags(
|
||||
/**
|
||||
* The Uri of the [RawSong]'s audio file, obtained from SAF.
|
||||
* This should ideally be a black box only used for comparison.
|
||||
* The Uri of the [RawSong]'s audio file, obtained from SAF. This should ideally be a black box
|
||||
* only used for comparison.
|
||||
*/
|
||||
@PrimaryKey val uri: String,
|
||||
/** The latest date the [RawSong]'s audio file was modified, as a unix epoch timestamp. */
|
||||
|
@ -77,8 +76,6 @@ data class Tags(
|
|||
var subtitle: String? = null,
|
||||
/** @see RawSong.date */
|
||||
var date: Date? = null,
|
||||
/** @see RawSong.coverPerceptualHash */
|
||||
var coverPerceptualHash: String? = null,
|
||||
/** @see RawSong.albumMusicBrainzId */
|
||||
var albumMusicBrainzId: String? = null,
|
||||
/** @see RawSong.albumName */
|
||||
|
@ -117,8 +114,6 @@ data class Tags(
|
|||
rawSong.subtitle = subtitle
|
||||
rawSong.date = date
|
||||
|
||||
rawSong.coverPerceptualHash = coverPerceptualHash
|
||||
|
||||
rawSong.albumMusicBrainzId = albumMusicBrainzId
|
||||
rawSong.albumName = albumName
|
||||
rawSong.albumSortName = albumSortName
|
||||
|
@ -163,7 +158,6 @@ data class Tags(
|
|||
disc = rawSong.disc,
|
||||
subtitle = rawSong.subtitle,
|
||||
date = rawSong.date,
|
||||
coverPerceptualHash = rawSong.coverPerceptualHash,
|
||||
albumMusicBrainzId = rawSong.albumMusicBrainzId,
|
||||
albumName = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" },
|
||||
albumSortName = rawSong.albumSortName,
|
||||
|
|
|
@ -1,3 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ExoPlayerTagExtractor.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.stack.extractor
|
||||
|
||||
import android.os.HandlerThread
|
||||
|
@ -5,17 +23,18 @@ import androidx.media3.common.MediaItem
|
|||
import androidx.media3.exoplayer.MetadataRetriever
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.TrackGroupArray
|
||||
import java.util.concurrent.Future
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.oxycblt.auxio.music.device.RawSong
|
||||
import org.oxycblt.auxio.music.stack.fs.DeviceFile
|
||||
import java.util.concurrent.Future
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber as L
|
||||
|
||||
interface TagResult {
|
||||
class Hit(val rawSong: RawSong) : TagResult
|
||||
|
||||
class Miss(val file: DeviceFile) : TagResult
|
||||
}
|
||||
|
||||
|
@ -23,27 +42,23 @@ interface ExoPlayerTagExtractor {
|
|||
fun process(deviceFiles: Flow<DeviceFile>): Flow<TagResult>
|
||||
}
|
||||
|
||||
class ExoPlayerTagExtractorImpl @Inject constructor(
|
||||
class ExoPlayerTagExtractorImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val mediaSourceFactory: MediaSource.Factory,
|
||||
private val tagInterpreter2: TagInterpreter2,
|
||||
private val tagInterpreter2: TagInterpreter,
|
||||
) : ExoPlayerTagExtractor {
|
||||
override fun process(deviceFiles: Flow<DeviceFile>) = flow {
|
||||
val threadPool = ThreadPool(8, Handler(this))
|
||||
deviceFiles.collect { file ->
|
||||
threadPool.enqueue(file)
|
||||
}
|
||||
deviceFiles.collect { file -> threadPool.enqueue(file) }
|
||||
threadPool.empty()
|
||||
}
|
||||
|
||||
private inner class Handler(
|
||||
private val collector: FlowCollector<TagResult>
|
||||
) : ThreadPool.Handler<DeviceFile, TrackGroupArray> {
|
||||
private inner class Handler(private val collector: FlowCollector<TagResult>) :
|
||||
ThreadPool.Handler<DeviceFile, TrackGroupArray> {
|
||||
override suspend fun produce(thread: HandlerThread, input: DeviceFile) =
|
||||
MetadataRetriever.retrieveMetadata(
|
||||
mediaSourceFactory,
|
||||
MediaItem.fromUri(input.uri),
|
||||
thread
|
||||
)
|
||||
mediaSourceFactory, MediaItem.fromUri(input.uri), thread)
|
||||
|
||||
override suspend fun consume(input: DeviceFile, output: TrackGroupArray) {
|
||||
if (output.isEmpty) {
|
||||
|
@ -76,10 +91,7 @@ class ExoPlayerTagExtractorImpl @Inject constructor(
|
|||
private class ThreadPool<I, O>(size: Int, private val handler: Handler<I, O>) {
|
||||
private val slots =
|
||||
Array<Slot<I, O>>(size) {
|
||||
Slot(
|
||||
thread = HandlerThread("Auxio:ThreadPool:$it"),
|
||||
task = null
|
||||
)
|
||||
Slot(thread = HandlerThread("Auxio:ThreadPool:$it"), task = null)
|
||||
}
|
||||
|
||||
suspend fun enqueue(input: I) {
|
||||
|
@ -114,25 +126,19 @@ private class ThreadPool<I, O>(size: Int, private val handler: Handler<I, O>) {
|
|||
// In-practice this should never block, as all clients
|
||||
// check if the future is done before calling this function.
|
||||
// If you don't maintain that invariant, this will explode.
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
handler.consume(input, future.get())
|
||||
@Suppress("BlockingMethodInNonBlockingContext") handler.consume(input, future.get())
|
||||
} catch (e: Exception) {
|
||||
L.e("Failed to complete task for $input, ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
|
||||
private data class Slot<I, O>(
|
||||
val thread: HandlerThread,
|
||||
var task: Task<I, O>?
|
||||
)
|
||||
private data class Slot<I, O>(val thread: HandlerThread, var task: Task<I, O>?)
|
||||
|
||||
private data class Task<I, O>(
|
||||
val input: I,
|
||||
val future: Future<O>
|
||||
)
|
||||
private data class Task<I, O>(val input: I, val future: Future<O>)
|
||||
|
||||
interface Handler<I, O> {
|
||||
suspend fun produce(thread: HandlerThread, input: I): Future<O>
|
||||
|
||||
suspend fun consume(input: I, output: O)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ExtractorModule.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.stack.extractor
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface MetadataModule {
|
||||
@Binds fun tagInterpreter(interpreter: TagInterpreterImpl): TagInterpreter
|
||||
|
||||
@Binds fun exoPlayerTagExtractor(extractor: ExoPlayerTagExtractorImpl): ExoPlayerTagExtractor
|
||||
}
|
|
@ -21,7 +21,6 @@ package org.oxycblt.auxio.music.stack.extractor
|
|||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.media3.exoplayer.MetadataRetriever
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.image.extractor.CoverExtractor
|
||||
import org.oxycblt.auxio.music.device.RawSong
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
|
@ -32,7 +31,7 @@ import org.oxycblt.auxio.util.nonZeroOrNull
|
|||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface TagInterpreter2 {
|
||||
interface TagInterpreter {
|
||||
/**
|
||||
* Poll to see if this worker is done processing.
|
||||
*
|
||||
|
@ -41,8 +40,7 @@ interface TagInterpreter2 {
|
|||
fun interpretOn(textTags: TextTags, rawSong: RawSong)
|
||||
}
|
||||
|
||||
class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverExtractor) :
|
||||
TagInterpreter2 {
|
||||
class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||
override fun interpretOn(textTags: TextTags, rawSong: RawSong) {
|
||||
populateWithId3v2(rawSong, textTags.id3v2)
|
||||
populateWithVorbis(rawSong, textTags.vorbis)
|
||||
|
@ -198,15 +196,13 @@ class TagInterpreter2Impl @Inject constructor(private val coverExtractor: CoverE
|
|||
// Track.
|
||||
parseVorbisPositionField(
|
||||
comments["tracknumber"]?.first(),
|
||||
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first()
|
||||
)
|
||||
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first())
|
||||
?.let { rawSong.track = it }
|
||||
|
||||
// Disc and it's subtitle name.
|
||||
parseVorbisPositionField(
|
||||
comments["discnumber"]?.first(),
|
||||
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first()
|
||||
)
|
||||
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first())
|
||||
?.let { rawSong.disc = it }
|
||||
comments["discsubtitle"]?.let { rawSong.subtitle = it.first() }
|
||||
|
|
@ -37,8 +37,12 @@ interface DeviceFiles {
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
||||
DeviceFiles {
|
||||
class DeviceFilesImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val volumeManager: VolumeManager
|
||||
) : DeviceFiles {
|
||||
private val contentResolver = context.contentResolverSafe
|
||||
|
||||
override fun explore(uris: Flow<Uri>): Flow<DeviceFile> =
|
||||
|
@ -49,6 +53,9 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
|
|||
uri: Uri,
|
||||
relativePath: Components
|
||||
): Flow<DeviceFile> = flow {
|
||||
// TODO: Temporary to maintain path api parity
|
||||
// Figure out what we actually want to do to paths now in saf world.
|
||||
val external = volumeManager.getInternalVolume()
|
||||
contentResolver.useQuery(uri, PROJECTION) { cursor ->
|
||||
val childUriIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
|
@ -77,7 +84,7 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
|
|||
// rather than just being a glorified async.
|
||||
val lastModified = cursor.getLong(lastModifiedIndex)
|
||||
val size = cursor.getLong(sizeIndex)
|
||||
emit(DeviceFile(childUri, mimeType, path, size, lastModified))
|
||||
emit(DeviceFile(childUri, mimeType, Path(external, path), size, lastModified))
|
||||
}
|
||||
}
|
||||
// Hypothetically, we could just emitAll as we recurse into a new directory,
|
||||
|
@ -96,9 +103,14 @@ class DeviceFilesImpl @Inject constructor(@ApplicationContext private val contex
|
|||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
)
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceFile(val uri: Uri, val mimeType: String, val path: Components, val size: Long, val lastModified: Long)
|
||||
data class DeviceFile(
|
||||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
val path: Path,
|
||||
val size: Long,
|
||||
val lastModified: Long
|
||||
)
|
||||
|
|
|
@ -51,4 +51,6 @@ class FsModule {
|
|||
interface FsBindsModule {
|
||||
@Binds
|
||||
fun documentPathFactory(documentTreePathFactory: DocumentPathFactoryImpl): DocumentPathFactory
|
||||
|
||||
@Binds fun deviceFiles(deviceFilesImpl: DeviceFilesImpl): DeviceFiles
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue