Refactor chapter tracking logic

Could probably call this if we ever make it update tracking on manually
marking chapters as read.
This commit is contained in:
arkon 2023-07-10 17:13:58 -04:00
parent 9a817e49be
commit efabe801be
3 changed files with 69 additions and 60 deletions

View File

@ -16,6 +16,7 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.track.interactor.TrackChapter
import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl import tachiyomi.data.chapter.ChapterRepositoryImpl
import tachiyomi.data.history.HistoryRepositoryImpl import tachiyomi.data.history.HistoryRepositoryImpl
@ -109,6 +110,7 @@ class DomainModule : InjektModule {
addFactory { GetApplicationRelease(get(), get()) } addFactory { GetApplicationRelease(get(), get()) }
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { TrackChapter(get(), get(), get(), get()) }
addFactory { DeleteTrack(get()) } addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) } addFactory { GetTracksPerManga(get()) }
addFactory { GetTracks(get()) } addFactory { GetTracks(get()) }

View File

@ -0,0 +1,56 @@
package eu.kanade.domain.track.interactor
import android.content.Context
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.data.track.TrackManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import logcat.LogPriority
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
class TrackChapter(
private val getTracks: GetTracks,
private val trackManager: TrackManager,
private val insertTrack: InsertTrack,
private val delayedTrackingStore: DelayedTrackingStore,
) {
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) = coroutineScope {
launchNonCancellable {
val tracks = getTracks.await(mangaId)
if (tracks.isEmpty()) return@launchNonCancellable
tracks.mapNotNull { track ->
val service = trackManager.getService(track.syncId)
if (service != null && service.isLogged && chapterNumber > track.lastChapterRead) {
val updatedTrack = track.copy(lastChapterRead = chapterNumber)
async {
runCatching {
try {
service.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
} catch (e: Exception) {
delayedTrackingStore.addItem(updatedTrack)
DelayedTrackingUpdateJob.setupTask(context)
throw e
}
}
}
} else {
null
}
}
.awaitAll()
.mapNotNull { it.exceptionOrNull() }
.forEach { logcat(LogPriority.INFO, it) }
}
}
}

View File

@ -10,10 +10,8 @@ import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.model.orientationType import eu.kanade.domain.manga.model.orientationType
import eu.kanade.domain.manga.model.readingModeType import eu.kanade.domain.manga.model.readingModeType
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.interactor.TrackChapter
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.data.database.models.toDomainChapter import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.data.download.DownloadProvider
@ -21,7 +19,6 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.saver.Image import eu.kanade.tachiyomi.data.saver.Image
import eu.kanade.tachiyomi.data.saver.ImageSaver import eu.kanade.tachiyomi.data.saver.ImageSaver
import eu.kanade.tachiyomi.data.saver.Location import eu.kanade.tachiyomi.data.saver.Location
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
@ -42,11 +39,8 @@ import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.takeBytes import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.system.isOnline
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -75,8 +69,6 @@ import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -96,12 +88,10 @@ class ReaderViewModel(
private val basePreferences: BasePreferences = Injekt.get(), private val basePreferences: BasePreferences = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val trackPreferences: TrackPreferences = Injekt.get(), private val trackPreferences: TrackPreferences = Injekt.get(),
private val delayedTrackingStore: DelayedTrackingStore = Injekt.get(), private val trackChapter: TrackChapter = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(), private val getNextChapters: GetNextChapters = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(),
private val upsertHistory: UpsertHistory = Injekt.get(), private val upsertHistory: UpsertHistory = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(),
private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(), private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(),
@ -197,12 +187,6 @@ class ReaderViewModel(
.map(::ReaderChapter) .map(::ReaderChapter)
} }
private var hasTrackers: Boolean = false
private val checkTrackers: (Manga) -> Unit = { manga ->
val tracks = runBlocking { getTracks.await(manga.id) }
hasTrackers = tracks.isNotEmpty()
}
private val incognitoMode = preferences.incognitoMode().get() private val incognitoMode = preferences.incognitoMode().get()
private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading().get() private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading().get()
@ -258,8 +242,6 @@ class ReaderViewModel(
mutableState.update { it.copy(manga = manga) } mutableState.update { it.copy(manga = manga) }
if (chapterId == -1L) chapterId = initialChapterId if (chapterId == -1L) chapterId = initialChapterId
checkTrackers(manga)
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source) val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
@ -419,7 +401,7 @@ class ReaderViewModel(
// Save last page read and mark as read if needed // Save last page read and mark as read if needed
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
updateChapterProgress(page.index) updateChapterProgress(selectedChapter, page.index)
} }
if (selectedChapter != getCurrentChapter()) { if (selectedChapter != getCurrentChapter()) {
@ -499,9 +481,7 @@ class ReaderViewModel(
* Saves the chapter progress (last read page and whether it's read) * Saves the chapter progress (last read page and whether it's read)
* if incognito mode isn't on. * if incognito mode isn't on.
*/ */
private suspend fun updateChapterProgress(pageIndex: Int) { private suspend fun updateChapterProgress(readerChapter: ReaderChapter, pageIndex: Int) {
val readerChapter = getCurrentChapter() ?: return
mutableState.update { mutableState.update {
it.copy(currentPage = pageIndex + 1) it.copy(currentPage = pageIndex + 1)
} }
@ -532,18 +512,19 @@ class ReaderViewModel(
} }
fun flushReadTimer() { fun flushReadTimer() {
getCurrentChapter()?.let {
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
updateHistory() updateHistory(it)
}
} }
} }
/** /**
* Saves the chapter last read history if incognito mode isn't on. * Saves the chapter last read history if incognito mode isn't on.
*/ */
private suspend fun updateHistory() { private suspend fun updateHistory(readerChapter: ReaderChapter) {
if (incognitoMode) return if (incognitoMode) return
val readerChapter = getCurrentChapter() ?: return
val chapterId = readerChapter.chapter.id!! val chapterId = readerChapter.chapter.id!!
val endTime = Date() val endTime = Date()
val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0 val sessionReadDuration = chapterReadStartTime?.let { endTime.time - it } ?: 0
@ -829,44 +810,14 @@ class ReaderViewModel(
* will run in a background thread and errors are ignored. * will run in a background thread and errors are ignored.
*/ */
private fun updateTrackChapterRead(readerChapter: ReaderChapter) { private fun updateTrackChapterRead(readerChapter: ReaderChapter) {
if (incognitoMode || !hasTrackers) return if (incognitoMode) return
if (!trackPreferences.autoUpdateTrack().get()) return if (!trackPreferences.autoUpdateTrack().get()) return
val manga = manga ?: return val manga = manga ?: return
val chapterRead = readerChapter.chapter.chapter_number.toDouble()
val trackManager = Injekt.get<TrackManager>()
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
getTracks.await(manga.id) trackChapter.await(context, manga.id, readerChapter.chapter.chapter_number.toDouble())
.mapNotNull { track ->
val service = trackManager.getService(track.syncId)
if (service != null && service.isLogged && chapterRead > track.lastChapterRead) {
val updatedTrack = track.copy(lastChapterRead = chapterRead)
// We want these to execute even if the presenter is destroyed and leaks
// for a while. The view can still be garbage collected.
async {
runCatching {
try {
if (!context.isOnline()) error("Couldn't update tracker as device is offline")
service.update(updatedTrack.toDbTrack(), true)
insertTrack.await(updatedTrack)
} catch (e: Exception) {
delayedTrackingStore.addItem(updatedTrack)
DelayedTrackingUpdateJob.setupTask(context)
throw e
}
}
}
} else {
null
}
}
.awaitAll()
.mapNotNull { it.exceptionOrNull() }
.forEach { logcat(LogPriority.INFO, it) }
} }
} }