Local manga index in database
parent
66644d55a4
commit
169e31e9ba
@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration22To23 : Migration(22, 23) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `local_index` (`manga_id` INTEGER NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import androidx.collection.MutableLongObjectMap
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
|
||||
class LocalMangaMappingCache {
|
||||
|
||||
private val map = MutableLongObjectMap<File>()
|
||||
|
||||
suspend fun get(mangaId: Long): LocalManga? {
|
||||
val file = synchronized(this) {
|
||||
map[mangaId]
|
||||
} ?: return null
|
||||
return runCatchingCancellable {
|
||||
LocalMangaInput.of(file).getManga()
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
operator fun set(mangaId: Long, localManga: LocalManga?) = synchronized(this) {
|
||||
if (localManga == null) {
|
||||
map.remove(mangaId)
|
||||
} else {
|
||||
map[mangaId] = localManga.file
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package org.koitharu.kotatsu.local.data.index
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import androidx.room.withTransaction
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class LocalMangaIndex @Inject constructor(
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
private val db: MangaDatabase,
|
||||
private val localStorageManager: LocalStorageManager,
|
||||
@ApplicationContext context: Context,
|
||||
private val localMangaRepositoryProvider: Provider<LocalMangaRepository>,
|
||||
) : FlowCollector<LocalManga?> {
|
||||
|
||||
private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
private var previousHash: Long
|
||||
get() = prefs.getLong(KEY_HASH, 0L)
|
||||
set(value) = prefs.edit { putLong(KEY_HASH, value) }
|
||||
|
||||
override suspend fun emit(value: LocalManga?) {
|
||||
if (value != null) {
|
||||
put(value)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun update(): Boolean {
|
||||
val newHash = computeHash()
|
||||
if (newHash == previousHash) {
|
||||
return false
|
||||
}
|
||||
db.withTransaction {
|
||||
val dao = db.getLocalMangaIndexDao()
|
||||
dao.clear()
|
||||
localMangaRepositoryProvider.get().getRawListAsFlow()
|
||||
.collect { dao.upsert(it.toEntity()) }
|
||||
}
|
||||
previousHash = newHash
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun get(mangaId: Long): LocalManga? {
|
||||
val path = db.getLocalMangaIndexDao().findPath(mangaId) ?: return null
|
||||
return runCatchingCancellable {
|
||||
LocalMangaInput.of(File(path)).getManga()
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
suspend fun put(manga: LocalManga) = db.withTransaction {
|
||||
mangaDataRepository.storeManga(manga.manga)
|
||||
db.getLocalMangaIndexDao().upsert(manga.toEntity())
|
||||
}
|
||||
|
||||
suspend fun delete(mangaId: Long) {
|
||||
db.getLocalMangaIndexDao().delete(mangaId)
|
||||
}
|
||||
|
||||
private fun LocalManga.toEntity() = LocalMangaIndexEntity(
|
||||
mangaId = manga.id,
|
||||
path = file.path,
|
||||
)
|
||||
|
||||
private suspend fun computeHash(): Long {
|
||||
return runCatchingCancellable {
|
||||
localStorageManager.getReadableDirs()
|
||||
.fold(0L) { acc, file -> acc + file.computeHash() }
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrDefault(0L)
|
||||
}
|
||||
|
||||
private suspend fun File.computeHash(): Long = runInterruptible(Dispatchers.IO) {
|
||||
lastModified() // TODO size
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PREF_NAME = "_local_index"
|
||||
private const val KEY_HASH = "hash"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.local.data.index
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
|
||||
@Dao
|
||||
interface LocalMangaIndexDao {
|
||||
|
||||
@Query("SELECT path FROM local_index WHERE manga_id = :mangaId")
|
||||
suspend fun findPath(mangaId: Long): String?
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(entity: LocalMangaIndexEntity)
|
||||
|
||||
@Query("DELETE FROM local_index WHERE manga_id = :mangaId")
|
||||
suspend fun delete(mangaId: Long)
|
||||
|
||||
@Query("DELETE FROM local_index")
|
||||
suspend fun clear()
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.local.data.index
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "local_index",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
class LocalMangaIndexEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "path") val path: String,
|
||||
)
|
||||
@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.local.ui
|
||||
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
||||
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LocalIndexUpdateService : CoroutineIntentService() {
|
||||
|
||||
@Inject
|
||||
lateinit var localMangaIndex: LocalMangaIndex
|
||||
|
||||
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||
localMangaIndex.update()
|
||||
}
|
||||
|
||||
override fun onError(startId: Int, error: Throwable) = Unit
|
||||
}
|
||||
Loading…
Reference in New Issue