Tracker improvements

This commit is contained in:
Koitharu
2024-04-17 08:53:41 +03:00
parent 846c346a86
commit 7ec2e0c5cc
5 changed files with 73 additions and 30 deletions

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.core.model.isLocal 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.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.peek 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.core.util.ext.sanitize
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase 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.model.Manga
import org.koitharu.kotatsu.parsers.util.recoverNotNull import org.koitharu.kotatsu.parsers.util.recoverNotNull
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.tracker.domain.Tracker
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
class DetailsLoadUseCase @Inject constructor( class DetailsLoadUseCase @Inject constructor(
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
@@ -33,6 +37,7 @@ class DetailsLoadUseCase @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
private val recoverUseCase: RecoverMangaUseCase, private val recoverUseCase: RecoverMangaUseCase,
private val imageGetter: Html.ImageGetter, private val imageGetter: Html.ImageGetter,
private val trackerProvider: Provider<Tracker>,
) { ) {
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow { operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
@@ -49,6 +54,7 @@ class DetailsLoadUseCase @Inject constructor(
send(MangaDetails(manga, null, null, false)) send(MangaDetails(manga, null, null, false))
try { try {
val details = getDetails(manga) val details = getDetails(manga)
launch { updateTracker(manga) }
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false)) send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true)) send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
} catch (e: IOException) { } catch (e: IOException) {
@@ -90,4 +96,10 @@ class DetailsLoadUseCase @Inject constructor(
} }
return spannable return spannable
} }
private suspend fun updateTracker(details: Manga) = runCatchingCancellable {
trackerProvider.get().syncWithDetails(details)
}.onFailure { e ->
e.printStackTraceDebug()
}
} }

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.history.data package org.koitharu.kotatsu.history.data
import androidx.room.withTransaction import androidx.room.withTransaction
import dagger.Lazy
import dagger.Reusable import dagger.Reusable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged 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.parsers.model.MangaTag
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble 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 import javax.inject.Inject
const val PROGRESS_NONE = -1f const val PROGRESS_NONE = -1f
@@ -34,10 +35,10 @@ const val PROGRESS_NONE = -1f
@Reusable @Reusable
class HistoryRepository @Inject constructor( class HistoryRepository @Inject constructor(
private val db: MangaDatabase, private val db: MangaDatabase,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
private val mangaRepository: MangaDataRepository, private val mangaRepository: MangaDataRepository,
private val trackerLazy: Lazy<Tracker>,
) { ) {
suspend fun getList(offset: Int, limit: Int): List<Manga> { suspend fun getList(offset: Int, limit: Int): List<Manga> {
@@ -114,7 +115,7 @@ class HistoryRepository @Inject constructor(
deletedAt = 0L, deletedAt = 0L,
), ),
) )
trackingRepository.syncWithHistory(manga, chapterId) trackerLazy.get().syncWithHistory(manga, chapterId)
scrobblers.forEach { it.tryScrobble(manga, chapterId) } scrobblers.forEach { it.tryScrobble(manga, chapterId) }
} }
} }

View File

@@ -33,6 +33,7 @@ class TrackEntity(
const val RESULT_HAS_UPDATE = 1 const val RESULT_HAS_UPDATE = 1
const val RESULT_NO_UPDATE = 2 const val RESULT_NO_UPDATE = 2
const val RESULT_FAILED = 3 const val RESULT_FAILED = 3
const val RESULT_EXTERNAL_MODIFICATION = 4
fun create(mangaId: Long) = TrackEntity( fun create(mangaId: Long) = TrackEntity(
mangaId = mangaId, mangaId = mangaId,

View File

@@ -2,11 +2,13 @@ package org.koitharu.kotatsu.tracker.domain
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import coil.request.CachePolicy import coil.request.CachePolicy
import dagger.Reusable
import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.CompositeMutex2 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.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable 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.domain.model.MangaUpdates
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
import org.koitharu.kotatsu.tracker.work.TrackingItem import org.koitharu.kotatsu.tracker.work.TrackingItem
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
@Reusable
class Tracker @Inject constructor( class Tracker @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
private val repository: TrackingRepository, private val repository: TrackingRepository,
@@ -32,7 +36,7 @@ class Tracker @Inject constructor(
val categoryId = repository.getCategoryId(it.manga.id) val categoryId = repository.getCategoryId(it.manga.id)
TrackingItem( TrackingItem(
tracking = it, tracking = it,
channelId = if (categoryId == 0L) { channelId = if (categoryId == NO_ID) {
channels.getHistoryChannelId() channels.getHistoryChannelId()
} else { } else {
channels.getFavouritesChannelId(categoryId) channels.getFavouritesChannelId(categoryId)
@@ -66,6 +70,34 @@ class Tracker @Inject constructor(
return updates 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 @VisibleForTesting
suspend fun checkUpdates(manga: Manga, commit: Boolean): MangaUpdates.Success { suspend fun checkUpdates(manga: Manga, commit: Boolean): MangaUpdates.Success {
val track = repository.getTrack(manga) val track = repository.getTrack(manga)
@@ -118,6 +150,7 @@ class Tracker @Inject constructor(
private companion object { private companion object {
const val NO_ID = 0L
private val mangaMutex = CompositeMutex2<Long>() private val mangaMutex = CompositeMutex2<Long>()
suspend inline fun <T> withMangaLock(id: Long, action: () -> T): T { suspend inline fun <T> withMangaLock(id: Long, action: () -> T): T {

View File

@@ -31,10 +31,6 @@ import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
private const val NO_ID = 0L 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 private const val MAX_LOG_SIZE = 120
@Reusable @Reusable
@@ -99,13 +95,23 @@ class TrackingRepository @Inject constructor(
@VisibleForTesting @VisibleForTesting
suspend fun getTrack(manga: Manga): MangaTracking { 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( return MangaTracking(
manga = manga, manga = manga,
lastChapterId = track?.lastChapterId ?: NO_ID, lastChapterId = track.lastChapterId,
lastCheck = track?.lastCheckTime?.toInstantOrNull(), lastCheck = track.lastCheckTime.toInstantOrNull(),
lastChapterDate = track?.lastChapterDate?.toInstantOrNull(), lastChapterDate = track.lastChapterDate.toInstantOrNull(),
newChapters = track?.newChapters ?: 0, newChapters = track.newChapters,
) )
} }
@@ -168,24 +174,14 @@ class TrackingRepository @Inject constructor(
} }
} }
suspend fun syncWithHistory(manga: Manga, chapterId: Long) { suspend fun mergeWith(tracking: MangaTracking) {
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
val entity = TrackEntity( val entity = TrackEntity(
mangaId = manga.id, mangaId = tracking.manga.id,
lastChapterId = lastChapterId, lastChapterId = tracking.lastChapterId,
newChapters = when { newChapters = tracking.newChapters,
track.newChapters == 0 -> 0 lastCheckTime = tracking.lastCheck?.toEpochMilli() ?: 0L,
chapterIndex < 0 -> track.newChapters lastChapterDate = tracking.lastChapterDate?.toEpochMilli() ?: 0L,
chapterIndex >= lastNewChapterIndex -> chapters.lastIndex - chapterIndex lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
else -> track.newChapters
},
lastCheckTime = System.currentTimeMillis(),
lastChapterDate = maxOf(track.lastChapterDate, chapters.lastOrNull()?.uploadDate ?: 0L),
lastResult = track.lastResult,
) )
db.getTracksDao().upsert(entity) db.getTracksDao().upsert(entity)
} }