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.
This commit is contained in:
RePod 2021-03-10 06:57:16 -05:00 committed by GitHub
parent addce58067
commit b5ef15ee35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 110 additions and 21 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'LANraragi' extName = 'LANraragi'
pkgNameSuffix = 'all.lanraragi' pkgNameSuffix = 'all.lanraragi'
extClass = '.LANraragi' extClass = '.LANraragi'
extVersionCode = 4 extVersionCode = 5
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -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.extension.all.lanraragi.model.Category
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -28,6 +29,7 @@ import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable
import rx.Single import rx.Single
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -53,19 +55,36 @@ open class LANraragi : ConfigurableSource, HttpSource() {
private val gson: Gson = Gson() private val gson: Gson = Gson()
override fun mangaDetailsParse(response: Response): SManga { private var randomArchiveID: String = ""
val id = getId(response)
return SManga.create().apply { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
thumbnail_url = getThumbnailUri(id) 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<Archive>(response.body()!!.string())
return archiveToSManga(archive)
} }
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
// Upgrade the LRR reader URL to the API metadata endpoint val id = if (manga.url == "/random") randomArchiveID else getReaderId(manga.url)
// without breaking WebView (i.e. for management).
val id = manga.url.split('=').last()
val uri = getApiUriBuilder("/api/archives/$id/metadata").build() val uri = getApiUriBuilder("/api/archives/$id/metadata").build()
return GET(uri.toString(), headers) return GET(uri.toString(), headers)
@ -75,6 +94,16 @@ open class LANraragi : ConfigurableSource, HttpSource() {
val archive = gson.fromJson<Archive>(response.body()!!.string()) val archive = gson.fromJson<Archive>(response.body()!!.string())
val uri = getApiUriBuilder("/api/archives/${archive.arcid}/extract") 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( return listOf(
SChapter.create().apply { SChapter.create().apply {
val uriBuild = uri.build() val uriBuild = uri.build()
@ -117,6 +146,8 @@ open class LANraragi : ConfigurableSource, HttpSource() {
val filters = mutableListOf<Filter<*>>() val filters = mutableListOf<Filter<*>>()
val prefNewOnly = preferences.getBoolean("latestNewOnly", false) val prefNewOnly = preferences.getBoolean("latestNewOnly", false)
filters.add(LatestView())
if (prefNewOnly) filters.add(NewArchivesOnly(true)) if (prefNewOnly) filters.add(NewArchivesOnly(true))
if (latestNamespacePref.isNotBlank()) { if (latestNamespacePref.isNotBlank()) {
@ -142,6 +173,7 @@ open class LANraragi : ConfigurableSource, HttpSource() {
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
is LatestView -> if (filter.state) uri.appendQueryParameter("latest", "latest")
is StartingPage -> { is StartingPage -> {
startPageOffset = filter.state.toIntOrNull() ?: 1 startPageOffset = filter.state.toIntOrNull() ?: 1
@ -170,25 +202,50 @@ open class LANraragi : ConfigurableSource, HttpSource() {
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val jsonResult = gson.fromJson<ArchiveSearchResult>(response.body()!!.string()) val jsonResult = gson.fromJson<ArchiveSearchResult>(response.body()!!.string())
val currentStart = getStart(response) val currentStart = getStart(response)
val archives = arrayListOf<SManga>()
lastResultCount = jsonResult.data.size lastResultCount = jsonResult.data.size
maxResultCount = if (lastResultCount >= maxResultCount) lastResultCount else maxResultCount maxResultCount = if (lastResultCount >= maxResultCount) lastResultCount else maxResultCount
lastRecordsFiltered = jsonResult.recordsFiltered lastRecordsFiltered = jsonResult.recordsFiltered
totalRecords = jsonResult.recordsTotal totalRecords = jsonResult.recordsTotal
return MangasPage( if (canShowRandom(response)) {
jsonResult.data.map { archives.add(
SManga.create().apply { SManga.create().apply {
url = "/reader?id=${it.arcid}" url = "/random"
title = it.title title = "Random"
thumbnail_url = getThumbnailUri(it.arcid) description = "Refresh for a random archive."
genre = it.tags // Get the server's "noThumb" thumbnail by default
artist = getArtist(it.tags) thumbnail_url = getThumbnailUri("tachiyomi")
author = artist
} }
}, )
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 { 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 DescendingOrder(overrideState: Boolean = false) : Filter.CheckBox("Descending Order", overrideState)
private class NewArchivesOnly(overrideState: Boolean = false) : Filter.CheckBox("New Archives Only", overrideState) private class NewArchivesOnly(overrideState: Boolean = false) : Filter.CheckBox("New Archives Only", overrideState)
private class UntaggedArchivesOnly : Filter.CheckBox("Untagged Archives Only", false) private class UntaggedArchivesOnly : Filter.CheckBox("Untagged Archives Only", false)
@ -379,6 +437,18 @@ open class LANraragi : ConfigurableSource, HttpSource() {
} }
// Helper // 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<Pair<String?, String>>) : protected open class UriPartFilter(displayName: String, val vals: Array<Pair<String?, String>>) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) { Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first fun toUriPart() = vals[state].first
@ -430,6 +500,14 @@ open class LANraragi : ConfigurableSource, HttpSource() {
return getTopResponse(response).request().url().queryParameter("start")!!.toInt() 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<String>? { private fun getNSTag(tags: String?, tag: String): List<String>? {
tags?.split(',')?.forEach { tags?.split(',')?.forEach {
if (it.contains(':')) { if (it.contains(':')) {
@ -460,8 +538,19 @@ open class LANraragi : ConfigurableSource, HttpSource() {
// Headers (currently auth) are done in headersBuilder // Headers (currently auth) are done in headersBuilder
override val client: OkHttpClient = network.client.newBuilder().build() 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 { 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 { Single.fromCallable {
client.newCall(GET("$baseUrl/api/categories", headers)).execute() client.newCall(GET("$baseUrl/api/categories", headers)).execute()
} }