diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
index 3c7d614c0..350e4cb81 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt
@@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.oxycblt.auxio.image.covers.SettingCovers
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
+import org.oxycblt.auxio.music.shim.WriteOnlySongCache
import org.oxycblt.musikr.IndexingProgress
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.Library
@@ -38,7 +39,7 @@ import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.Storage
-import org.oxycblt.musikr.cache.StoredCache
+import org.oxycblt.musikr.cache.MutableSongCache
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators
@@ -236,7 +237,7 @@ class MusicRepositoryImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
- private val storedCache: StoredCache,
+ private val songCache: MutableSongCache,
private val storedPlaylists: StoredPlaylists,
private val settingCovers: SettingCovers,
private val musicSettings: MusicSettings
@@ -387,7 +388,7 @@ constructor(
val currentRevision = musicSettings.revision
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID()
- val cache = if (withCache) storedCache.visible() else storedCache.invisible()
+ val cache = if (withCache) WriteOnlySongCache(songCache) else songCache
val covers = settingCovers.create(context, newRevision)
val storage = Storage(cache, covers, storedPlaylists)
val interpretation = Interpretation(nameFactory, separators)
diff --git a/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt b/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt
index 977936cd5..7516cf3f3 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/shim/MusikrShimModule.kt
@@ -25,7 +25,8 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
-import org.oxycblt.musikr.cache.StoredCache
+import org.oxycblt.musikr.cache.DBSongCache
+import org.oxycblt.musikr.cache.SongCache
import org.oxycblt.musikr.playlist.db.StoredPlaylists
@Module
@@ -33,7 +34,7 @@ import org.oxycblt.musikr.playlist.db.StoredPlaylists
class MusikrShimModule {
@Singleton
@Provides
- fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context)
+ fun songCache(@ApplicationContext context: Context): SongCache = DBSongCache.from(context)
@Singleton
@Provides
diff --git a/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyStoredCache.kt b/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyStoredCache.kt
new file mode 100644
index 000000000..3f908ad7d
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/music/shim/WriteOnlyStoredCache.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2025 Auxio Project
+ * WriteOnlyStoredCache.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 .
+ */
+
+package org.oxycblt.auxio.music.shim
+
+import org.oxycblt.musikr.Song
+import org.oxycblt.musikr.cache.CacheResult
+import org.oxycblt.musikr.cache.CachedSong
+import org.oxycblt.musikr.cache.MutableSongCache
+import org.oxycblt.musikr.fs.device.DeviceFile
+
+class WriteOnlySongCache(private val songCache: MutableSongCache) : MutableSongCache {
+ override suspend fun read(file: DeviceFile) =
+ when (val result = songCache.read(file)) {
+ is CacheResult.Hit -> CacheResult.Outdated(file, result.song.addedMs)
+ else -> result
+ }
+
+ override suspend fun write(song: CachedSong) {
+ songCache.write(song)
+ }
+
+ override suspend fun cleanup(exclude: Collection) {
+ songCache.cleanup(exclude)
+ }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt
index fa9535b35..50d866190 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt
@@ -18,7 +18,7 @@
package org.oxycblt.musikr
-import org.oxycblt.musikr.cache.Cache
+import org.oxycblt.musikr.cache.MutableSongCache
import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming
@@ -30,7 +30,7 @@ data class Storage(
* A repository of cached metadata to read and write from over the course of music loading only.
* This will be used only during music loading.
*/
- val cache: Cache,
+ val cache: MutableSongCache,
/**
* A repository of cover images to for re-use during music loading. Should be kept in lock-step
diff --git a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt
index da788f9c8..022dccb2c 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt
@@ -160,6 +160,7 @@ private class LibraryResultImpl(
override val library: MutableLibrary
) : LibraryResult {
override suspend fun cleanup() {
+ storage.cache.cleanup(library.songs)
storage.covers.cleanup(library.songs.mapNotNull { it.cover })
}
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt
deleted file mode 100644
index 4e8b2075c..000000000
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (c) 2023 Auxio Project
- * CacheDatabase.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 .
- */
-
-package org.oxycblt.musikr.cache
-
-import android.content.Context
-import androidx.room.Dao
-import androidx.room.Database
-import androidx.room.Entity
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.PrimaryKey
-import androidx.room.Query
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import androidx.room.Transaction
-import androidx.room.TypeConverter
-import androidx.room.TypeConverters
-import org.oxycblt.musikr.pipeline.RawSong
-import org.oxycblt.musikr.tag.Date
-import org.oxycblt.musikr.util.correctWhitespace
-import org.oxycblt.musikr.util.splitEscaped
-
-@Database(entities = [CachedSong::class], version = 57, exportSchema = false)
-internal abstract class CacheDatabase : RoomDatabase() {
- abstract fun visibleDao(): VisibleCacheDao
-
- abstract fun invisibleDao(): InvisibleCacheDao
-
- abstract fun writeDao(): CacheWriteDao
-
- companion object {
- fun from(context: Context) =
- Room.databaseBuilder(
- context.applicationContext, CacheDatabase::class.java, "music_cache.db")
- .fallbackToDestructiveMigration()
- .build()
- }
-}
-
-@Dao
-internal interface VisibleCacheDao {
- @Query("SELECT * FROM CachedSong WHERE uri = :uri")
- suspend fun selectSong(uri: String): CachedSong?
-
- @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri")
- suspend fun selectAddedMs(uri: String): Long?
-
- @Transaction suspend fun touch(uri: String) = updateTouchedNs(uri, System.nanoTime())
-
- @Query("UPDATE CachedSong SET touchedNs = :nowNs WHERE uri = :uri")
- suspend fun updateTouchedNs(uri: String, nowNs: Long)
-}
-
-@Dao
-internal interface InvisibleCacheDao {
- @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri")
- suspend fun selectAddedMs(uri: String): Long?
-}
-
-@Dao
-internal interface CacheWriteDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
-
- @Query("DELETE FROM CachedSong WHERE touchedNs < :now") suspend fun pruneOlderThan(now: Long)
-}
-
-@Entity
-@TypeConverters(CachedSong.Converters::class)
-internal data class CachedSong(
- @PrimaryKey val uri: String,
- val modifiedMs: Long,
- val addedMs: Long,
- val touchedNs: Long,
- val mimeType: String,
- val durationMs: Long,
- val bitrateHz: Int,
- val sampleRateHz: Int,
- val musicBrainzId: String?,
- val name: String,
- val sortName: String?,
- val track: Int?,
- val disc: Int?,
- val subtitle: String?,
- val date: Date?,
- val albumMusicBrainzId: String?,
- val albumName: String?,
- val albumSortName: String?,
- val releaseTypes: List,
- val artistMusicBrainzIds: List,
- val artistNames: List,
- val artistSortNames: List,
- val albumArtistMusicBrainzIds: List,
- val albumArtistNames: List,
- val albumArtistSortNames: List,
- val genreNames: List,
- val replayGainTrackAdjustment: Float?,
- val replayGainAlbumAdjustment: Float?,
- val coverId: String?,
-) {
- object Converters {
- @TypeConverter
- fun fromMultiValue(values: List) =
- values.joinToString(";") { it.replace(";", "\\;") }
-
- @TypeConverter
- fun toMultiValue(string: String) = string.splitEscaped { it == ';' }.correctWhitespace()
-
- @TypeConverter fun fromDate(date: Date?) = date?.toString()
-
- @TypeConverter fun toDate(string: String?) = string?.let(Date::from)
- }
-
- companion object {
- fun fromRawSong(rawSong: RawSong) =
- CachedSong(
- uri = rawSong.file.uri.toString(),
- modifiedMs = rawSong.file.modifiedMs,
- addedMs = rawSong.addedMs,
- // Should be strictly monotonic so we don't prune this
- // by accident later.
- touchedNs = System.nanoTime(),
- musicBrainzId = rawSong.tags.musicBrainzId,
- name = rawSong.tags.name,
- sortName = rawSong.tags.sortName,
- durationMs = rawSong.tags.durationMs,
- track = rawSong.tags.track,
- disc = rawSong.tags.disc,
- subtitle = rawSong.tags.subtitle,
- date = rawSong.tags.date,
- albumMusicBrainzId = rawSong.tags.albumMusicBrainzId,
- albumName = rawSong.tags.albumName,
- albumSortName = rawSong.tags.albumSortName,
- releaseTypes = rawSong.tags.releaseTypes,
- artistMusicBrainzIds = rawSong.tags.artistMusicBrainzIds,
- artistNames = rawSong.tags.artistNames,
- artistSortNames = rawSong.tags.artistSortNames,
- albumArtistMusicBrainzIds = rawSong.tags.albumArtistMusicBrainzIds,
- albumArtistNames = rawSong.tags.albumArtistNames,
- albumArtistSortNames = rawSong.tags.albumArtistSortNames,
- genreNames = rawSong.tags.genreNames,
- replayGainTrackAdjustment = rawSong.tags.replayGainTrackAdjustment,
- replayGainAlbumAdjustment = rawSong.tags.replayGainAlbumAdjustment,
- coverId = rawSong.cover?.id,
- mimeType = rawSong.properties.mimeType,
- bitrateHz = rawSong.properties.bitrateKbps,
- sampleRateHz = rawSong.properties.sampleRateHz)
- }
-}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/DBSongCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/DBSongCache.kt
new file mode 100644
index 000000000..d69b3e07b
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/DBSongCache.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2024 Auxio Project
+ * DBSongCache.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 .
+ */
+
+package org.oxycblt.musikr.cache
+
+import android.content.Context
+import org.oxycblt.musikr.Song
+import org.oxycblt.musikr.fs.device.DeviceFile
+import org.oxycblt.musikr.metadata.Properties
+import org.oxycblt.musikr.tag.parse.ParsedTags
+
+class DBSongCache
+private constructor(private val readDao: CacheReadDao, private val writeDao: CacheWriteDao) :
+ MutableSongCache {
+ override suspend fun read(file: DeviceFile): CacheResult {
+ val data = readDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file)
+ if (data.modifiedMs != file.modifiedMs) {
+ // We *found* this file earlier, but it's out of date.
+ // Send back it with the timestamp so it will be re-used.
+ // The touch timestamp will be updated on write.
+ return CacheResult.Outdated(file, data.addedMs)
+ }
+ val cachedSong =
+ data.run {
+ CachedSong(
+ file,
+ Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
+ ParsedTags(
+ musicBrainzId = musicBrainzId,
+ name = name,
+ sortName = sortName,
+ durationMs = durationMs,
+ track = track,
+ disc = disc,
+ subtitle = subtitle,
+ date = date,
+ albumMusicBrainzId = albumMusicBrainzId,
+ albumName = albumName,
+ albumSortName = albumSortName,
+ releaseTypes = releaseTypes,
+ artistMusicBrainzIds = artistMusicBrainzIds,
+ artistNames = artistNames,
+ artistSortNames = artistSortNames,
+ albumArtistMusicBrainzIds = albumArtistMusicBrainzIds,
+ albumArtistNames = albumArtistNames,
+ albumArtistSortNames = albumArtistSortNames,
+ genreNames = genreNames,
+ replayGainTrackAdjustment = replayGainTrackAdjustment,
+ replayGainAlbumAdjustment = replayGainAlbumAdjustment),
+ coverId = coverId,
+ addedMs = addedMs)
+ }
+ return CacheResult.Hit(cachedSong)
+ }
+
+ override suspend fun write(song: CachedSong) {
+ writeDao.updateSong(
+ CachedSongData(
+ uri = song.file.uri.toString(),
+ modifiedMs = song.file.modifiedMs,
+ addedMs = song.addedMs,
+ mimeType = song.properties.mimeType,
+ durationMs = song.properties.durationMs,
+ bitrateHz = song.properties.bitrateKbps,
+ sampleRateHz = song.properties.sampleRateHz,
+ musicBrainzId = song.tags.musicBrainzId,
+ name = song.tags.name,
+ sortName = song.tags.sortName,
+ track = song.tags.track,
+ disc = song.tags.disc,
+ subtitle = song.tags.subtitle,
+ date = song.tags.date,
+ albumMusicBrainzId = song.tags.albumMusicBrainzId,
+ albumName = song.tags.albumName,
+ albumSortName = song.tags.albumSortName,
+ releaseTypes = song.tags.releaseTypes,
+ artistMusicBrainzIds = song.tags.artistMusicBrainzIds,
+ artistNames = song.tags.artistNames,
+ artistSortNames = song.tags.artistSortNames,
+ albumArtistMusicBrainzIds = song.tags.albumArtistMusicBrainzIds,
+ albumArtistNames = song.tags.albumArtistNames,
+ albumArtistSortNames = song.tags.albumArtistSortNames,
+ genreNames = song.tags.genreNames,
+ replayGainTrackAdjustment = song.tags.replayGainTrackAdjustment,
+ replayGainAlbumAdjustment = song.tags.replayGainAlbumAdjustment,
+ coverId = song.coverId))
+ }
+
+ override suspend fun cleanup(exclude: Collection) {
+ writeDao.deleteExcludingUris(exclude.map { it.uri.toString() })
+ }
+
+ companion object {
+ fun from(context: Context) =
+ CacheDatabase.from(context).run { DBSongCache(readDao(), writeDao()) }
+ }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/SongCache.kt
similarity index 60%
rename from musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt
rename to musikr/src/main/java/org/oxycblt/musikr/cache/SongCache.kt
index b6145a44e..9d1015a3f 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/SongCache.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2024 Auxio Project
- * Cache.kt is part of Auxio.
+ * SongCache.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
@@ -18,31 +18,31 @@
package org.oxycblt.musikr.cache
-import org.oxycblt.musikr.fs.DeviceFile
+import org.oxycblt.musikr.Song
+import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Properties
-import org.oxycblt.musikr.pipeline.RawSong
import org.oxycblt.musikr.tag.parse.ParsedTags
-abstract class Cache {
- internal abstract suspend fun read(file: DeviceFile): CacheResult
-
- internal abstract suspend fun write(song: RawSong)
-
- internal abstract suspend fun finalize(songs: List)
-
- abstract class Factory {
- internal abstract fun open(): Cache
- }
+interface SongCache {
+ suspend fun read(file: DeviceFile): CacheResult
}
-internal sealed interface CacheResult {
- data class Hit(
- val file: DeviceFile,
- val properties: Properties,
- val tags: ParsedTags,
- val coverId: String?,
- val addedMs: Long
- ) : CacheResult
+interface MutableSongCache : SongCache {
+ suspend fun write(song: CachedSong)
+
+ suspend fun cleanup(exclude: Collection)
+}
+
+data class CachedSong(
+ val file: DeviceFile,
+ val properties: Properties,
+ val tags: ParsedTags,
+ val coverId: String?,
+ val addedMs: Long
+)
+
+sealed interface CacheResult {
+ data class Hit(val song: CachedSong) : CacheResult
data class Outdated(val file: DeviceFile, val addedMs: Long) : CacheResult
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/SongCacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/SongCacheDatabase.kt
new file mode 100644
index 000000000..95dbc2a87
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/cache/SongCacheDatabase.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2023 Auxio Project
+ * SongCacheDatabase.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 .
+ */
+
+package org.oxycblt.musikr.cache
+
+import android.content.Context
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.Transaction
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import org.oxycblt.musikr.tag.Date
+import org.oxycblt.musikr.util.correctWhitespace
+import org.oxycblt.musikr.util.splitEscaped
+
+@Database(entities = [CachedSongData::class], version = 57, exportSchema = false)
+internal abstract class CacheDatabase : RoomDatabase() {
+ abstract fun readDao(): CacheReadDao
+
+ abstract fun writeDao(): CacheWriteDao
+
+ companion object {
+ fun from(context: Context) =
+ Room.databaseBuilder(
+ context.applicationContext, CacheDatabase::class.java, "music_cache.db")
+ .fallbackToDestructiveMigration()
+ .build()
+ }
+}
+
+@Dao
+internal interface CacheReadDao {
+ @Query("SELECT * FROM CachedSongData WHERE uri = :uri")
+ suspend fun selectSong(uri: String): CachedSongData?
+}
+
+@Dao
+internal interface CacheWriteDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun updateSong(cachedSong: CachedSongData)
+
+ /** Delete every CachedSong whose URI is not in the uris list */
+ @Transaction
+ suspend fun deleteExcludingUris(uris: List) {
+ // SQLite has a limit of 999 variables in a query
+ val chunks = uris.chunked(999)
+ for (chunk in chunks) {
+ deleteExcludingUriChunk(chunk)
+ }
+ }
+
+ @Query("DELETE FROM CachedSongData WHERE uri NOT IN (:uris)")
+ suspend fun deleteExcludingUriChunk(uris: List)
+}
+
+@Entity
+@TypeConverters(CachedSongData.Converters::class)
+internal data class CachedSongData(
+ @PrimaryKey val uri: String,
+ val modifiedMs: Long,
+ val addedMs: Long,
+ val mimeType: String,
+ val durationMs: Long,
+ val bitrateHz: Int,
+ val sampleRateHz: Int,
+ val musicBrainzId: String?,
+ val name: String,
+ val sortName: String?,
+ val track: Int?,
+ val disc: Int?,
+ val subtitle: String?,
+ val date: Date?,
+ val albumMusicBrainzId: String?,
+ val albumName: String?,
+ val albumSortName: String?,
+ val releaseTypes: List,
+ val artistMusicBrainzIds: List,
+ val artistNames: List,
+ val artistSortNames: List,
+ val albumArtistMusicBrainzIds: List,
+ val albumArtistNames: List,
+ val albumArtistSortNames: List,
+ val genreNames: List,
+ val replayGainTrackAdjustment: Float?,
+ val replayGainAlbumAdjustment: Float?,
+ val coverId: String?,
+) {
+ object Converters {
+ @TypeConverter
+ fun fromMultiValue(values: List) =
+ values.joinToString(";") { it.replace(";", "\\;") }
+
+ @TypeConverter
+ fun toMultiValue(string: String) = string.splitEscaped { it == ';' }.correctWhitespace()
+
+ @TypeConverter fun fromDate(date: Date?) = date?.toString()
+
+ @TypeConverter fun toDate(string: String?) = string?.let(Date::from)
+ }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
deleted file mode 100644
index 7ec7df30c..000000000
--- a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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 .
- */
-
-package org.oxycblt.musikr.cache
-
-import android.content.Context
-import org.oxycblt.musikr.cover.Covers
-import org.oxycblt.musikr.fs.DeviceFile
-import org.oxycblt.musikr.metadata.Properties
-import org.oxycblt.musikr.pipeline.RawSong
-import org.oxycblt.musikr.tag.parse.ParsedTags
-
-interface StoredCache {
- fun visible(): Cache.Factory
-
- fun invisible(): Cache.Factory
-
- companion object {
- fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context))
- }
-}
-
-private class StoredCacheImpl(private val cacheDatabase: CacheDatabase) : StoredCache {
- override fun visible(): Cache.Factory = VisibleStoredCache.Factory(cacheDatabase)
-
- override fun invisible(): Cache.Factory = InvisibleStoredCache.Factory(cacheDatabase)
-}
-
-private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) : Cache() {
- private val created = System.nanoTime()
-
- override suspend fun write(song: RawSong) = writeDao.updateSong(CachedSong.fromRawSong(song))
-
- override suspend fun finalize() {
- // Anything not create during this cache's use implies that it has not been
- // access during this run and should be pruned.
- writeDao.pruneOlderThan(created)
- }
-}
-
-private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeDao: CacheWriteDao) :
- BaseStoredCache(writeDao) {
- override suspend fun read(file: DeviceFile, covers: Covers): CacheResult {
- val cachedSong = visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file)
- if (cachedSong.modifiedMs != file.modifiedMs) {
- // We *found* this file earlier, but it's out of date.
- // Send back it with the timestamp so it will be re-used.
- // The touch timestamp will be updated on write.
- return CacheResult.Outdated(file, cachedSong.addedMs)
- }
- // Valid file, update the touch time.
- visibleDao.touch(file.uri.toString())
- return cachedSong.run {
- CacheResult.Hit(
- file,
- Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
- ParsedTags(
- musicBrainzId = musicBrainzId,
- name = name,
- sortName = sortName,
- durationMs = durationMs,
- track = track,
- disc = disc,
- subtitle = subtitle,
- date = date,
- albumMusicBrainzId = albumMusicBrainzId,
- albumName = albumName,
- albumSortName = albumSortName,
- releaseTypes = releaseTypes,
- artistMusicBrainzIds = artistMusicBrainzIds,
- artistNames = artistNames,
- artistSortNames = artistSortNames,
- albumArtistMusicBrainzIds = albumArtistMusicBrainzIds,
- albumArtistNames = albumArtistNames,
- albumArtistSortNames = albumArtistSortNames,
- genreNames = genreNames,
- replayGainTrackAdjustment = replayGainTrackAdjustment,
- replayGainAlbumAdjustment = replayGainAlbumAdjustment),
- coverId = coverId,
- addedMs = addedMs)
- }
- }
-
- class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
- override fun open() =
- VisibleStoredCache(cacheDatabase.visibleDao(), cacheDatabase.writeDao())
- }
-}
-
-private class InvisibleStoredCache(
- private val invisibleCacheDao: InvisibleCacheDao,
- writeDao: CacheWriteDao
-) : BaseStoredCache(writeDao) {
- override suspend fun read(file: DeviceFile, covers: Covers): CacheResult {
- val addedMs =
- invisibleCacheDao.selectAddedMs(file.uri.toString()) ?: return CacheResult.Miss(file)
- return CacheResult.Outdated(file, addedMs)
- }
-
- class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
- override fun open() =
- InvisibleStoredCache(cacheDatabase.invisibleDao(), cacheDatabase.writeDao())
- }
-}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt
similarity index 90%
rename from musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt
rename to musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt
index 6baac772f..4c9153c4a 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/fs/DeviceFile.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt
@@ -16,11 +16,12 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.fs
+package org.oxycblt.musikr.fs.device
import android.net.Uri
+import org.oxycblt.musikr.fs.Path
-internal data class DeviceFile(
+data class DeviceFile(
val uri: Uri,
val mimeType: String,
val path: Path,
diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt
index 2f737995c..b2cb69ee8 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt
@@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flow
-import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.Path
diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt
index 758e4483a..f73745c95 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/Metadata.kt
@@ -53,7 +53,7 @@ internal data class Metadata(
}
}
-internal data class Properties(
+data class Properties(
val mimeType: String,
val durationMs: Long,
val bitrateKbps: Int,
diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
index ccc39914a..08a7f8903 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/MetadataExtractor.kt
@@ -24,7 +24,7 @@ import android.os.ParcelFileDescriptor
import java.io.FileInputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import org.oxycblt.musikr.fs.DeviceFile
+import org.oxycblt.musikr.fs.device.DeviceFile
internal interface MetadataExtractor {
suspend fun open(deviceFile: DeviceFile): MetadataHandle?
diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt
index 1f5a01a99..eec84dbea 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt
@@ -21,7 +21,7 @@ package org.oxycblt.musikr.metadata
import android.util.Log
import java.io.FileInputStream
import java.nio.ByteBuffer
-import org.oxycblt.musikr.fs.DeviceFile
+import org.oxycblt.musikr.fs.device.DeviceFile
internal class NativeInputStream(private val deviceFile: DeviceFile, fis: FileInputStream) {
private val channel = fis.channel
diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt
index d5105f3a8..e0b48ee00 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/TagLibJNI.kt
@@ -19,7 +19,7 @@
package org.oxycblt.musikr.metadata
import java.io.FileInputStream
-import org.oxycblt.musikr.fs.DeviceFile
+import org.oxycblt.musikr.fs.device.DeviceFile
internal object TagLibJNI {
init {
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt
index 2b68f3b93..663220c43 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt
@@ -30,11 +30,10 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage
-import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cache.CacheResult
+import org.oxycblt.musikr.cache.SongCache
import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.ObtainResult
-import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.device.DeviceFiles
import org.oxycblt.musikr.playlist.db.StoredPlaylists
@@ -53,42 +52,47 @@ internal interface ExploreStep {
private class ExploreStepImpl(
private val deviceFiles: DeviceFiles,
private val storedPlaylists: StoredPlaylists,
- private val cache: Cache,
+ private val songCache: SongCache,
private val covers: Covers
) : ExploreStep {
override fun explore(locations: List): Flow {
- val audios =
+ val audioFiles =
deviceFiles
.explore(locations.asFlow())
.filter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE }
- .map { evaluateAudio(it) }
.flowOn(Dispatchers.IO)
.buffer()
- val playlists =
+ val readDistribution = audioFiles.distribute(8)
+ val read =
+ readDistribution.flows.mapx { flow ->
+ flow
+ .tryMap { file ->
+ when (val cacheResult = songCache.read(file)) {
+ is CacheResult.Hit -> {
+ val cachedSong = cacheResult.song
+ val coverResult = cachedSong.coverId?.let { covers.obtain(it) }
+ if (coverResult !is ObtainResult.Hit) {
+ return@tryMap NewSong(file, cachedSong.addedMs)
+ }
+ RawSong(
+ cachedSong.file,
+ cachedSong.properties,
+ cachedSong.tags,
+ coverResult.cover,
+ cachedSong.addedMs)
+ }
+ is CacheResult.Outdated -> NewSong(file, cacheResult.addedMs)
+ is CacheResult.Miss -> NewSong(file, System.currentTimeMillis())
+ }
+ }
+ .flowOn(Dispatchers.IO)
+ .buffer()
+ }
+ val storedPlaylists =
flow { emitAll(storedPlaylists.read().asFlow()) }
.map { RawPlaylist(it) }
.flowOn(Dispatchers.IO)
.buffer()
- return merge(audios, playlists)
- }
-
- private suspend fun evaluateAudio(file: DeviceFile): Explored {
- return when (val cacheResult = cache.read(file)) {
- is CacheResult.Hit -> {
- val coverResult = cacheResult.coverId?.let { covers.obtain(it) }
- when (coverResult) {
- is ObtainResult.Hit ->
- RawSong(
- file,
- cacheResult.properties,
- cacheResult.tags,
- coverResult.cover,
- cacheResult.addedMs)
- else -> NewSong(file, cacheResult.addedMs)
- }
- }
- is CacheResult.Outdated -> NewSong(file, cacheResult.addedMs)
- is CacheResult.Miss -> NewSong(file, System.currentTimeMillis())
- }
+ return merge(readDistribution.manager, *read, storedPlaylists)
}
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
index fc52b23ed..fdc57ef9a 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt
@@ -28,7 +28,8 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage
-import org.oxycblt.musikr.cache.Cache
+import org.oxycblt.musikr.cache.CachedSong
+import org.oxycblt.musikr.cache.MutableSongCache
import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.metadata.Metadata
import org.oxycblt.musikr.metadata.MetadataExtractor
@@ -48,7 +49,7 @@ internal interface ExtractStep {
private class ExtractStepImpl(
private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser,
- private val cache: Cache,
+ private val cache: MutableSongCache,
private val storedCovers: MutableCovers
) : ExtractStep {
override fun extract(nodes: Flow): Flow {
@@ -107,7 +108,9 @@ private class ExtractStepImpl(
writeDistribution.flows.mapx { flow ->
flow
.tryMap {
- cache.write(it)
+ val cachedSong =
+ CachedSong(it.file, it.properties, it.tags, it.cover?.id, it.addedMs)
+ cache.write(cachedSong)
it
}
.flowOn(Dispatchers.IO)
diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt
index 32434d620..bd4ef2753 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineItem.kt
@@ -19,7 +19,7 @@
package org.oxycblt.musikr.pipeline
import org.oxycblt.musikr.cover.Cover
-import org.oxycblt.musikr.fs.DeviceFile
+import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.tag.parse.ParsedTags
diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt
index a7dc4d3c5..2a143aee7 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt
@@ -20,7 +20,7 @@ package org.oxycblt.musikr.tag.parse
import org.oxycblt.musikr.tag.Date
-internal data class ParsedTags(
+data class ParsedTags(
val durationMs: Long,
val replayGainTrackAdjustment: Float? = null,
val replayGainAlbumAdjustment: Float? = null,
diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt
index 42d76af43..3d67f07b9 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt
@@ -18,7 +18,7 @@
package org.oxycblt.musikr.tag.parse
-import org.oxycblt.musikr.fs.DeviceFile
+import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
import org.oxycblt.musikr.util.unlikelyToBeNull