musikr: build new cache api

- No more factory pattern
- Extendable API
This commit is contained in:
Alexander Capehart 2025-01-21 14:18:44 -07:00
parent 0e2efe2c88
commit dbf2dd510c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
21 changed files with 353 additions and 351 deletions

View file

@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.image.covers.SettingCovers import org.oxycblt.auxio.image.covers.SettingCovers
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
import org.oxycblt.auxio.music.shim.WriteOnlySongCache
import org.oxycblt.musikr.IndexingProgress import org.oxycblt.musikr.IndexingProgress
import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.Library import org.oxycblt.musikr.Library
@ -38,7 +39,7 @@ import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.Storage 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.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
@ -236,7 +237,7 @@ class MusicRepositoryImpl
@Inject @Inject
constructor( constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val storedCache: StoredCache, private val songCache: MutableSongCache,
private val storedPlaylists: StoredPlaylists, private val storedPlaylists: StoredPlaylists,
private val settingCovers: SettingCovers, private val settingCovers: SettingCovers,
private val musicSettings: MusicSettings private val musicSettings: MusicSettings
@ -387,7 +388,7 @@ constructor(
val currentRevision = musicSettings.revision val currentRevision = musicSettings.revision
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() 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 covers = settingCovers.create(context, newRevision)
val storage = Storage(cache, covers, storedPlaylists) val storage = Storage(cache, covers, storedPlaylists)
val interpretation = Interpretation(nameFactory, separators) val interpretation = Interpretation(nameFactory, separators)

View file

@ -25,7 +25,8 @@ 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.StoredCache import org.oxycblt.musikr.cache.DBSongCache
import org.oxycblt.musikr.cache.SongCache
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
@Module @Module
@ -33,7 +34,7 @@ import org.oxycblt.musikr.playlist.db.StoredPlaylists
class MusikrShimModule { class MusikrShimModule {
@Singleton @Singleton
@Provides @Provides
fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context) fun songCache(@ApplicationContext context: Context): SongCache = DBSongCache.from(context)
@Singleton @Singleton
@Provides @Provides

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Song>) {
songCache.cleanup(exclude)
}
}

View file

@ -18,7 +18,7 @@
package org.oxycblt.musikr 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.cover.MutableCovers
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming 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. * 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. * 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 * A repository of cover images to for re-use during music loading. Should be kept in lock-step

View file

@ -160,6 +160,7 @@ private class LibraryResultImpl(
override val library: MutableLibrary override val library: MutableLibrary
) : LibraryResult { ) : LibraryResult {
override suspend fun cleanup() { override suspend fun cleanup() {
storage.cache.cleanup(library.songs)
storage.covers.cleanup(library.songs.mapNotNull { it.cover }) storage.covers.cleanup(library.songs.mapNotNull { it.cover })
} }
} }

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>,
val artistMusicBrainzIds: List<String>,
val artistNames: List<String>,
val artistSortNames: List<String>,
val albumArtistMusicBrainzIds: List<String>,
val albumArtistNames: List<String>,
val albumArtistSortNames: List<String>,
val genreNames: List<String>,
val replayGainTrackAdjustment: Float?,
val replayGainAlbumAdjustment: Float?,
val coverId: String?,
) {
object Converters {
@TypeConverter
fun fromMultiValue(values: List<String>) =
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)
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Song>) {
writeDao.deleteExcludingUris(exclude.map { it.uri.toString() })
}
companion object {
fun from(context: Context) =
CacheDatabase.from(context).run { DBSongCache(readDao(), writeDao()) }
}
}

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * 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 * 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 * it under the terms of the GNU General Public License as published by
@ -18,31 +18,31 @@
package org.oxycblt.musikr.cache 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.metadata.Properties
import org.oxycblt.musikr.pipeline.RawSong
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags
abstract class Cache { interface SongCache {
internal abstract suspend fun read(file: DeviceFile): CacheResult suspend fun read(file: DeviceFile): CacheResult
internal abstract suspend fun write(song: RawSong)
internal abstract suspend fun finalize(songs: List<RawSong>)
abstract class Factory {
internal abstract fun open(): Cache
}
} }
internal sealed interface CacheResult { interface MutableSongCache : SongCache {
data class Hit( suspend fun write(song: CachedSong)
val file: DeviceFile,
val properties: Properties, suspend fun cleanup(exclude: Collection<Song>)
val tags: ParsedTags, }
val coverId: String?,
val addedMs: Long data class CachedSong(
) : CacheResult 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 data class Outdated(val file: DeviceFile, val addedMs: Long) : CacheResult

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>) {
// 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<String>)
}
@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<String>,
val artistMusicBrainzIds: List<String>,
val artistNames: List<String>,
val artistSortNames: List<String>,
val albumArtistMusicBrainzIds: List<String>,
val albumArtistNames: List<String>,
val albumArtistSortNames: List<String>,
val genreNames: List<String>,
val replayGainTrackAdjustment: Float?,
val replayGainAlbumAdjustment: Float?,
val coverId: String?,
) {
object Converters {
@TypeConverter
fun fromMultiValue(values: List<String>) =
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)
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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())
}
}

View file

@ -16,11 +16,12 @@
* 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.musikr.fs package org.oxycblt.musikr.fs.device
import android.net.Uri import android.net.Uri
import org.oxycblt.musikr.fs.Path
internal data class DeviceFile( data class DeviceFile(
val uri: Uri, val uri: Uri,
val mimeType: String, val mimeType: String,
val path: Path, val path: Path,

View file

@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path

View file

@ -53,7 +53,7 @@ internal data class Metadata(
} }
} }
internal data class Properties( data class Properties(
val mimeType: String, val mimeType: String,
val durationMs: Long, val durationMs: Long,
val bitrateKbps: Int, val bitrateKbps: Int,

View file

@ -24,7 +24,7 @@ import android.os.ParcelFileDescriptor
import java.io.FileInputStream import java.io.FileInputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
internal interface MetadataExtractor { internal interface MetadataExtractor {
suspend fun open(deviceFile: DeviceFile): MetadataHandle? suspend fun open(deviceFile: DeviceFile): MetadataHandle?

View file

@ -21,7 +21,7 @@ package org.oxycblt.musikr.metadata
import android.util.Log import android.util.Log
import java.io.FileInputStream import java.io.FileInputStream
import java.nio.ByteBuffer 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) { internal class NativeInputStream(private val deviceFile: DeviceFile, fis: FileInputStream) {
private val channel = fis.channel private val channel = fis.channel

View file

@ -19,7 +19,7 @@
package org.oxycblt.musikr.metadata package org.oxycblt.musikr.metadata
import java.io.FileInputStream import java.io.FileInputStream
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.device.DeviceFile
internal object TagLibJNI { internal object TagLibJNI {
init { init {

View file

@ -30,11 +30,10 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cache.CacheResult import org.oxycblt.musikr.cache.CacheResult
import org.oxycblt.musikr.cache.SongCache
import org.oxycblt.musikr.cover.Covers import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.ObtainResult import org.oxycblt.musikr.cover.ObtainResult
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.device.DeviceFiles import org.oxycblt.musikr.fs.device.DeviceFiles
import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.playlist.db.StoredPlaylists
@ -53,42 +52,47 @@ internal interface ExploreStep {
private class ExploreStepImpl( private class ExploreStepImpl(
private val deviceFiles: DeviceFiles, private val deviceFiles: DeviceFiles,
private val storedPlaylists: StoredPlaylists, private val storedPlaylists: StoredPlaylists,
private val cache: Cache, private val songCache: SongCache,
private val covers: Covers private val covers: Covers
) : ExploreStep { ) : ExploreStep {
override fun explore(locations: List<MusicLocation>): Flow<Explored> { override fun explore(locations: List<MusicLocation>): Flow<Explored> {
val audios = val audioFiles =
deviceFiles deviceFiles
.explore(locations.asFlow()) .explore(locations.asFlow())
.filter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE } .filter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE }
.map { evaluateAudio(it) }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer() .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()) } flow { emitAll(storedPlaylists.read().asFlow()) }
.map { RawPlaylist(it) } .map { RawPlaylist(it) }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer() .buffer()
return merge(audios, playlists) return merge(readDistribution.manager, *read, storedPlaylists)
}
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())
}
} }
} }

View file

@ -28,7 +28,8 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage 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.cover.MutableCovers
import org.oxycblt.musikr.metadata.Metadata import org.oxycblt.musikr.metadata.Metadata
import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.metadata.MetadataExtractor
@ -48,7 +49,7 @@ internal interface ExtractStep {
private class ExtractStepImpl( private class ExtractStepImpl(
private val metadataExtractor: MetadataExtractor, private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser, private val tagParser: TagParser,
private val cache: Cache, private val cache: MutableSongCache,
private val storedCovers: MutableCovers private val storedCovers: MutableCovers
) : ExtractStep { ) : ExtractStep {
override fun extract(nodes: Flow<Explored.New>): Flow<Extracted> { override fun extract(nodes: Flow<Explored.New>): Flow<Extracted> {
@ -107,7 +108,9 @@ private class ExtractStepImpl(
writeDistribution.flows.mapx { flow -> writeDistribution.flows.mapx { flow ->
flow flow
.tryMap { .tryMap {
cache.write(it) val cachedSong =
CachedSong(it.file, it.properties, it.tags, it.cover?.id, it.addedMs)
cache.write(cachedSong)
it it
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)

View file

@ -19,7 +19,7 @@
package org.oxycblt.musikr.pipeline package org.oxycblt.musikr.pipeline
import org.oxycblt.musikr.cover.Cover 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.metadata.Properties
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags

View file

@ -20,7 +20,7 @@ package org.oxycblt.musikr.tag.parse
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date
internal data class ParsedTags( data class ParsedTags(
val durationMs: Long, val durationMs: Long,
val replayGainTrackAdjustment: Float? = null, val replayGainTrackAdjustment: Float? = null,
val replayGainAlbumAdjustment: Float? = null, val replayGainAlbumAdjustment: Float? = null,

View file

@ -18,7 +18,7 @@
package org.oxycblt.musikr.tag.parse 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.metadata.Metadata
import org.oxycblt.musikr.util.unlikelyToBeNull import org.oxycblt.musikr.util.unlikelyToBeNull