@ -1,7 +1,6 @@
package org.koitharu.kotatsu.tracker.domain
import androidx.annotation.VisibleForTesting
import androidx.collection.MutableLongSet
import androidx.room.withTransaction
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
@ -14,20 +13,19 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core. model.isLocal
import org.koitharu.kotatsu.core. prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@ -42,6 +40,7 @@ private const val MAX_LOG_SIZE = 120
@Reusable
class TrackingRepository @Inject constructor (
private val db : MangaDatabase ,
private val settings : AppSettings ,
private val localMangaRepositoryProvider : Provider < LocalMangaRepository > ,
) {
@ -70,36 +69,18 @@ class TrackingRepository @Inject constructor(
. onStart { gcIfNotCalled ( ) }
}
@Deprecated ( " " )
suspend fun getTracks ( mangaList : Collection < Manga > ) : List < MangaTracking > {
val ids = mangaList . mapToSet { it . id }
val dao = db . getTracksDao ( )
val tracks = if ( ids . size <= MAX _QUERY _IDS ) {
dao . findAll ( ids )
} else {
// TODO split tracks in the worker
ids . windowed ( MAX _QUERY _IDS , MAX _QUERY _IDS , true )
. flatMap { dao . findAll ( it ) }
} . groupBy { it . mangaId }
val idSet = MutableLongSet ( mangaList . size )
val result = ArrayList < MangaTracking > ( mangaList . size )
for ( item in mangaList ) {
val manga = if ( item . isLocal ) {
localMangaRepositoryProvider . get ( ) . getRemoteManga ( item ) ?: continue
} else {
item
}
if ( ! idSet . add ( manga . id ) ) {
continue
}
val track = tracks [ manga . id ] ?. lastOrNull ( )
result += MangaTracking (
manga = manga ,
lastChapterId = track ?. lastChapterId ?: NO _ID ,
lastCheck = track ?. lastCheckTime ?. takeUnless { it == 0L } ?. let ( Instant :: ofEpochMilli ) ,
suspend fun getCategoryId ( mangaId : Long ) : Long {
return db . getFavouritesDao ( ) . findCategoriesIdsWithTrack ( mangaId ) . firstOrNull ( ) ?: NO _ID
}
suspend fun getTracks ( offset : Int , limit : Int ) : List < MangaTracking > {
return db . getTracksDao ( ) . findAll ( offset , limit ) . map {
MangaTracking (
manga = it . manga . toManga ( emptySet ( ) ) ,
lastChapterId = it . track . lastChapterId ,
lastCheck = it . track . lastCheckTime . toInstantOrNull ( ) ,
)
}
return result
}
@VisibleForTesting
@ -108,7 +89,7 @@ class TrackingRepository @Inject constructor(
return MangaTracking (
manga = manga ,
lastChapterId = track ?. lastChapterId ?: NO _ID ,
lastCheck = track ?. lastCheckTime ?. t akeUnless { it == 0L } ?. let ( Instant :: ofEpochMilli ) ,
lastCheck = track ?. lastCheckTime ?. t oInstantOrNull( ) ,
)
}
@ -145,11 +126,11 @@ class TrackingRepository @Inject constructor(
}
}
suspend fun saveUpdates ( updates : MangaUpdates . Succes s) {
suspend fun saveUpdates ( updates : MangaUpdate s) {
db . withTransaction {
val track = getOrCreateTrack ( updates . manga . id ) . mergeWith ( updates )
db . getTracksDao ( ) . upsert ( track )
if ( updates . isValid && updates . newChapters . isNotEmpty ( ) ) {
if ( updates is MangaUpdates . Success && updates . isValid && updates . newChapters . isNotEmpty ( ) ) {
updatePercent ( updates )
val logEntity = TrackLogEntity (
mangaId = updates . manga . id ,
@ -211,15 +192,38 @@ class TrackingRepository @Inject constructor(
}
}
suspend fun updateTracks ( ) = db . withTransaction {
val dao = db . getTracksDao ( )
dao . gc ( )
val ids = dao . findAllIds ( ) . toMutableSet ( )
val size = ids . size
// history
if ( AppSettings . TRACK _HISTORY in settings . trackSources ) {
val historyIds = db . getHistoryDao ( ) . findAllIds ( )
for ( mangaId in historyIds ) {
if ( ! ids . remove ( mangaId ) ) {
dao . upsert ( TrackEntity . create ( mangaId ) )
}
}
}
// favorites
if ( AppSettings . TRACK _FAVOURITES in settings . trackSources ) {
val favoritesIds = db . getFavouritesDao ( ) . findIdsWithTrack ( )
for ( mangaId in favoritesIds ) {
if ( ! ids . remove ( mangaId ) ) {
dao . upsert ( TrackEntity . create ( mangaId ) )
}
}
}
// remove unused
for ( mangaId in ids ) {
dao . delete ( mangaId )
}
size - ids . size
}
private suspend fun getOrCreateTrack ( mangaId : Long ) : TrackEntity {
return db . getTracksDao ( ) . find ( mangaId ) ?: TrackEntity (
mangaId = mangaId ,
lastChapterId = 0L ,
newChapters = 0 ,
lastCheckTime = 0L ,
lastChapterDate = 0 ,
lastResult = TrackEntity . RESULT _NONE ,
)
return db . getTracksDao ( ) . find ( mangaId ) ?: TrackEntity . create ( mangaId )
}
private suspend fun updatePercent ( updates : MangaUpdates . Success ) {
@ -237,16 +241,27 @@ class TrackingRepository @Inject constructor(
db . getHistoryDao ( ) . update ( history . copy ( percent = newPercent ) )
}
private fun TrackEntity . mergeWith ( updates : MangaUpdates . Succes s) : TrackEntity {
private fun TrackEntity . mergeWith ( updates : MangaUpdate s) : TrackEntity {
val chapters = updates . manga . chapters . orEmpty ( )
return TrackEntity (
mangaId = mangaId ,
lastChapterId = chapters . lastOrNull ( ) ?. id ?: NO _ID ,
newChapters = if ( updates . isValid ) newChapters + updates . newChapters . size else 0 ,
lastCheckTime = System . currentTimeMillis ( ) ,
lastChapterDate = updates . lastChapterDate ( ) . ifZero { lastChapterDate } ,
lastResult = if ( updates . isNotEmpty ( ) ) TrackEntity . RESULT _HAS _UPDATE else TrackEntity . RESULT _NO _UPDATE ,
)
return when ( updates ) {
is MangaUpdates . Failure -> TrackEntity (
mangaId = mangaId ,
lastChapterId = lastChapterId ,
newChapters = newChapters ,
lastCheckTime = System . currentTimeMillis ( ) ,
lastChapterDate = lastChapterDate ,
lastResult = TrackEntity . RESULT _FAILED ,
)
is MangaUpdates . Success -> TrackEntity (
mangaId = mangaId ,
lastChapterId = chapters . lastOrNull ( ) ?. id ?: NO _ID ,
newChapters = if ( updates . isValid ) newChapters + updates . newChapters . size else 0 ,
lastCheckTime = System . currentTimeMillis ( ) ,
lastChapterDate = updates . lastChapterDate ( ) . ifZero { lastChapterDate } ,
lastResult = if ( updates . isNotEmpty ( ) ) TrackEntity . RESULT _HAS _UPDATE else TrackEntity . RESULT _NO _UPDATE ,
)
}
}
private suspend fun gcIfNotCalled ( ) {