285 lines
11 KiB
Kotlin
285 lines
11 KiB
Kotlin
package exh.eh
|
|
|
|
import android.content.Context
|
|
import androidx.work.Constraints
|
|
import androidx.work.CoroutineWorker
|
|
import androidx.work.ExistingPeriodicWorkPolicy
|
|
import androidx.work.NetworkType
|
|
import androidx.work.OneTimeWorkRequestBuilder
|
|
import androidx.work.PeriodicWorkRequestBuilder
|
|
import androidx.work.WorkManager
|
|
import androidx.work.WorkerParameters
|
|
import com.elvishew.xlog.Logger
|
|
import com.elvishew.xlog.XLog
|
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
|
import eu.kanade.domain.manga.model.toSManga
|
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
|
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
|
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
|
import exh.debug.DebugToggles
|
|
import exh.eh.EHentaiUpdateWorkerConstants.UPDATES_PER_ITERATION
|
|
import exh.log.xLog
|
|
import exh.metadata.metadata.EHentaiSearchMetadata
|
|
import exh.util.cancellable
|
|
import kotlinx.coroutines.flow.asFlow
|
|
import kotlinx.coroutines.flow.mapNotNull
|
|
import kotlinx.coroutines.flow.toList
|
|
import kotlinx.serialization.encodeToString
|
|
import kotlinx.serialization.json.Json
|
|
import tachiyomi.domain.UnsortedPreferences
|
|
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
|
|
import tachiyomi.domain.chapter.model.Chapter
|
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_CHARGING
|
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY_ON_WIFI
|
|
import tachiyomi.domain.manga.interactor.GetExhFavoriteMangaWithMetadata
|
|
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
|
|
import tachiyomi.domain.manga.interactor.InsertFlatMetadata
|
|
import tachiyomi.domain.manga.model.Manga
|
|
import tachiyomi.domain.source.service.SourceManager
|
|
import uy.kohesive.injekt.Injekt
|
|
import uy.kohesive.injekt.api.get
|
|
import uy.kohesive.injekt.injectLazy
|
|
import java.util.concurrent.TimeUnit
|
|
import kotlin.time.Duration.Companion.days
|
|
|
|
class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerParameters) :
|
|
CoroutineWorker(context, workerParams) {
|
|
private val preferences: UnsortedPreferences by injectLazy()
|
|
private val sourceManager: SourceManager by injectLazy()
|
|
private val updateHelper: EHentaiUpdateHelper by injectLazy()
|
|
private val logger: Logger = xLog()
|
|
private val updateManga: UpdateManga by injectLazy()
|
|
private val syncChaptersWithSource: SyncChaptersWithSource by injectLazy()
|
|
private val getChapterByMangaId: GetChapterByMangaId by injectLazy()
|
|
private val getFlatMetadataById: GetFlatMetadataById by injectLazy()
|
|
private val insertFlatMetadata: InsertFlatMetadata by injectLazy()
|
|
private val getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata by injectLazy()
|
|
|
|
private val updateNotifier by lazy { LibraryUpdateNotifier(context) }
|
|
|
|
override suspend fun doWork(): Result {
|
|
return try {
|
|
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
|
Result.failure()
|
|
} else {
|
|
startUpdating()
|
|
logger.d("Update job completed!")
|
|
Result.success()
|
|
}
|
|
} catch (e: Exception) {
|
|
Result.failure()
|
|
}
|
|
}
|
|
|
|
private suspend fun startUpdating() {
|
|
logger.d("Update job started!")
|
|
val startTime = System.currentTimeMillis()
|
|
|
|
logger.d("Finding manga with metadata...")
|
|
val metadataManga = getExhFavoriteMangaWithMetadata.await()
|
|
|
|
logger.d("Filtering manga and raising metadata...")
|
|
val curTime = System.currentTimeMillis()
|
|
val allMeta = metadataManga.asFlow().cancellable().mapNotNull { manga ->
|
|
val meta = getFlatMetadataById.await(manga.id)
|
|
?: return@mapNotNull null
|
|
|
|
val raisedMeta = meta.raise<EHentaiSearchMetadata>()
|
|
|
|
// Don't update galleries too frequently
|
|
if (raisedMeta.aged || (curTime - raisedMeta.lastUpdateCheck < MIN_BACKGROUND_UPDATE_FREQ && DebugToggles.RESTRICT_EXH_GALLERY_UPDATE_CHECK_FREQUENCY.enabled)) {
|
|
return@mapNotNull null
|
|
}
|
|
|
|
val chapter = getChapterByMangaId.await(manga.id).minByOrNull {
|
|
it.dateUpload
|
|
}
|
|
|
|
UpdateEntry(manga, raisedMeta, chapter)
|
|
}.toList().sortedBy { it.meta.lastUpdateCheck }
|
|
|
|
logger.d("Found %s manga to update, starting updates!", allMeta.size)
|
|
val mangaMetaToUpdateThisIter = allMeta.take(UPDATES_PER_ITERATION)
|
|
|
|
var failuresThisIteration = 0
|
|
var updatedThisIteration = 0
|
|
val updatedManga = mutableListOf<Pair<Manga, Array<Chapter>>>()
|
|
val modifiedThisIteration = mutableSetOf<Long>()
|
|
|
|
try {
|
|
for ((index, entry) in mangaMetaToUpdateThisIter.withIndex()) {
|
|
val (manga, meta) = entry
|
|
if (failuresThisIteration > MAX_UPDATE_FAILURES) {
|
|
logger.w("Too many update failures, aborting...")
|
|
break
|
|
}
|
|
|
|
logger.d(
|
|
"Updating gallery (index: %s, manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s, modifiedThisIteration.size: %s)...",
|
|
index,
|
|
manga.id,
|
|
meta.gId,
|
|
meta.gToken,
|
|
failuresThisIteration,
|
|
modifiedThisIteration.size,
|
|
)
|
|
|
|
if (manga.id in modifiedThisIteration) {
|
|
// We already processed this manga!
|
|
logger.w("Gallery already updated this iteration, skipping...")
|
|
updatedThisIteration++
|
|
continue
|
|
}
|
|
|
|
val (new, chapters) = try {
|
|
updateEntryAndGetChapters(manga)
|
|
} catch (e: GalleryNotUpdatedException) {
|
|
if (e.network) {
|
|
failuresThisIteration++
|
|
|
|
logger.e("> Network error while updating gallery!", e)
|
|
logger.e(
|
|
"> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)",
|
|
manga.id,
|
|
meta.gId,
|
|
meta.gToken,
|
|
failuresThisIteration,
|
|
)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if (chapters.isEmpty()) {
|
|
logger.e(
|
|
"No chapters found for gallery (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)!",
|
|
manga.id,
|
|
meta.gId,
|
|
meta.gToken,
|
|
failuresThisIteration,
|
|
)
|
|
|
|
continue
|
|
}
|
|
|
|
// Find accepted root and discard others
|
|
val (acceptedRoot, discardedRoots, hasNew) =
|
|
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters)
|
|
|
|
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
|
|
(hasNew && updatedManga.none { it.first.id == acceptedRoot.manga.id })
|
|
) {
|
|
updatedManga += acceptedRoot.manga to new.toTypedArray()
|
|
}
|
|
|
|
modifiedThisIteration += acceptedRoot.manga.id
|
|
modifiedThisIteration += discardedRoots.map { it.manga.id }
|
|
updatedThisIteration++
|
|
}
|
|
} finally {
|
|
preferences.exhAutoUpdateStats().set(
|
|
Json.encodeToString(
|
|
EHentaiUpdaterStats(
|
|
startTime,
|
|
allMeta.size,
|
|
updatedThisIteration,
|
|
),
|
|
),
|
|
)
|
|
|
|
if (updatedManga.isNotEmpty()) {
|
|
updateNotifier.showUpdateNotifications(updatedManga)
|
|
}
|
|
}
|
|
}
|
|
|
|
// New, current
|
|
private suspend fun updateEntryAndGetChapters(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
|
val source = sourceManager.get(manga.source) as? EHentai
|
|
?: throw GalleryNotUpdatedException(false, IllegalStateException("Missing EH-based source (${manga.source})!"))
|
|
|
|
try {
|
|
val updatedManga = source.getMangaDetails(manga.toSManga())
|
|
updateManga.awaitUpdateFromSource(manga, updatedManga, false)
|
|
|
|
val newChapters = source.getChapterList(manga.toSManga())
|
|
|
|
val new = syncChaptersWithSource.await(newChapters, manga, source)
|
|
return new to getChapterByMangaId.await(manga.id)
|
|
} catch (t: Throwable) {
|
|
if (t is EHentai.GalleryNotFoundException) {
|
|
val meta = getFlatMetadataById.await(manga.id)?.raise<EHentaiSearchMetadata>()
|
|
if (meta != null) {
|
|
// Age dead galleries
|
|
logger.d("Aged %s - notfound", manga.id)
|
|
meta.aged = true
|
|
insertFlatMetadata.await(meta)
|
|
}
|
|
throw GalleryNotUpdatedException(false, t)
|
|
}
|
|
throw GalleryNotUpdatedException(true, t)
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val MAX_UPDATE_FAILURES = 5
|
|
|
|
private val MIN_BACKGROUND_UPDATE_FREQ = 1.days.inWholeMilliseconds
|
|
|
|
private const val TAG = "EHBackgroundUpdater"
|
|
|
|
private val logger by lazy { XLog.tag("EHUpdaterScheduler") }
|
|
|
|
fun launchBackgroundTest(context: Context) {
|
|
WorkManager.getInstance(context).enqueue(OneTimeWorkRequestBuilder<EHentaiUpdateWorker>().build())
|
|
}
|
|
|
|
fun scheduleBackground(context: Context, prefInterval: Int? = null, prefRestrictions: Set<String>? = null) {
|
|
val preferences = Injekt.get<UnsortedPreferences>()
|
|
val interval = prefInterval ?: preferences.exhAutoUpdateFrequency().get()
|
|
if (interval > 0) {
|
|
val restrictions = prefRestrictions ?: preferences.exhAutoUpdateRequirements().get()
|
|
val acRestriction = DEVICE_CHARGING in restrictions
|
|
|
|
val constraints = Constraints.Builder()
|
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
.setRequiresCharging(acRestriction)
|
|
.build()
|
|
|
|
val request = PeriodicWorkRequestBuilder<EHentaiUpdateWorker>(
|
|
interval.toLong(),
|
|
TimeUnit.HOURS,
|
|
10,
|
|
TimeUnit.MINUTES,
|
|
)
|
|
.addTag(TAG)
|
|
.setConstraints(constraints)
|
|
.build()
|
|
|
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
|
|
logger.d("Successfully scheduled background update job!")
|
|
} else {
|
|
cancelBackground(context)
|
|
}
|
|
}
|
|
|
|
fun cancelBackground(context: Context) {
|
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
|
}
|
|
}
|
|
|
|
fun requiresWifiConnection(preferences: UnsortedPreferences): Boolean {
|
|
val restrictions = preferences.exhAutoUpdateRequirements().get()
|
|
return DEVICE_ONLY_ON_WIFI in restrictions
|
|
}
|
|
}
|
|
|
|
data class UpdateEntry(val manga: Manga, val meta: EHentaiSearchMetadata, val rootChapter: Chapter?)
|
|
|
|
object EHentaiUpdateWorkerConstants {
|
|
const val UPDATES_PER_ITERATION = 50
|
|
|
|
val GALLERY_AGE_TIME = 365.days.inWholeMilliseconds
|
|
}
|