package ani.dantotsu.parsers.novel import android.content.Context import ani.dantotsu.currContext import ani.dantotsu.logger import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import logcat.LogPriority import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.system.logcat import uy.kohesive.injekt.injectLazy import java.util.Date import kotlin.time.Duration.Companion.days class NovelExtensionGithubApi { private val networkService: NetworkHelper by injectLazy() private val novelExtensionManager: NovelExtensionManager by injectLazy() private val json: Json by injectLazy() private val lastExtCheck: Long = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getLong("last_ext_check", 0)?:0 private var requiresFallbackSource = false suspend fun findExtensions(): List { return withIOContext { val githubResponse = if (requiresFallbackSource) { null } else { try { networkService.client .newCall(GET("${REPO_URL_PREFIX}index.min.json")) .awaitSuccess() } catch (e: Throwable) { logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" } requiresFallbackSource = true null } } val response = githubResponse ?: run { logger("using fallback source") networkService.client .newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json")) .awaitSuccess() } logger("response: $response") val extensions = with(json) { response .parseAs>() .toExtensions() } // Sanity check - a small number of extensions probably means something broke // with the repo generator /*if (extensions.size < 10) { //TODO: uncomment when more extensions are added throw Exception() }*/ logger("extensions: $extensions") extensions } } suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List? { // Limit checks to once a day at most if (fromAvailableExtensionList && Date().time < lastExtCheck + 1.days.inWholeMilliseconds) { return null } val extensions = if (fromAvailableExtensionList) { novelExtensionManager.availableExtensionsFlow.value } else { findExtensions().also { context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()?.putLong("last_ext_check", Date().time)?.apply() } } val installedExtensions = NovelExtensionLoader.loadExtensions(context) .filterIsInstance() .map { it.extension } val extensionsWithUpdate = mutableListOf() for (installedExt in installedExtensions) { val pkgName = installedExt.pkgName val availableExt = extensions.find { it.pkgName == pkgName } ?: continue val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer) if (hasUpdate) { extensionsWithUpdate.add(installedExt) } } if (extensionsWithUpdate.isNotEmpty()) { ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name }) } return extensionsWithUpdate } private fun List.toExtensions(): List { return mapNotNull { extension -> val sources = extension.sources?.map { source -> NovelExtensionSourceJsonObject( source.id, source.lang, source.name, source.baseUrl, ) } val iconUrl = "${REPO_URL_PREFIX}icons/${extension.pkg}.png" NovelExtension.Available( extension.name, extension.pkg, extension.apk, extension.code, sources?.toSources() ?: emptyList(), iconUrl, ) } } private fun List.toSources(): List { return map { source -> AvailableNovelSources( source.id, source.lang, source.name, source.baseUrl, ) } } fun getApkUrl(extension: NovelExtension.Available): String { return "${getUrlPrefix()}apk/${extension.pkgName}.apk" } private fun getUrlPrefix(): String { return if (requiresFallbackSource) { FALLBACK_REPO_URL_PREFIX } else { REPO_URL_PREFIX } } } private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/dannovels/novel-extensions/main/" private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/dannovels/novel-extensions@latest/" @Serializable private data class NovelExtensionJsonObject( val name: String, val pkg: String, val apk: String, val lang: String, val code: Long, val version: String, val nsfw: Int, val hasReadme: Int = 0, val hasChangelog: Int = 0, val sources: List?, ) @Serializable private data class NovelExtensionSourceJsonObject( val id: Long, val lang: String, val name: String, val baseUrl: String, )