diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt index ca240adac..5c1e92d63 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/domain/DetailsLoadUseCase.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible import okio.IOException import org.koitharu.kotatsu.core.model.isLocal @@ -17,6 +18,7 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.util.ext.peek +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.sanitize import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase @@ -25,7 +27,9 @@ import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.recoverNotNull import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.tracker.domain.Tracker import javax.inject.Inject +import javax.inject.Provider class DetailsLoadUseCase @Inject constructor( private val mangaDataRepository: MangaDataRepository, @@ -33,6 +37,7 @@ class DetailsLoadUseCase @Inject constructor( private val mangaRepositoryFactory: MangaRepository.Factory, private val recoverUseCase: RecoverMangaUseCase, private val imageGetter: Html.ImageGetter, + private val trackerProvider: Provider, ) { operator fun invoke(intent: MangaIntent): Flow = channelFlow { @@ -49,6 +54,7 @@ class DetailsLoadUseCase @Inject constructor( send(MangaDetails(manga, null, null, false)) try { val details = getDetails(manga) + launch { updateTracker(manga) } send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false)) send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true)) } catch (e: IOException) { @@ -90,4 +96,10 @@ class DetailsLoadUseCase @Inject constructor( } return spannable } + + private suspend fun updateTracker(details: Manga) = runCatchingCancellable { + trackerProvider.get().syncWithDetails(details) + }.onFailure { e -> + e.printStackTraceDebug() + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index 44b61d4c5..15eabac4d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.history.data import androidx.room.withTransaction +import dagger.Lazy import dagger.Reusable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -26,7 +27,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble -import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import org.koitharu.kotatsu.tracker.domain.Tracker import javax.inject.Inject const val PROGRESS_NONE = -1f @@ -34,10 +35,10 @@ const val PROGRESS_NONE = -1f @Reusable class HistoryRepository @Inject constructor( private val db: MangaDatabase, - private val trackingRepository: TrackingRepository, private val settings: AppSettings, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val mangaRepository: MangaDataRepository, + private val trackerLazy: Lazy, ) { suspend fun getList(offset: Int, limit: Int): List { @@ -114,7 +115,7 @@ class HistoryRepository @Inject constructor( deletedAt = 0L, ), ) - trackingRepository.syncWithHistory(manga, chapterId) + trackerLazy.get().syncWithHistory(manga, chapterId) scrobblers.forEach { it.tryScrobble(manga, chapterId) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt index e232b2156..c152253ac 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TrackEntity.kt @@ -33,6 +33,7 @@ class TrackEntity( const val RESULT_HAS_UPDATE = 1 const val RESULT_NO_UPDATE = 2 const val RESULT_FAILED = 3 + const val RESULT_EXTERNAL_MODIFICATION = 4 fun create(mangaId: Long) = TrackEntity( mangaId = mangaId, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt index ae64b77d7..5570c3839 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt @@ -2,11 +2,13 @@ package org.koitharu.kotatsu.tracker.domain import androidx.annotation.VisibleForTesting import coil.request.CachePolicy +import dagger.Reusable import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.CompositeMutex2 +import org.koitharu.kotatsu.core.util.ext.toInstantOrNull import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -14,10 +16,12 @@ import org.koitharu.kotatsu.tracker.domain.model.MangaTracking import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels import org.koitharu.kotatsu.tracker.work.TrackingItem +import java.time.Instant import javax.inject.Inject import kotlin.contracts.InvocationKind import kotlin.contracts.contract +@Reusable class Tracker @Inject constructor( private val settings: AppSettings, private val repository: TrackingRepository, @@ -32,7 +36,7 @@ class Tracker @Inject constructor( val categoryId = repository.getCategoryId(it.manga.id) TrackingItem( tracking = it, - channelId = if (categoryId == 0L) { + channelId = if (categoryId == NO_ID) { channels.getHistoryChannelId() } else { channels.getFavouritesChannelId(categoryId) @@ -66,6 +70,34 @@ class Tracker @Inject constructor( return updates } + suspend fun syncWithDetails(details: Manga) { + requireNotNull(details.chapters) + val track = repository.getTrackOrNull(details) ?: return + val updates = compare(track, details, getBranch(details)) + repository.saveUpdates(updates) + } + + suspend fun syncWithHistory(details: Manga, chapterId: Long) { + val chapters = requireNotNull(details.chapters) + val track = repository.getTrackOrNull(details) ?: return + val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } + val lastNewChapterIndex = chapters.size - track.newChapters + val lastChapter = chapters.lastOrNull() + val tracking = MangaTracking( + manga = details, + lastChapterId = lastChapter?.id ?: NO_ID, + lastCheck = Instant.now(), + lastChapterDate = lastChapter?.uploadDate?.toInstantOrNull() ?: track.lastChapterDate, + newChapters = when { + track.newChapters == 0 -> 0 + chapterIndex < 0 -> track.newChapters + chapterIndex >= lastNewChapterIndex -> chapters.lastIndex - chapterIndex + else -> track.newChapters + }, + ) + repository.mergeWith(tracking) + } + @VisibleForTesting suspend fun checkUpdates(manga: Manga, commit: Boolean): MangaUpdates.Success { val track = repository.getTrack(manga) @@ -118,6 +150,7 @@ class Tracker @Inject constructor( private companion object { + const val NO_ID = 0L private val mangaMutex = CompositeMutex2() suspend inline fun withMangaLock(id: Long, action: () -> T): T { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index 059165b46..3efa1c38e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -31,10 +31,6 @@ import javax.inject.Inject import javax.inject.Provider private const val NO_ID = 0L - -@Deprecated("Use buckets") -private const val MAX_QUERY_IDS = 100 -private const val MAX_BUCKET_SIZE = 20 private const val MAX_LOG_SIZE = 120 @Reusable @@ -99,13 +95,23 @@ class TrackingRepository @Inject constructor( @VisibleForTesting suspend fun getTrack(manga: Manga): MangaTracking { - val track = db.getTracksDao().find(manga.id) + return getTrackOrNull(manga) ?: MangaTracking( + manga = manga, + lastChapterId = NO_ID, + lastCheck = null, + lastChapterDate = null, + newChapters = 0, + ) + } + + suspend fun getTrackOrNull(manga: Manga): MangaTracking? { + val track = db.getTracksDao().find(manga.id) ?: return null return MangaTracking( manga = manga, - lastChapterId = track?.lastChapterId ?: NO_ID, - lastCheck = track?.lastCheckTime?.toInstantOrNull(), - lastChapterDate = track?.lastChapterDate?.toInstantOrNull(), - newChapters = track?.newChapters ?: 0, + lastChapterId = track.lastChapterId, + lastCheck = track.lastCheckTime.toInstantOrNull(), + lastChapterDate = track.lastChapterDate.toInstantOrNull(), + newChapters = track.newChapters, ) } @@ -168,24 +174,14 @@ class TrackingRepository @Inject constructor( } } - suspend fun syncWithHistory(manga: Manga, chapterId: Long) { - val chapters = manga.chapters ?: return - val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId } - val track = getOrCreateTrack(manga.id) - val lastNewChapterIndex = chapters.size - track.newChapters - val lastChapterId = chapters.lastOrNull()?.id ?: NO_ID + suspend fun mergeWith(tracking: MangaTracking) { val entity = TrackEntity( - mangaId = manga.id, - lastChapterId = lastChapterId, - newChapters = when { - track.newChapters == 0 -> 0 - chapterIndex < 0 -> track.newChapters - chapterIndex >= lastNewChapterIndex -> chapters.lastIndex - chapterIndex - else -> track.newChapters - }, - lastCheckTime = System.currentTimeMillis(), - lastChapterDate = maxOf(track.lastChapterDate, chapters.lastOrNull()?.uploadDate ?: 0L), - lastResult = track.lastResult, + mangaId = tracking.manga.id, + lastChapterId = tracking.lastChapterId, + newChapters = tracking.newChapters, + lastCheckTime = tracking.lastCheck?.toEpochMilli() ?: 0L, + lastChapterDate = tracking.lastChapterDate?.toEpochMilli() ?: 0L, + lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION, ) db.getTracksDao().upsert(entity) }