From b5ef15ee35392b96a180b2abf4a8a73759374a6c Mon Sep 17 00:00:00 2001 From: RePod Date: Wed, 10 Mar 2021 06:57:16 -0500 Subject: [PATCH] LANraragi: Random item, clear new status (#6091) * LANraragi: Introduce Random item. Appears as the first item under Browse when there's no meaningful filtering. Maintain Latest flow at the cost of an extra query param. * LANraragi: Get and use the Random ID. Helper functions to get a new random ID. Helper functions to get the ID from weird spots. Separate network client to not follow redirects, saving server-side extracts. * LANraragi: Obtain random ID on init. To save one entire refresh for that quality user experience. The call is still to a 301 that is not followed. * LANraragi: Unset isnew on archives. More obvious since Latest was hooked up. Separate from actual reading progress. This was happening indirectly before the previous extension version swapped to API endpoints for metadata. * LANraragi: Bump extension version * LANraragi: AZ detection for ID. Due to how it updates info and chapters independently leading to an expected race condition. When detected avoid the race by accessing the ID via thumbnail. Always using the thumbnail moves the race to non-AZs instead. * Revert "LANraragi: AZ detection for ID." This reverts commit 28541d8d0daf989c129884090311e49148f05112. --- src/all/lanraragi/build.gradle | 2 +- .../extension/all/lanraragi/LANraragi.kt | 129 +++++++++++++++--- 2 files changed, 110 insertions(+), 21 deletions(-) diff --git a/src/all/lanraragi/build.gradle b/src/all/lanraragi/build.gradle index 156f6af9e..819307dcc 100644 --- a/src/all/lanraragi/build.gradle +++ b/src/all/lanraragi/build.gradle @@ -5,7 +5,7 @@ ext { extName = 'LANraragi' pkgNameSuffix = 'all.lanraragi' extClass = '.LANraragi' - extVersionCode = 4 + extVersionCode = 5 libVersion = '1.2' } diff --git a/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/LANraragi.kt b/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/LANraragi.kt index c1e54879f..fd6823d69 100644 --- a/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/LANraragi.kt +++ b/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/LANraragi.kt @@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.extension.all.lanraragi.model.ArchiveSearchResult import eu.kanade.tachiyomi.extension.all.lanraragi.model.Category import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservable import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList @@ -28,6 +29,7 @@ import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import rx.Observable import rx.Single import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -53,19 +55,36 @@ open class LANraragi : ConfigurableSource, HttpSource() { private val gson: Gson = Gson() - override fun mangaDetailsParse(response: Response): SManga { - val id = getId(response) + private var randomArchiveID: String = "" - return SManga.create().apply { - thumbnail_url = getThumbnailUri(id) - } + override fun fetchMangaDetails(manga: SManga): Observable { + val id = if (manga.url == "/random") randomArchiveID else getReaderId(manga.url) + val uri = getApiUriBuilder("/api/archives/$id/metadata").build() + + if (manga.url == "/random") randomArchiveID = getRandomID(getRandomIDResponse()) + + return client.newCall(GET(uri.toString(), headers)) + .asObservable().doOnNext { + if (!it.isSuccessful && it.code() == 404) error("Log in with WebView then try again.") + } + .map { mangaDetailsParse(it).apply { initialized = true } } + } + + override fun mangaDetailsRequest(manga: SManga): Request { + // Catch-all that includes /random's ID via thumbnail + val id = getThumbnailId(manga.thumbnail_url!!) + + return GET("$baseUrl/reader?id=$id", headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + val archive = gson.fromJson(response.body()!!.string()) + + return archiveToSManga(archive) } override fun chapterListRequest(manga: SManga): Request { - // Upgrade the LRR reader URL to the API metadata endpoint - // without breaking WebView (i.e. for management). - - val id = manga.url.split('=').last() + val id = if (manga.url == "/random") randomArchiveID else getReaderId(manga.url) val uri = getApiUriBuilder("/api/archives/$id/metadata").build() return GET(uri.toString(), headers) @@ -75,6 +94,16 @@ open class LANraragi : ConfigurableSource, HttpSource() { val archive = gson.fromJson(response.body()!!.string()) val uri = getApiUriBuilder("/api/archives/${archive.arcid}/extract") + // Replicate old behavior and unset "isnew" for the archive. + if (archive.isnew == "true") { + val clearNew = Request.Builder() + .url("$baseUrl/api/archives/${archive.arcid}/isnew") + .delete() + .build() + + client.newCall(clearNew).execute() + } + return listOf( SChapter.create().apply { val uriBuild = uri.build() @@ -117,6 +146,8 @@ open class LANraragi : ConfigurableSource, HttpSource() { val filters = mutableListOf>() val prefNewOnly = preferences.getBoolean("latestNewOnly", false) + filters.add(LatestView()) + if (prefNewOnly) filters.add(NewArchivesOnly(true)) if (latestNamespacePref.isNotBlank()) { @@ -142,6 +173,7 @@ open class LANraragi : ConfigurableSource, HttpSource() { filters.forEach { filter -> when (filter) { + is LatestView -> if (filter.state) uri.appendQueryParameter("latest", "latest") is StartingPage -> { startPageOffset = filter.state.toIntOrNull() ?: 1 @@ -170,25 +202,50 @@ open class LANraragi : ConfigurableSource, HttpSource() { override fun searchMangaParse(response: Response): MangasPage { val jsonResult = gson.fromJson(response.body()!!.string()) val currentStart = getStart(response) + val archives = arrayListOf() lastResultCount = jsonResult.data.size maxResultCount = if (lastResultCount >= maxResultCount) lastResultCount else maxResultCount lastRecordsFiltered = jsonResult.recordsFiltered totalRecords = jsonResult.recordsTotal - return MangasPage( - jsonResult.data.map { + if (canShowRandom(response)) { + archives.add( SManga.create().apply { - url = "/reader?id=${it.arcid}" - title = it.title - thumbnail_url = getThumbnailUri(it.arcid) - genre = it.tags - artist = getArtist(it.tags) - author = artist + url = "/random" + title = "Random" + description = "Refresh for a random archive." + // Get the server's "noThumb" thumbnail by default + thumbnail_url = getThumbnailUri("tachiyomi") } - }, - currentStart + jsonResult.data.size < jsonResult.recordsFiltered - ) + ) + } + + jsonResult.data.map { + archives.add(archiveToSManga(it)) + } + + return MangasPage(archives, currentStart + jsonResult.data.size < jsonResult.recordsFiltered) + } + + private fun canShowRandom(response: Response): Boolean { + // Server has archives, no meaningful filtering, not paginating, and not Latest + return ( + totalRecords > 0 && + lastRecordsFiltered == totalRecords && + getStart(response) == 0 && + response.request().url().queryParameter("latest") == null + ) + } + + private fun archiveToSManga(archive: Archive) = SManga.create().apply { + url = "/reader?id=${archive.arcid}" + title = archive.title + description = archive.title + thumbnail_url = getThumbnailUri(archive.arcid) + genre = archive.tags + artist = getArtist(archive.tags) + author = artist } override fun headersBuilder() = Headers.Builder().apply { @@ -198,6 +255,7 @@ open class LANraragi : ConfigurableSource, HttpSource() { } } + private class LatestView() : Filter.CheckBox("", true) private class DescendingOrder(overrideState: Boolean = false) : Filter.CheckBox("Descending Order", overrideState) private class NewArchivesOnly(overrideState: Boolean = false) : Filter.CheckBox("New Archives Only", overrideState) private class UntaggedArchivesOnly : Filter.CheckBox("Untagged Archives Only", false) @@ -379,6 +437,18 @@ open class LANraragi : ConfigurableSource, HttpSource() { } // Helper + private fun getRandomID(response: Response): String { + return response.let { + it.headers("Location").first() + }.split("=").last() + } + + private fun getRandomIDResponse(): Response { + // Separate function for init and Library + // /random 301's to the ID so the request is over as quickly as it starts + return clientNoFollow.newCall(GET("$baseUrl/random", headers)).execute() + } + protected open class UriPartFilter(displayName: String, val vals: Array>) : Filter.Select(displayName, vals.map { it.second }.toTypedArray()) { fun toUriPart() = vals[state].first @@ -430,6 +500,14 @@ open class LANraragi : ConfigurableSource, HttpSource() { return getTopResponse(response).request().url().queryParameter("start")!!.toInt() } + private fun getReaderId(url: String): String { + return Regex("""\/reader\?id=(\w{40})""").find(url)?.groupValues?.get(1) ?: "" + } + + private fun getThumbnailId(url: String): String { + return Regex("""\/(\w{40})\/thumbnail""").find(url)?.groupValues?.get(1) ?: "" + } + private fun getNSTag(tags: String?, tag: String): List? { tags?.split(',')?.forEach { if (it.contains(':')) { @@ -460,8 +538,19 @@ open class LANraragi : ConfigurableSource, HttpSource() { // Headers (currently auth) are done in headersBuilder override val client: OkHttpClient = network.client.newBuilder().build() + // Specifically for /random to grab IDs without triggering a server-side extract + private val clientNoFollow: OkHttpClient = client.newBuilder().followRedirects(false).build() init { + // Save users a Random refresh in the extension and from Library + Single.fromCallable { getRandomIDResponse() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { randomArchiveID = getRandomID(it) }, + {} + ) + Single.fromCallable { client.newCall(GET("$baseUrl/api/categories", headers)).execute() }