@ -1,7 +1,6 @@
package org.xtimms.shirizu.data.repository
package org.xtimms.shirizu.data.repository
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.ExperimentalCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -12,7 +11,6 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.xtimms.shirizu.core.database.ShirizuDatabase
import org.xtimms.shirizu.core.database.ShirizuDatabase
import org.xtimms.shirizu.core.database.entity.MangaEntity
import org.xtimms.shirizu.core.database.entity.MangaEntity
import org.xtimms.shirizu.core.database.entity.TrackEntity
import org.xtimms.shirizu.core.database.entity.TrackEntity
@ -21,19 +19,23 @@ import org.xtimms.shirizu.core.database.entity.toFavouriteCategory
import org.xtimms.shirizu.core.database.entity.toManga
import org.xtimms.shirizu.core.database.entity.toManga
import org.xtimms.shirizu.core.database.entity.toTrackingLogItem
import org.xtimms.shirizu.core.database.entity.toTrackingLogItem
import org.xtimms.shirizu.core.model.FavouriteCategory
import org.xtimms.shirizu.core.model.FavouriteCategory
import org.xtimms.shirizu.core.model.isLocal
import org.xtimms.shirizu.core.parser.local.LocalMangaRepository
import org.xtimms.shirizu.core.parser.local.LocalMangaRepository
import org.xtimms.shirizu.core.tracker.model.MangaTracking
import org.xtimms.shirizu.core.tracker.model.MangaTracking
import org.xtimms.shirizu.core.tracker.model.MangaUpdates
import org.xtimms.shirizu.core.tracker.model.MangaUpdates
import org.xtimms.shirizu.core.tracker.model.TrackingLogItem
import org.xtimms.shirizu.core.tracker.model.TrackingLogItem
import org.xtimms.shirizu.utils.lang.ifZero
import org.xtimms.shirizu.utils.lang.mapItems
import org.xtimms.shirizu.utils.lang.mapItems
import java.time.Instant
import org.xtimms.shirizu.utils.lang.toInstantOrNull
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
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 _QUERY _IDS = 100
private const val MAX _BUCKET _SIZE = 20
private const val MAX _LOG _SIZE = 120
@Reusable
@Reusable
class TrackingRepository @Inject constructor (
class TrackingRepository @Inject constructor (
@ -66,35 +68,18 @@ class TrackingRepository @Inject constructor(
. onStart { gcIfNotCalled ( ) }
. onStart { gcIfNotCalled ( ) }
}
}
suspend fun getTracks ( mangaList : Collection < Manga > ) : List < MangaTracking > {
suspend fun getCategoryId ( mangaId : Long ) : Long {
val ids = mangaList . mapToSet { it . id }
return db . getFavouritesDao ( ) . findCategoriesIdsWithTrack ( mangaId ) . firstOrNull ( ) ?: NO _ID
val dao = db . getTracksDao ( )
}
val tracks = if ( ids . size <= MAX _QUERY _IDS ) {
dao . findAll ( ids )
suspend fun getTracks ( offset : Int , limit : Int ) : List < MangaTracking > {
} else {
return db . getTracksDao ( ) . findAll ( offset , limit ) . map {
// TODO split tracks in the worker
MangaTracking (
ids . windowed ( MAX _QUERY _IDS , MAX _QUERY _IDS , true )
manga = it . manga . toManga ( emptySet ( ) ) ,
. flatMap { dao . findAll ( it ) }
lastChapterId = it . track . lastChapterId ,
} . groupBy { it . mangaId }
lastCheck = it . track . lastCheckTime . toInstantOrNull ( ) ,
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 ?. lastCheck ?. takeUnless { it == 0L } ?. let ( Instant :: ofEpochMilli ) ,
)
)
}
}
return result
}
}
@VisibleForTesting
@VisibleForTesting
@ -103,7 +88,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 ?. lastCheck ?. takeUnless { it == 0L } ?. let ( Instant :: ofEpochMilli ) ,
lastCheck = track ?. lastCheck Time?. toInstantOrNull ( ) ,
)
)
}
}
@ -133,16 +118,19 @@ class TrackingRepository @Inject constructor(
suspend fun clearCounters ( ) = db . getTracksDao ( ) . clearCounters ( )
suspend fun clearCounters ( ) = db . getTracksDao ( ) . clearCounters ( )
suspend fun gc ( ) {
suspend fun gc ( ) = db . withTransaction {
db . getTracksDao ( ) . gc ( )
db . getTracksDao ( ) . gc ( )
db . getTrackLogsDao ( ) . gc ( )
db . getTrackLogsDao ( ) . run {
gc ( )
trim ( MAX _LOG _SIZE )
}
}
}
suspend fun saveUpdates ( updates : MangaUpdates . Success ) {
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 ,
@ -174,7 +162,6 @@ class TrackingRepository @Inject constructor(
val lastChapterId = chapters . lastOrNull ( ) ?. id ?: NO _ID
val lastChapterId = chapters . lastOrNull ( ) ?. id ?: NO _ID
val entity = TrackEntity (
val entity = TrackEntity (
mangaId = manga . id ,
mangaId = manga . id ,
totalChapters = chapters . size ,
lastChapterId = lastChapterId ,
lastChapterId = lastChapterId ,
newChapters = when {
newChapters = when {
track . newChapters == 0 -> 0
track . newChapters == 0 -> 0
@ -182,8 +169,9 @@ class TrackingRepository @Inject constructor(
chapterIndex >= lastNewChapterIndex -> chapters . lastIndex - chapterIndex
chapterIndex >= lastNewChapterIndex -> chapters . lastIndex - chapterIndex
else -> track . newChapters
else -> track . newChapters
} ,
} ,
lastCheck = System . currentTimeMillis ( ) ,
lastCheckTime = System . currentTimeMillis ( ) ,
lastNotifiedChapterId = lastChapterId ,
lastChapterDate = maxOf ( track . lastChapterDate , chapters . lastOrNull ( ) ?. uploadDate ?: 0L ) ,
lastResult = track . lastResult ,
)
)
db . getTracksDao ( ) . upsert ( entity )
db . getTracksDao ( ) . upsert ( entity )
}
}
@ -204,19 +192,36 @@ class TrackingRepository @Inject constructor(
}
}
}
}
suspend fun getAllHistoryManga ( ) : List < Manga > {
suspend fun updateTracks ( ) = db . withTransaction {
return db . getHistoryDao ( ) . findAllManga ( ) . toMangaList ( )
val dao = db . getTracksDao ( )
dao . gc ( )
val ids = dao . findAllIds ( ) . toMutableSet ( )
val size = ids . size
// history
val historyIds = db . getHistoryDao ( ) . findAllIds ( )
for ( mangaId in historyIds ) {
if ( ! ids . remove ( mangaId ) ) {
dao . upsert ( TrackEntity . create ( mangaId ) )
}
}
// favourites
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 ,
totalChapters = 0 ,
lastChapterId = 0L ,
newChapters = 0 ,
lastCheck = 0L ,
lastNotifiedChapterId = 0L ,
)
}
}
private suspend fun updatePercent ( updates : MangaUpdates . Success ) {
private suspend fun updatePercent ( updates : MangaUpdates . Success ) {
@ -234,16 +239,27 @@ 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 ) {
mangaId = mangaId ,
is MangaUpdates . Failure -> TrackEntity (
totalChapters = chapters . size ,
mangaId = mangaId ,
lastChapterId = chapters . lastOrNull ( ) ?. id ?: NO _ID ,
lastChapterId = lastChapterId ,
newChapters = if ( updates . isValid ) newChapters + updates . newChapters . size else 0 ,
newChapters = newChapters ,
lastCheck = System . currentTimeMillis ( ) ,
lastCheckTime = System . currentTimeMillis ( ) ,
lastNotifiedChapterId = NO _ID ,
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 ( ) {
private suspend fun gcIfNotCalled ( ) {