@ -1,7 +1,6 @@
package org.koitharu.kotatsu.tracker.domain
package org.koitharu.kotatsu.tracker.domain
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting
import androidx.collection.MutableLongSet
import androidx.room.withTransaction
import androidx.room.withTransaction
import dagger.Reusable
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
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.MangaEntity
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.model.FavouriteCategory
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.ifZero
import org.koitharu.kotatsu.core.util.ext.mapItems
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.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
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.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
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.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Provider
@ -42,6 +40,7 @@ private const val MAX_LOG_SIZE = 120
@Reusable
@Reusable
class TrackingRepository @Inject constructor (
class TrackingRepository @Inject constructor (
private val db : MangaDatabase ,
private val db : MangaDatabase ,
private val settings : AppSettings ,
private val localMangaRepositoryProvider : Provider < LocalMangaRepository > ,
private val localMangaRepositoryProvider : Provider < LocalMangaRepository > ,
) {
) {
@ -70,36 +69,18 @@ class TrackingRepository @Inject constructor(
. onStart { gcIfNotCalled ( ) }
. onStart { gcIfNotCalled ( ) }
}
}
@Deprecated ( " " )
suspend fun getCategoryId ( mangaId : Long ) : Long {
suspend fun getTracks ( mangaList : Collection < Manga > ) : List < MangaTracking > {
return db . getFavouritesDao ( ) . findCategoriesIdsWithTrack ( mangaId ) . firstOrNull ( ) ?: NO _ID
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 (
suspend fun getTracks ( offset : Int , limit : Int ) : List < MangaTracking > {
manga = manga ,
return db . getTracksDao ( ) . findAll ( offset , limit ) . map {
lastChapterId = track ?. lastChapterId ?: NO _ID ,
MangaTracking (
lastCheck = track ?. lastCheckTime ?. takeUnless { it == 0L } ?. let ( Instant :: ofEpochMilli ) ,
manga = it . manga . toManga ( emptySet ( ) ) ,
lastChapterId = it . track . lastChapterId ,
lastCheck = it . track . lastCheckTime . toInstantOrNull ( ) ,
)
)
}
}
return result
}
}
@VisibleForTesting
@VisibleForTesting
@ -108,7 +89,7 @@ class TrackingRepository @Inject constructor(
return MangaTracking (
return MangaTracking (
manga = manga ,
manga = manga ,
lastChapterId = track ?. lastChapterId ?: NO _ID ,
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 {
db . withTransaction {
val track = getOrCreateTrack ( updates . manga . id ) . mergeWith ( updates )
val track = getOrCreateTrack ( updates . manga . id ) . mergeWith ( updates )
db . getTracksDao ( ) . upsert ( track )
db . getTracksDao ( ) . upsert ( track )
if ( updates . isValid && updates . newChapters . isNotEmpty ( ) ) {
if ( updates is MangaUpdates . Success && updates . isValid && updates . newChapters . isNotEmpty ( ) ) {
updatePercent ( updates )
updatePercent ( updates )
val logEntity = TrackLogEntity (
val logEntity = TrackLogEntity (
mangaId = updates . manga . id ,
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 {
private suspend fun getOrCreateTrack ( mangaId : Long ) : TrackEntity {
return db . getTracksDao ( ) . find ( mangaId ) ?: TrackEntity (
return db . getTracksDao ( ) . find ( mangaId ) ?: TrackEntity . create ( mangaId )
mangaId = mangaId ,
lastChapterId = 0L ,
newChapters = 0 ,
lastCheckTime = 0L ,
lastChapterDate = 0 ,
lastResult = TrackEntity . RESULT _NONE ,
)
}
}
private suspend fun updatePercent ( updates : MangaUpdates . Success ) {
private suspend fun updatePercent ( updates : MangaUpdates . Success ) {
@ -237,9 +241,19 @@ class TrackingRepository @Inject constructor(
db . getHistoryDao ( ) . update ( history . copy ( percent = newPercent ) )
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 ( )
val chapters = updates . manga . chapters . orEmpty ( )
return TrackEntity (
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 ,
mangaId = mangaId ,
lastChapterId = chapters . lastOrNull ( ) ?. id ?: NO _ID ,
lastChapterId = chapters . lastOrNull ( ) ?. id ?: NO _ID ,
newChapters = if ( updates . isValid ) newChapters + updates . newChapters . size else 0 ,
newChapters = if ( updates . isValid ) newChapters + updates . newChapters . size else 0 ,
@ -248,6 +262,7 @@ class TrackingRepository @Inject constructor(
lastResult = if ( updates . isNotEmpty ( ) ) TrackEntity . RESULT _HAS _UPDATE else TrackEntity . RESULT _NO _UPDATE ,
lastResult = if ( updates . isNotEmpty ( ) ) TrackEntity . RESULT _HAS _UPDATE else TrackEntity . RESULT _NO _UPDATE ,
)
)
}
}
}
private suspend fun gcIfNotCalled ( ) {
private suspend fun gcIfNotCalled ( ) {
if ( isGcCalled . compareAndSet ( false , true ) ) {
if ( isGcCalled . compareAndSet ( false , true ) ) {