all: cleanup
This commit is contained in:
parent
61fd11fe04
commit
75612dd1eb
10 changed files with 91 additions and 108 deletions
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 Auxio Project
|
|
||||||
* Indexing.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
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import org.oxycblt.musikr.IndexingProgress
|
|
||||||
|
|
||||||
/** Version-aware permission identifier for reading audio files. */
|
|
||||||
val PERMISSION_READ_AUDIO =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
android.Manifest.permission.READ_MEDIA_AUDIO
|
|
||||||
} else {
|
|
||||||
android.Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the current state of the music loader.
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
sealed interface IndexingState {
|
|
||||||
/**
|
|
||||||
* Music loading is on-going.
|
|
||||||
*
|
|
||||||
* @param progress The current progress of the music loading.
|
|
||||||
*/
|
|
||||||
data class Indexing(val progress: IndexingProgress) : IndexingState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Music loading has completed.
|
|
||||||
*
|
|
||||||
* @param error If music loading has failed, the error that occurred will be here. Otherwise, it
|
|
||||||
* will be null.
|
|
||||||
*/
|
|
||||||
data class Completed(val error: Exception?) : IndexingState
|
|
||||||
}
|
|
|
@ -26,7 +26,6 @@ import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import org.oxycblt.musikr.cache.Cache
|
|
||||||
import org.oxycblt.musikr.cache.StoredCache
|
import org.oxycblt.musikr.cache.StoredCache
|
||||||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||||
|
|
||||||
|
@ -41,7 +40,9 @@ interface MusicModule {
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class MusikrShimModule {
|
class MusikrShimModule {
|
||||||
@Singleton @Provides fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context)
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context)
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
@ -209,6 +209,28 @@ interface MusicRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the current state of the music loader.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
sealed interface IndexingState {
|
||||||
|
/**
|
||||||
|
* Music loading is on-going.
|
||||||
|
*
|
||||||
|
* @param progress The current progress of the music loading.
|
||||||
|
*/
|
||||||
|
data class Indexing(val progress: IndexingProgress) : IndexingState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Music loading has completed.
|
||||||
|
*
|
||||||
|
* @param error If music loading has failed, the error that occurred will be here. Otherwise, it
|
||||||
|
* will be null.
|
||||||
|
*/
|
||||||
|
data class Completed(val error: Exception?) : IndexingState
|
||||||
|
}
|
||||||
|
|
||||||
class MusicRepositoryImpl
|
class MusicRepositoryImpl
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
@ -75,7 +75,7 @@ class RevisionedCover(private val revision: UUID, val inner: Cover) : Cover by i
|
||||||
get() = "${inner.id}@${revision}"
|
get() = "${inner.id}@${revision}"
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun String.toUuidOrNull(): UUID? =
|
private fun String.toUuidOrNull(): UUID? =
|
||||||
try {
|
try {
|
||||||
UUID.fromString(this)
|
UUID.fromString(this)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.interpret
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
|
@ -96,7 +96,7 @@
|
||||||
tools:layout="@layout/dialog_music_locations" />
|
tools:layout="@layout/dialog_music_locations" />
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/separators_dialog"
|
android:id="@+id/separators_dialog"
|
||||||
android:name="org.oxycblt.auxio.music.metadata.SeparatorsDialog"
|
android:name="org.oxycblt.auxio.music.interpret.SeparatorsDialog"
|
||||||
android:label="separators_dialog"
|
android:label="separators_dialog"
|
||||||
tools:layout="@layout/dialog_separators" />
|
tools:layout="@layout/dialog_separators" />
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ package org.oxycblt.musikr.cache
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
|
@ -32,7 +31,6 @@ import androidx.room.RoomDatabase
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import androidx.room.Update
|
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
import org.oxycblt.musikr.fs.DeviceFile
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
import org.oxycblt.musikr.metadata.Properties
|
import org.oxycblt.musikr.metadata.Properties
|
||||||
|
@ -67,8 +65,7 @@ internal interface VisibleCacheDao {
|
||||||
@Query("SELECT addedMs FROM CachedSong WHERE uri = :uri")
|
@Query("SELECT addedMs FROM CachedSong WHERE uri = :uri")
|
||||||
suspend fun selectAddedMs(uri: String): Long?
|
suspend fun selectAddedMs(uri: String): Long?
|
||||||
|
|
||||||
@Transaction
|
@Transaction suspend fun touch(uri: String) = updateTouchedNs(uri, System.nanoTime())
|
||||||
suspend fun touch(uri: String) = updateTouchedNs(uri, System.nanoTime())
|
|
||||||
|
|
||||||
@Query("UPDATE cachedsong SET touchedNs = :nowNs WHERE uri = :uri")
|
@Query("UPDATE cachedsong SET touchedNs = :nowNs WHERE uri = :uri")
|
||||||
suspend fun updateTouchedNs(uri: String, nowNs: Long)
|
suspend fun updateTouchedNs(uri: String, nowNs: Long)
|
||||||
|
@ -84,8 +81,7 @@ internal interface InvisibleCacheDao {
|
||||||
internal interface CacheWriteDao {
|
internal interface CacheWriteDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
|
||||||
|
|
||||||
@Query("DELETE FROM CachedSong WHERE touchedNs < :now")
|
@Query("DELETE FROM CachedSong WHERE touchedNs < :now") suspend fun pruneOlderThan(now: Long)
|
||||||
suspend fun pruneOlderThan(now: Long)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Auxio Project
|
||||||
|
* StoredCache.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.musikr.cache
|
package org.oxycblt.musikr.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -24,8 +42,7 @@ private class StoredCacheImpl(private val cacheDatabase: CacheDatabase) : Stored
|
||||||
private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) : Cache() {
|
private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) : Cache() {
|
||||||
private val created = System.nanoTime()
|
private val created = System.nanoTime()
|
||||||
|
|
||||||
override suspend fun write(song: RawSong) =
|
override suspend fun write(song: RawSong) = writeDao.updateSong(CachedSong.fromRawSong(song))
|
||||||
writeDao.updateSong(CachedSong.fromRawSong(song))
|
|
||||||
|
|
||||||
override suspend fun finalize() {
|
override suspend fun finalize() {
|
||||||
// Anything not create during this cache's use implies that it has not been
|
// Anything not create during this cache's use implies that it has not been
|
||||||
|
@ -34,13 +51,10 @@ private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class VisibleStoredCache(
|
private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeDao: CacheWriteDao) :
|
||||||
private val visibleDao: VisibleCacheDao,
|
BaseStoredCache(writeDao) {
|
||||||
writeDao: CacheWriteDao
|
|
||||||
) : BaseStoredCache(writeDao) {
|
|
||||||
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult {
|
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult {
|
||||||
val song =
|
val song = visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null)
|
||||||
visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null)
|
|
||||||
if (song.modifiedMs != file.lastModified) {
|
if (song.modifiedMs != file.lastModified) {
|
||||||
// We *found* this file earlier, but it's out of date.
|
// We *found* this file earlier, but it's out of date.
|
||||||
// Send back it with the timestamp so it will be re-used.
|
// Send back it with the timestamp so it will be re-used.
|
||||||
|
@ -53,7 +67,8 @@ private class VisibleStoredCache(
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
|
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
|
||||||
override fun open() = VisibleStoredCache(cacheDatabase.visibleDao(), cacheDatabase.writeDao())
|
override fun open() =
|
||||||
|
VisibleStoredCache(cacheDatabase.visibleDao(), cacheDatabase.writeDao())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +80,7 @@ private class InvisibleStoredCache(
|
||||||
CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString()))
|
CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString()))
|
||||||
|
|
||||||
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
|
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
|
||||||
override fun open() = InvisibleStoredCache(cacheDatabase.invisibleDao(), cacheDatabase.writeDao())
|
override fun open() =
|
||||||
|
InvisibleStoredCache(cacheDatabase.invisibleDao(), cacheDatabase.writeDao())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package org.oxycblt.musikr.pipeline
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
|
@ -52,8 +53,7 @@ internal interface ExtractStep {
|
||||||
MetadataExtractor.new(),
|
MetadataExtractor.new(),
|
||||||
TagParser.new(),
|
TagParser.new(),
|
||||||
storage.cache,
|
storage.cache,
|
||||||
storage.storedCovers
|
storage.storedCovers)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ private class ExtractStepImpl(
|
||||||
private val cacheFactory: Cache.Factory,
|
private val cacheFactory: Cache.Factory,
|
||||||
private val storedCovers: MutableStoredCovers
|
private val storedCovers: MutableStoredCovers
|
||||||
) : ExtractStep {
|
) : ExtractStep {
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
||||||
val cache = cacheFactory.open()
|
val cache = cacheFactory.open()
|
||||||
val addingMs = System.currentTimeMillis()
|
val addingMs = System.currentTimeMillis()
|
||||||
|
@ -114,13 +115,13 @@ private class ExtractStepImpl(
|
||||||
|
|
||||||
val metadata =
|
val metadata =
|
||||||
fds.mapNotNull { fileWith ->
|
fds.mapNotNull { fileWith ->
|
||||||
wrap(fileWith.file) { _ ->
|
wrap(fileWith.file) { _ ->
|
||||||
metadataExtractor
|
metadataExtractor
|
||||||
.extract(fileWith.with)
|
.extract(fileWith.with)
|
||||||
?.let { FileWith(fileWith.file, it) }
|
?.let { FileWith(fileWith.file, it) }
|
||||||
.also { withContext(Dispatchers.IO) { fileWith.with.close() } }
|
.also { withContext(Dispatchers.IO) { fileWith.with.close() } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
// Covers are pretty big, so cap the amount of parsed metadata in-memory to at most
|
// Covers are pretty big, so cap the amount of parsed metadata in-memory to at most
|
||||||
// 8 to minimize GCs.
|
// 8 to minimize GCs.
|
||||||
|
@ -150,19 +151,17 @@ private class ExtractStepImpl(
|
||||||
}
|
}
|
||||||
.flattenMerge()
|
.flattenMerge()
|
||||||
|
|
||||||
val merged = merge(
|
val merged =
|
||||||
filterFlow.manager,
|
merge(
|
||||||
readDistributedFlow.manager,
|
filterFlow.manager,
|
||||||
cacheFlow.manager,
|
readDistributedFlow.manager,
|
||||||
cachedSongs,
|
cacheFlow.manager,
|
||||||
writeDistributedFlow.manager,
|
cachedSongs,
|
||||||
writtenSongs,
|
writeDistributedFlow.manager,
|
||||||
playlistNodes
|
writtenSongs,
|
||||||
)
|
playlistNodes)
|
||||||
|
|
||||||
return merged.onCompletion {
|
return merged.onCompletion { cache.finalize() }
|
||||||
cache.finalize()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class FileWith<T>(val file: DeviceFile, val with: T)
|
private data class FileWith<T>(val file: DeviceFile, val with: T)
|
||||||
|
|
|
@ -53,21 +53,22 @@ internal data class PreSong(
|
||||||
val preArtists: List<PreArtist>,
|
val preArtists: List<PreArtist>,
|
||||||
val preGenres: List<PreGenre>
|
val preGenres: List<PreGenre>
|
||||||
) {
|
) {
|
||||||
val uid = musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) }
|
val uid =
|
||||||
?: Music.UID.auxio(Music.UID.Item.SONG) {
|
musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) }
|
||||||
// Song UIDs are based on the raw data without parsing so that they remain
|
?: Music.UID.auxio(Music.UID.Item.SONG) {
|
||||||
// consistent across music setting changes. Parents are not held up to the
|
// Song UIDs are based on the raw data without parsing so that they remain
|
||||||
// same standard since grouping is already inherently linked to settings.
|
// consistent across music setting changes. Parents are not held up to the
|
||||||
update(rawName)
|
// same standard since grouping is already inherently linked to settings.
|
||||||
update(preAlbum.rawName)
|
update(rawName)
|
||||||
update(date)
|
update(preAlbum.rawName)
|
||||||
|
update(date)
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
update(disc?.number)
|
update(disc?.number)
|
||||||
|
|
||||||
update(preArtists.map { artist -> artist.rawName })
|
update(preArtists.map { artist -> artist.rawName })
|
||||||
update(preAlbum.preArtists.map { artist -> artist.rawName })
|
update(preAlbum.preArtists.map { artist -> artist.rawName })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class PreAlbum(
|
internal data class PreAlbum(
|
||||||
|
|
Loading…
Reference in a new issue