all: cleanup

This commit is contained in:
Alexander Capehart 2024-12-26 14:04:15 -05:00
parent 61fd11fe04
commit 75612dd1eb
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 91 additions and 108 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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(

View file

@ -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) {

View file

@ -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

View file

@ -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" />

View file

@ -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

View file

@ -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())
} }
} }

View file

@ -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)

View file

@ -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(