komikku/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt

391 lines
16 KiB
Kotlin
Raw Normal View History

2019-04-18 23:40:13 +02:00
package exh.eh
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.os.Build
import com.elvishew.xlog.XLog
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
2019-04-18 23:40:13 +02:00
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.all.EHentai
2020-03-31 22:38:40 +02:00
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
2019-04-18 23:40:13 +02:00
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateWorkerConstants.UPDATES_PER_ITERATION
2019-04-18 23:40:13 +02:00
import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
2019-04-18 23:40:13 +02:00
import exh.util.await
import exh.util.cancellable
import exh.util.jobScheduler
2020-08-15 21:57:07 +02:00
import java.util.ArrayList
import kotlin.coroutines.CoroutineContext
import kotlin.time.ExperimentalTime
import kotlin.time.days
import kotlin.time.hours
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
2019-04-18 23:40:13 +02:00
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class EHentaiUpdateWorker : JobService(), CoroutineScope {
2019-04-18 23:40:13 +02:00
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + Job()
private val db: DatabaseHelper by injectLazy()
private val prefs: PreferencesHelper by injectLazy()
private val gson: Gson by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val updateHelper: EHentaiUpdateHelper by injectLazy()
private val logger = XLog.tag("EHUpdater")
private val updateNotifier by lazy { LibraryUpdateNotifier(this) }
2019-04-18 23:40:13 +02:00
/**
* This method is called if the system has determined that you must stop execution of your job
* even before you've had a chance to call [.jobFinished].
*
*
* This will happen if the requirements specified at schedule time are no longer met. For
* example you may have requested WiFi with
* [android.app.job.JobInfo.Builder.setRequiredNetworkType], yet while your
* job was executing the user toggled WiFi. Another example is if you had specified
* [android.app.job.JobInfo.Builder.setRequiresDeviceIdle], and the phone left its
* idle maintenance window. You are solely responsible for the behavior of your application
* upon receipt of this message; your app will likely start to misbehave if you ignore it.
*
*
* Once this method returns, the system releases the wakelock that it is holding on
* behalf of the job.
*
* @param params The parameters identifying this job, as supplied to
* the job in the [.onStartJob] callback.
* @return `true` to indicate to the JobManager whether you'd like to reschedule
* this job based on the retry criteria provided at job creation-time; or `false`
* to end the job entirely. Regardless of the value returned, your job must stop executing.
*/
override fun onStopJob(params: JobParameters?): Boolean {
2019-04-18 23:59:17 +02:00
runBlocking { this@EHentaiUpdateWorker.coroutineContext[Job]?.cancelAndJoin() }
2019-04-18 23:40:13 +02:00
return false
}
/**
* Called to indicate that the job has begun executing. Override this method with the
* logic for your job. Like all other component lifecycle callbacks, this method executes
* on your application's main thread.
*
*
* Return `true` from this method if your job needs to continue running. If you
* do this, the job remains active until you call
* [.jobFinished] to tell the system that it has completed
* its work, or until the job's required constraints are no longer satisfied. For
* example, if the job was scheduled using
* [setRequiresCharging(true)][JobInfo.Builder.setRequiresCharging],
* it will be immediately halted by the system if the user unplugs the device from power,
* the job's [.onStopJob] callback will be invoked, and the app
* will be expected to shut down all ongoing work connected with that job.
*
*
* The system holds a wakelock on behalf of your app as long as your job is executing.
* This wakelock is acquired before this method is invoked, and is not released until either
* you call [.jobFinished], or after the system invokes
* [.onStopJob] to notify your job that it is being shut down
* prematurely.
*
*
* Returning `false` from this method means your job is already finished. The
* system's wakelock for the job will be released, and [.onStopJob]
* will not be invoked.
*
* @param params Parameters specifying info about this job, including the optional
* extras configured with [ This object serves to identify this specific running job instance when calling][JobInfo.Builder.setExtras]
*/
override fun onStartJob(params: JobParameters): Boolean {
launch {
startUpdating()
logger.d("Update job completed!")
jobFinished(params, false)
}
return true
}
2020-08-05 01:32:36 +02:00
private suspend fun startUpdating() {
2019-04-18 23:40:13 +02:00
logger.d("Update job started!")
val startTime = System.currentTimeMillis()
logger.d("Finding manga with metadata...")
2019-04-19 10:19:07 +02:00
val metadataManga = db.getFavoriteMangaWithMetadata().await()
2019-04-18 23:40:13 +02:00
logger.d("Filtering manga and raising metadata...")
val curTime = System.currentTimeMillis()
val allMeta = metadataManga.asFlow().cancellable().mapNotNull { manga ->
2020-05-02 06:46:24 +02:00
if (manga.source != EH_SOURCE_ID && manga.source != EXH_SOURCE_ID) {
2019-04-18 23:40:13 +02:00
return@mapNotNull null
2020-05-02 06:46:24 +02:00
}
2019-04-18 23:40:13 +02:00
val meta = db.getFlatMetadataForManga(manga.id!!).asRxSingle().await()
2020-05-02 06:46:24 +02:00
?: return@mapNotNull null
2019-04-18 23:40:13 +02:00
val raisedMeta = meta.raise<EHentaiSearchMetadata>()
// Don't update galleries too frequently
2020-05-02 06:46:24 +02:00
if (raisedMeta.aged || (curTime - raisedMeta.lastUpdateCheck < MIN_BACKGROUND_UPDATE_FREQ && DebugToggles.RESTRICT_EXH_GALLERY_UPDATE_CHECK_FREQUENCY.enabled)) {
2019-04-18 23:40:13 +02:00
return@mapNotNull null
2020-05-02 06:46:24 +02:00
}
2019-04-18 23:40:13 +02:00
2020-08-24 23:25:10 +02:00
val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minByOrNull {
2019-04-18 23:40:13 +02:00
it.date_upload
}
UpdateEntry(manga, raisedMeta, chapter)
}.toList().sortedBy { it.meta.lastUpdateCheck }
2019-04-18 23:40:13 +02:00
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 = ArrayList<Pair<Manga, Array<Chapter>>>()
2019-04-18 23:40:13 +02:00
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
}
2020-05-02 06:46:24 +02:00
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
)
2019-04-18 23:40:13 +02:00
if (manga.id in modifiedThisIteration) {
// We already processed this manga!
logger.w("Gallery already updated this iteration, skipping...")
updatedThisIteration++
continue
}
val (new, chapters) = try {
2019-04-18 23:40:13 +02:00
updateEntryAndGetChapters(manga)
} catch (e: GalleryNotUpdatedException) {
if (e.network) {
failuresThisIteration++
logger.e("> Network error while updating gallery!", e)
2020-05-02 06:46:24 +02:00
logger.e(
"> (manga.id: %s, meta.gId: %s, meta.gToken: %s, failures-so-far: %s)",
manga.id,
meta.gId,
meta.gToken,
failuresThisIteration
)
2019-04-18 23:40:13 +02:00
}
continue
}
if (chapters.isEmpty()) {
2020-05-02 06:46:24 +02:00
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
)
2019-04-18 23:40:13 +02:00
continue
}
// Find accepted root and discard others
val (acceptedRoot, discardedRoots, hasNew) =
2020-05-02 06:46:24 +02:00
updateHelper.findAcceptedRootAndDiscardOthers(manga.source, chapters).await()
2019-04-18 23:40:13 +02:00
if ((new.isNotEmpty() && manga.id == acceptedRoot.manga.id) ||
(hasNew && updatedManga.none { it.first.id == acceptedRoot.manga.id })
2020-05-02 06:46:24 +02:00
) {
updatedManga += Pair(acceptedRoot.manga, new.toTypedArray())
}
2019-04-18 23:40:13 +02:00
modifiedThisIteration += acceptedRoot.manga.id!!
modifiedThisIteration += discardedRoots.map { it.manga.id!! }
updatedThisIteration++
}
} finally {
prefs.eh_autoUpdateStats().set(
2020-05-02 06:46:24 +02:00
gson.toJson(
EHentaiUpdaterStats(
startTime,
allMeta.size,
updatedThisIteration
2019-04-18 23:40:13 +02:00
)
2020-05-02 06:46:24 +02:00
)
2019-04-18 23:40:13 +02:00
)
if (updatedManga.isNotEmpty()) {
updateNotifier.showUpdateNotifications(updatedManga)
2019-08-09 10:40:44 +02:00
}
2019-04-18 23:40:13 +02:00
}
}
// New, current
2020-08-05 01:32:36 +02:00
private suspend fun updateEntryAndGetChapters(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
2019-07-28 08:32:10 +02:00
val source = sourceManager.get(manga.source) as? EHentai
2020-05-02 06:46:24 +02:00
?: throw GalleryNotUpdatedException(false, IllegalStateException("Missing EH-based source (${manga.source})!"))
2019-04-18 23:40:13 +02:00
try {
val updatedManga = source.fetchMangaDetails(manga).toSingle().await(Schedulers.io())
manga.copyFrom(updatedManga)
db.insertManga(manga).asRxSingle().await()
val newChapters = source.fetchChapterList(manga).toSingle().await(Schedulers.io())
val (new, _) = syncChaptersWithSource(db, newChapters, manga, source) // Not suspending, but does block, maybe fix this?
return new to db.getChapters(manga).await()
} catch (t: Throwable) {
if (t is EHentai.GalleryNotFoundException) {
2019-04-18 23:40:13 +02:00
val meta = db.getFlatMetadataForManga(manga.id!!).await()?.raise<EHentaiSearchMetadata>()
if (meta != null) {
2019-04-18 23:40:13 +02:00
// Age dead galleries
2020-02-10 20:04:16 +01:00
logger.d("Aged %s - notfound", manga.id)
2019-04-18 23:40:13 +02:00
meta.aged = true
db.insertFlatMetadata(meta.flatten()).await()
}
throw GalleryNotUpdatedException(false, t)
}
throw GalleryNotUpdatedException(true, t)
}
}
companion object {
private const val MAX_UPDATE_FAILURES = 5
@OptIn(ExperimentalTime::class)
private val MIN_BACKGROUND_UPDATE_FREQ = 1.days.toLongMilliseconds()
2019-04-18 23:40:13 +02:00
2019-08-13 07:41:31 +02:00
private const val JOB_ID_UPDATE_BACKGROUND = 700000
private const val JOB_ID_UPDATE_BACKGROUND_TEST = 700001
2019-04-18 23:40:13 +02:00
private val logger by lazy { XLog.tag("EHUpdaterScheduler") }
private fun Context.componentName(): ComponentName {
return ComponentName(this, EHentaiUpdateWorker::class.java)
}
private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder {
return JobInfo.Builder(
2020-05-02 06:46:24 +02:00
if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
else JOB_ID_UPDATE_BACKGROUND,
componentName()
)
2019-04-18 23:40:13 +02:00
}
private fun Context.periodicBackgroundJobInfo(
period: Long,
requireCharging: Boolean,
requireUnmetered: Boolean
): JobInfo {
2019-04-18 23:40:13 +02:00
return baseBackgroundJobInfo(false)
2020-05-02 06:46:24 +02:00
.setPeriodic(period)
.setPersisted(true)
.setRequiredNetworkType(
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
else JobInfo.NETWORK_TYPE_ANY
)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setRequiresBatteryNotLow(true)
2019-04-18 23:40:13 +02:00
}
2020-05-02 06:46:24 +02:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setEstimatedNetworkBytes(
15000L * UPDATES_PER_ITERATION,
1000L * UPDATES_PER_ITERATION
)
}
}
.setRequiresCharging(requireCharging)
2019-04-18 23:59:17 +02:00
// .setRequiresDeviceIdle(true) Job never seems to run with this
2020-05-02 06:46:24 +02:00
.build()
2019-04-18 23:40:13 +02:00
}
private fun Context.testBackgroundJobInfo(): JobInfo {
return baseBackgroundJobInfo(true)
2020-05-02 06:46:24 +02:00
.setOverrideDeadline(1)
.build()
2019-04-18 23:40:13 +02:00
}
2020-05-17 01:44:12 +02:00
fun launchBackgroundTest(context: Context): String {
2019-04-18 23:40:13 +02:00
val jobScheduler = context.jobScheduler
2020-08-05 01:32:36 +02:00
return if (jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
2019-04-18 23:40:13 +02:00
logger.e("Failed to schedule background test job!")
2020-08-05 01:32:36 +02:00
"Failed"
2019-04-18 23:40:13 +02:00
} else {
logger.d("Successfully scheduled background test job!")
2020-08-05 01:32:36 +02:00
"Success"
2019-04-18 23:40:13 +02:00
}
}
fun scheduleBackground(context: Context, prefInterval: Int? = null) {
cancelBackground(context)
val preferences = Injekt.get<PreferencesHelper>()
val duration = prefInterval ?: preferences.eh_autoUpdateFrequency().get()
if (duration > 0) {
2019-04-18 23:40:13 +02:00
val restrictions = preferences.eh_autoUpdateRequirements()!!
val acRestriction = "ac" in restrictions
val wifiRestriction = "wifi" in restrictions
@OptIn(ExperimentalTime::class)
2019-04-18 23:40:13 +02:00
val jobInfo = context.periodicBackgroundJobInfo(
duration.hours.toLongMilliseconds(),
2020-05-02 06:46:24 +02:00
acRestriction,
wifiRestriction
2019-04-18 23:40:13 +02:00
)
if (context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
2019-04-18 23:40:13 +02:00
logger.e("Failed to schedule background update job!")
} else {
logger.d("Successfully scheduled background update job!")
}
}
}
fun cancelBackground(context: Context) {
context.jobScheduler.cancel(JOB_ID_UPDATE_BACKGROUND)
}
}
}
data class UpdateEntry(val manga: Manga, val meta: EHentaiSearchMetadata, val rootChapter: Chapter?)
object EHentaiUpdateWorkerConstants {
const val UPDATES_PER_ITERATION = 50
@OptIn(ExperimentalTime::class)
val GALLERY_AGE_TIME = 365.days.toLongMilliseconds()
}