musikr: add date added support w/cache
This allows me to replicate something resembling date added support while reducing query load.
This commit is contained in:
parent
da76a03298
commit
4f920e922d
6 changed files with 46 additions and 21 deletions
|
@ -27,6 +27,7 @@ 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.Cache
|
||||||
|
import org.oxycblt.musikr.cache.StoredCache
|
||||||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
@ -40,7 +41,7 @@ interface MusicModule {
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class MusikrShimModule {
|
class MusikrShimModule {
|
||||||
@Singleton @Provides fun cache(@ApplicationContext context: Context) = Cache.from(context)
|
@Singleton @Provides fun storedCache(@ApplicationContext context: Context) = StoredCache.from(context)
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
@ -37,8 +37,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.Cache
|
import org.oxycblt.musikr.cache.StoredCache
|
||||||
import org.oxycblt.musikr.cache.WriteOnlyCache
|
|
||||||
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
|
||||||
|
@ -214,7 +213,7 @@ class MusicRepositoryImpl
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val cache: Cache,
|
private val storedCache: StoredCache,
|
||||||
private val storedPlaylists: StoredPlaylists,
|
private val storedPlaylists: StoredPlaylists,
|
||||||
private val musicSettings: MusicSettings
|
private val musicSettings: MusicSettings
|
||||||
) : MusicRepository {
|
) : MusicRepository {
|
||||||
|
@ -363,7 +362,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) cache else WriteOnlyCache(cache)
|
val cache = if (withCache) storedCache.full() else storedCache.writeOnly()
|
||||||
val covers = MutableRevisionedStoredCovers(context, newRevision)
|
val covers = MutableRevisionedStoredCovers(context, newRevision)
|
||||||
val storage = Storage(cache, covers, storedPlaylists)
|
val storage = Storage(cache, covers, storedPlaylists)
|
||||||
val interpretation = Interpretation(nameFactory, separators)
|
val interpretation = Interpretation(nameFactory, separators)
|
||||||
|
|
|
@ -27,30 +27,48 @@ abstract class Cache {
|
||||||
internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
|
internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
|
||||||
|
|
||||||
internal abstract suspend fun write(song: RawSong)
|
internal abstract suspend fun write(song: RawSong)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoredCache {
|
||||||
|
fun full(): Cache
|
||||||
|
|
||||||
|
fun writeOnly(): Cache
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(context: Context): Cache = CacheImpl(CacheDatabase.from(context).cachedSongsDao())
|
fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed interface CacheResult {
|
internal sealed interface CacheResult {
|
||||||
data class Hit(val song: RawSong) : CacheResult
|
data class Hit(val song: RawSong) : CacheResult
|
||||||
|
|
||||||
data class Miss(val file: DeviceFile) : CacheResult
|
data class Miss(val file: DeviceFile, val dateAdded: Long?) : CacheResult
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StoredCacheImpl(private val database: CacheDatabase) : StoredCache {
|
||||||
|
override fun full() = CacheImpl(database.cachedSongsDao())
|
||||||
|
|
||||||
|
override fun writeOnly() = WriteOnlyCacheImpl(database.cachedSongsDao())
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
|
private class CacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
|
||||||
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) =
|
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult {
|
||||||
cacheInfoDao.selectSong(file.uri.toString(), file.lastModified)?.let {
|
val song = cacheInfoDao.selectSong(file.uri.toString()) ?:
|
||||||
CacheResult.Hit(it.intoRawSong(file, storedCovers))
|
return CacheResult.Miss(file, null)
|
||||||
} ?: CacheResult.Miss(file)
|
if (song.dateModified != file.lastModified) {
|
||||||
|
return CacheResult.Miss(file, song.dateAdded)
|
||||||
|
}
|
||||||
|
return CacheResult.Hit(song.intoRawSong(file, storedCovers))
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun write(song: RawSong) =
|
override suspend fun write(song: RawSong) =
|
||||||
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
||||||
}
|
}
|
||||||
|
|
||||||
class WriteOnlyCache(private val inner: Cache) : Cache() {
|
private class WriteOnlyCacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
|
||||||
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = CacheResult.Miss(file)
|
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) =
|
||||||
|
CacheResult.Miss(file, cacheInfoDao.selectDateAdded(file.uri.toString()))
|
||||||
|
|
||||||
override suspend fun write(song: RawSong) = inner.write(song)
|
override suspend fun write(song: RawSong) =
|
||||||
|
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,11 @@ internal abstract class CacheDatabase : RoomDatabase() {
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
internal interface CacheInfoDao {
|
internal interface CacheInfoDao {
|
||||||
@Query("SELECT * FROM CachedSong WHERE uri = :uri AND dateModified = :dateModified")
|
@Query("SELECT * FROM CachedSong WHERE uri = :uri")
|
||||||
suspend fun selectSong(uri: String, dateModified: Long): CachedSong?
|
suspend fun selectSong(uri: String): CachedSong?
|
||||||
|
|
||||||
|
@Query("SELECT dateAdded FROM CachedSong WHERE uri = :uri")
|
||||||
|
suspend fun selectDateAdded(uri: String): Long?
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
|
||||||
}
|
}
|
||||||
|
@ -65,6 +68,7 @@ internal interface CacheInfoDao {
|
||||||
internal data class CachedSong(
|
internal data class CachedSong(
|
||||||
@PrimaryKey val uri: String,
|
@PrimaryKey val uri: String,
|
||||||
val dateModified: Long,
|
val dateModified: Long,
|
||||||
|
val dateAdded: Long,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
val durationMs: Long,
|
val durationMs: Long,
|
||||||
val bitrateHz: Int,
|
val bitrateHz: Int,
|
||||||
|
@ -117,7 +121,8 @@ internal data class CachedSong(
|
||||||
genreNames = genreNames,
|
genreNames = genreNames,
|
||||||
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
||||||
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
||||||
coverId?.let { storedCovers.obtain(it) })
|
coverId?.let { storedCovers.obtain(it) },
|
||||||
|
dateAdded = dateAdded)
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
|
@ -137,6 +142,7 @@ internal data class CachedSong(
|
||||||
CachedSong(
|
CachedSong(
|
||||||
uri = rawSong.file.uri.toString(),
|
uri = rawSong.file.uri.toString(),
|
||||||
dateModified = rawSong.file.lastModified,
|
dateModified = rawSong.file.lastModified,
|
||||||
|
dateAdded = rawSong.dateAdded,
|
||||||
musicBrainzId = rawSong.tags.musicBrainzId,
|
musicBrainzId = rawSong.tags.musicBrainzId,
|
||||||
name = rawSong.tags.name,
|
name = rawSong.tags.name,
|
||||||
sortName = rawSong.tags.sortName,
|
sortName = rawSong.tags.sortName,
|
||||||
|
|
|
@ -63,6 +63,7 @@ private class ExtractStepImpl(
|
||||||
private val storedCovers: MutableStoredCovers
|
private val storedCovers: MutableStoredCovers
|
||||||
) : ExtractStep {
|
) : ExtractStep {
|
||||||
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
val filterFlow =
|
val filterFlow =
|
||||||
nodes.divert {
|
nodes.divert {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
@ -127,7 +128,7 @@ private class ExtractStepImpl(
|
||||||
.mapNotNull { fileWith ->
|
.mapNotNull { fileWith ->
|
||||||
val tags = tagParser.parse(fileWith.file, fileWith.with)
|
val tags = tagParser.parse(fileWith.file, fileWith.with)
|
||||||
val cover = fileWith.with.cover?.let { storedCovers.write(it) }
|
val cover = fileWith.with.cover?.let { storedCovers.write(it) }
|
||||||
RawSong(fileWith.file, fileWith.with.properties, tags, cover)
|
RawSong(fileWith.file, fileWith.with.properties, tags, cover, startTime)
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
|
@ -163,7 +164,8 @@ internal data class RawSong(
|
||||||
val file: DeviceFile,
|
val file: DeviceFile,
|
||||||
val properties: Properties,
|
val properties: Properties,
|
||||||
val tags: ParsedTags,
|
val tags: ParsedTags,
|
||||||
val cover: Cover?
|
val cover: Cover?,
|
||||||
|
val dateAdded: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
internal sealed interface ExtractedMusic {
|
internal sealed interface ExtractedMusic {
|
||||||
|
|
|
@ -65,8 +65,7 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
|
||||||
size = song.file.size,
|
size = song.file.size,
|
||||||
format = Format.infer(song.file.mimeType, song.properties.mimeType),
|
format = Format.infer(song.file.mimeType, song.properties.mimeType),
|
||||||
lastModified = song.file.lastModified,
|
lastModified = song.file.lastModified,
|
||||||
// TODO: Figure out what to do with date added
|
dateAdded = song.dateAdded,
|
||||||
dateAdded = song.file.lastModified,
|
|
||||||
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
|
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
|
||||||
name = interpretation.naming.name(song.tags.name, song.tags.sortName),
|
name = interpretation.naming.name(song.tags.name, song.tags.sortName),
|
||||||
rawName = song.tags.name,
|
rawName = song.tags.name,
|
||||||
|
|
Loading…
Reference in a new issue