Local manga index in database

This commit is contained in:
Koitharu
2024-09-23 08:55:17 +03:00
committed by Mac135135
parent e0c983f4eb
commit 36bd3cc438
19 changed files with 248 additions and 98 deletions

View File

@@ -273,6 +273,7 @@
<service
android:name="org.koitharu.kotatsu.alternatives.ui.AutoFixService"
android:foregroundServiceType="dataSync" />
<service android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService" />
<service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />

View File

@@ -11,6 +11,7 @@ import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.acra.ACRA
@@ -28,6 +29,9 @@ import org.koitharu.kotatsu.core.os.AppValidator
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import java.security.Security
import javax.inject.Inject
@@ -60,6 +64,13 @@ open class BaseApp : Application(), Configuration.Provider {
@Inject
lateinit var workManagerProvider: Provider<WorkManager>
@Inject
lateinit var localMangaIndexProvider: Provider<LocalMangaIndex>
@Inject
@LocalStorageChanges
lateinit var localStorageChanges: MutableSharedFlow<LocalManga?>
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
@@ -82,6 +93,7 @@ open class BaseApp : Application(), Configuration.Provider {
}
processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers()
localStorageChanges.collect(localMangaIndexProvider.get())
}
workScheduleManager.init()
WorkServiceStopHelper(workManagerProvider).setup()

View File

@@ -35,6 +35,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration19To20
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
import org.koitharu.kotatsu.core.db.migrations.Migration21To22
import org.koitharu.kotatsu.core.db.migrations.Migration22To23
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -50,6 +51,8 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouritesDao
import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.local.data.index.LocalMangaIndexDao
import org.koitharu.kotatsu.local.data.index.LocalMangaIndexEntity
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingDao
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.stats.data.StatsDao
@@ -60,14 +63,14 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 22
const val DATABASE_VERSION = 23
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
],
version = DATABASE_VERSION,
)
@@ -98,6 +101,8 @@ abstract class MangaDatabase : RoomDatabase() {
abstract fun getSourcesDao(): MangaSourcesDao
abstract fun getStatsDao(): StatsDao
abstract fun getLocalMangaIndexDao(): LocalMangaIndexDao
}
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
@@ -122,6 +127,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration19To20(),
Migration20To21(),
Migration21To22(),
Migration22To23(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

View File

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

View File

@@ -71,6 +71,7 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.TempFileFilter
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.MangaLock

View File

@@ -199,6 +199,7 @@ abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
ListFilterOption.Downloaded -> "EXISTS(SELECT * FROM local_index WHERE local_index.manga_id = favourites.manga_id)"
else -> null
}
}

View File

@@ -42,7 +42,7 @@ class FavouritesRepository @Inject constructor(
fun observeAll(order: ListSortOrder, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
if (ListFilterOption.Downloaded in filterOptions) {
return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit)
return localObserver.observeAll(order, filterOptions, limit)
}
return db.getFavouritesDao().observeAll(order, filterOptions, limit)
.mapItems { it.toManga() }
@@ -60,7 +60,7 @@ class FavouritesRepository @Inject constructor(
limit: Int
): Flow<List<Manga>> {
if (ListFilterOption.Downloaded in filterOptions) {
return localObserver.observeAll(categoryId, order, filterOptions - ListFilterOption.Downloaded, limit)
return localObserver.observeAll(categoryId, order, filterOptions, limit)
}
return db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit)
.mapItems { it.toManga() }

View File

@@ -2,29 +2,30 @@ package org.koitharu.kotatsu.favourites.domain
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapLatest
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.data.FavouriteManga
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.domain.LocalObserveMapper
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@Reusable
class LocalFavoritesObserver @Inject constructor(
localMangaRepository: LocalMangaRepository,
localMangaIndex: LocalMangaIndex,
private val db: MangaDatabase,
) : LocalObserveMapper<FavouriteManga, Manga>(localMangaRepository, limitStep = 10) {
) : LocalObserveMapper<FavouriteManga, Manga>(localMangaIndex, limitStep = 10) {
fun observeAll(
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<Manga>> = observe(limit) { newLimit ->
db.getFavouritesDao().observeAll(order, filterOptions, newLimit)
): Flow<List<Manga>> = db.getFavouritesDao().observeAll(order, filterOptions, limit).mapLatest {
it.mapToLocal()
}
fun observeAll(
@@ -32,8 +33,8 @@ class LocalFavoritesObserver @Inject constructor(
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<Manga>> = observe(limit) { newLimit ->
db.getFavouritesDao().observeAll(categoryId, order, filterOptions, newLimit)
): Flow<List<Manga>> = db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit).mapLatest {
it.mapToLocal()
}
override fun toManga(e: FavouriteManga) = e.manga.toManga(e.tags.toMangaTags())

View File

@@ -159,6 +159,7 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
ListFilterOption.Downloaded -> "EXISTS(SELECT * FROM local_index WHERE local_index.manga_id = history.manga_id)"
else -> null
}
}

View File

@@ -1,29 +1,30 @@
package org.koitharu.kotatsu.history.data
import dagger.Reusable
import kotlinx.coroutines.flow.mapLatest
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.domain.LocalObserveMapper
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@Reusable
class HistoryLocalObserver @Inject constructor(
localMangaRepository: LocalMangaRepository,
localMangaIndex: LocalMangaIndex,
private val db: MangaDatabase,
) : LocalObserveMapper<HistoryWithManga, MangaWithHistory>(localMangaRepository, limitStep = 10) {
) : LocalObserveMapper<HistoryWithManga, MangaWithHistory>(localMangaIndex, limitStep = 10) {
fun observeAll(
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
) = observe(limit) { newLimit ->
db.getHistoryDao().observeAll(order, filterOptions, newLimit)
) = db.getHistoryDao().observeAll(order, filterOptions, limit).mapLatest {
it.mapToLocal()
}
override fun toManga(e: HistoryWithManga) = e.manga.toManga(e.tags.toMangaTags())

View File

@@ -82,7 +82,7 @@ class HistoryRepository @Inject constructor(
limit: Int
): Flow<List<MangaWithHistory>> {
if (ListFilterOption.Downloaded in filterOptions) {
return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit)
return localObserver.observeAll(order, filterOptions, limit)
}
return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems {
MangaWithHistory(

View File

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

View File

@@ -3,12 +3,11 @@ package org.koitharu.kotatsu.local.data
import android.net.Uri
import androidx.core.net.toFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.LocalMangaSource
@@ -20,6 +19,7 @@ import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.filterWith
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.data.output.LocalMangaUtil
@@ -43,13 +43,13 @@ private const val MAX_PARALLELISM = 4
@Singleton
class LocalMangaRepository @Inject constructor(
private val storageManager: LocalStorageManager,
private val localMangaIndex: LocalMangaIndex,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
private val settings: AppSettings,
private val lock: MangaLock,
) : MangaRepository {
override val source = LocalMangaSource
private val localMappingCache = LocalMangaMappingCache()
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities(
@@ -116,6 +116,7 @@ class LocalMangaRepository @Inject constructor(
val file = Uri.parse(manga.url).toFile()
val result = file.deleteAwait()
if (result) {
localMangaIndex.delete(manga.id)
localStorageChanges.emit(null)
}
return result
@@ -139,7 +140,7 @@ class LocalMangaRepository @Inject constructor(
suspend fun findSavedManga(remoteManga: Manga): LocalManga? = runCatchingCancellable {
// very fast path
localMappingCache.get(remoteManga.id)?.let {
localMangaIndex.get(remoteManga.id)?.let {
return@runCatchingCancellable it
}
// fast path
@@ -164,7 +165,9 @@ class LocalMangaRepository @Inject constructor(
}
}.firstOrNull()?.getManga()
}.onSuccess { x: LocalManga? ->
localMappingCache[remoteManga.id] = x
if (x != null) {
localMangaIndex.put(x)
}
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
@@ -199,18 +202,21 @@ class LocalMangaRepository @Inject constructor(
return true
}
private suspend fun getRawList(): ArrayList<LocalManga> {
val files = getAllFiles().toList() // TODO remove toList()
return coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
files.map { file ->
async(dispatcher) {
runCatchingCancellable { LocalMangaInput.ofOrNull(file)?.getManga() }.getOrNull()
fun getRawListAsFlow(): Flow<LocalManga> = channelFlow {
val files = getAllFiles()
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
for (file in files) {
launch(dispatcher) {
val m = LocalMangaInput.ofOrNull(file)?.getManga()
if (m != null) {
send(m)
}
}.awaitAll()
}.filterNotNullTo(ArrayList(files.size))
}
}
}
private suspend fun getRawList(): ArrayList<LocalManga> = getRawListAsFlow().toCollection(ArrayList())
private suspend fun getAllFiles() = storageManager.getReadableDirs()
.asSequence()
.flatMap { dir ->

View 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"
}
}

View File

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

View File

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

View File

@@ -1,56 +1,29 @@
package org.koitharu.kotatsu.local.domain
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.transformLatest
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.Collections
import java.util.WeakHashMap
abstract class LocalObserveMapper<E : Any, R : Any>(
private val localMangaRepository: LocalMangaRepository,
private val localMangaIndex: LocalMangaIndex,
private val limitStep: Int,
) {
private val cache = Collections.synchronizedMap(WeakHashMap<Manga, R?>())
protected fun observe(limit: Int, observer: (limit: Int) -> Flow<List<E>>): Flow<List<R>> {
val floatingLimit = MutableStateFlow(limit)
return floatingLimit.flatMapLatest { l ->
observer(l)
.transformLatest { fullList ->
val mapped = fullList.mapToLocal(cache)
if (mapped.size < limit && fullList.size == l) {
floatingLimit.value += limitStep
} else {
emit(mapped.take(limit))
}
}.distinctUntilChanged()
}
}
private suspend fun List<E>.mapToLocal(cache: MutableMap<Manga, R?>): List<R> = coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(8)
protected suspend fun List<E>.mapToLocal(): List<R> = coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(6)
map { item ->
val m = toManga(item)
if (cache.contains(m)) {
CompletableDeferred(cache[m])
} else async(dispatcher) {
async(dispatcher) {
val mapped = if (m.isLocal) {
m
} else {
localMangaRepository.findSavedManga(m)?.manga
localMangaIndex.get(m.id)?.manga
}
mapped?.let { mm -> toResult(item, mm) }.also { cache[m] = it }
mapped?.let { mm -> toResult(item, mm) }
}
}.awaitAll().filterNotNull()
}

View File

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

View File

@@ -55,6 +55,7 @@ import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.favourites.ui.container.FavouritesContainerFragment
import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
@@ -351,6 +352,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
MangaPrefetchService.prefetchLast(this@MainActivity)
requestNotificationsPermission()
}
startService(Intent(this@MainActivity, LocalMangaIndex::class.java))
}
}