Tracker improvements
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user