[Manatoki] cache all requests
This commit is contained in:
@@ -103,9 +103,6 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
override val name = "manatoki.net"
|
override val name = "manatoki.net"
|
||||||
override val iconResID = R.drawable.manatoki
|
override val iconResID = R.drawable.manatoki
|
||||||
|
|
||||||
private val readerInfoMutex = Mutex()
|
|
||||||
private val readerInfoCache = LruCache<String, ReaderInfo>(25)
|
|
||||||
|
|
||||||
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
override fun NavGraphBuilder.navGraph(navController: NavController) {
|
||||||
navigation(route = name, startDestination = "manatoki.net/") {
|
navigation(route = name, startDestination = "manatoki.net/") {
|
||||||
composable("manatoki.net/") { Main(navController) }
|
composable("manatoki.net/") { Main(navController) }
|
||||||
@@ -139,9 +136,6 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
|
|
||||||
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
|
val onReader: (ReaderInfo) -> Unit = { readerInfo ->
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
readerInfoMutex.withLock {
|
|
||||||
readerInfoCache.put(readerInfo.itemID, readerInfo)
|
|
||||||
}
|
|
||||||
sheetState.snapTo(ModalBottomSheetValue.Hidden)
|
sheetState.snapTo(ModalBottomSheetValue.Hidden)
|
||||||
navController.navigate("manatoki.net/reader/${readerInfo.itemID}")
|
navController.navigate("manatoki.net/reader/${readerInfo.itemID}")
|
||||||
}
|
}
|
||||||
@@ -397,16 +391,13 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (itemID != null)
|
if (itemID != null)
|
||||||
readerInfoMutex.withLock {
|
client.getItem(itemID, onReader = {
|
||||||
readerInfoCache.get(itemID)?.let {
|
readerInfo = it
|
||||||
readerInfo = it
|
model.load(it.urls) {
|
||||||
model.load(it.urls) {
|
set("User-Agent", imageUserAgent)
|
||||||
set("User-Agent", imageUserAgent)
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
model.error = true
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
else model.error = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val bookmark by bookmarkDao.contains(name, itemID ?: "").observeAsState(false)
|
val bookmark by bookmarkDao.contains(name, itemID ?: "").observeAsState(false)
|
||||||
@@ -448,13 +439,8 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
client.getItem(
|
client.getItem(
|
||||||
it,
|
it,
|
||||||
onReader = {
|
onReader = {
|
||||||
coroutineScope.launch {
|
navController.navigate("manatoki.net/reader/${it.itemID}") {
|
||||||
readerInfoMutex.withLock {
|
popUpTo("manatoki.net/")
|
||||||
readerInfoCache.put(it.itemID, it)
|
|
||||||
}
|
|
||||||
navController.navigate("manatoki.net/reader/${it.itemID}") {
|
|
||||||
popUpTo("manatoki.net/")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -648,9 +634,6 @@ class Manatoki(app: Application) : Source(), DIAware {
|
|||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
client.getItem(it, onReader = {
|
client.getItem(it, onReader = {
|
||||||
launch {
|
launch {
|
||||||
readerInfoMutex.withLock {
|
|
||||||
readerInfoCache.put(it.itemID, it)
|
|
||||||
}
|
|
||||||
sheetState.snapTo(ModalBottomSheetValue.Hidden)
|
sheetState.snapTo(ModalBottomSheetValue.Hidden)
|
||||||
navController.navigate("manatoki.net/reader/${it.itemID}")
|
navController.navigate("manatoki.net/reader/${it.itemID}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,15 @@
|
|||||||
package xyz.quaver.pupil.sources.manatoki
|
package xyz.quaver.pupil.sources.manatoki
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.collection.LruCache
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
|
||||||
import androidx.compose.runtime.saveable.Saver
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.common.util.concurrent.RateLimiter
|
import com.google.common.util.concurrent.RateLimiter
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
@@ -41,7 +36,6 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
@@ -104,119 +98,151 @@ fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val cache = LruCache<String, Any>(50)
|
||||||
suspend fun HttpClient.getItem(
|
suspend fun HttpClient.getItem(
|
||||||
itemID: String,
|
itemID: String,
|
||||||
onListing: (MangaListing) -> Unit = { },
|
onListing: (MangaListing) -> Unit = { },
|
||||||
onReader: (ReaderInfo) -> Unit = { }
|
onReader: (ReaderInfo) -> Unit = { },
|
||||||
|
onError: (Throwable) -> Unit = { throw it }
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
waitForRateLimit()
|
val cachedValue = synchronized(cache) {
|
||||||
val content: String = get("https://manatoki116.net/comic/$itemID")
|
cache.get(itemID)
|
||||||
|
}
|
||||||
|
|
||||||
val doc = Jsoup.parse(content)
|
if (cachedValue != null) {
|
||||||
|
when (cachedValue) {
|
||||||
yield()
|
is MangaListing -> onListing(cachedValue)
|
||||||
|
is ReaderInfo -> onReader(cachedValue)
|
||||||
if (doc.getElementsByClass("serial-list").size == 0) {
|
else -> onError(IllegalStateException("Cached value is not MangaListing nor ReaderInfo"))
|
||||||
val htmlData = doc
|
}
|
||||||
.selectFirst(".view-padding > script")!!
|
|
||||||
.data()
|
|
||||||
.splitToSequence('\n')
|
|
||||||
.fold(StringBuilder()) { sb, line ->
|
|
||||||
if (!line.startsWith("html_data")) return@fold sb
|
|
||||||
|
|
||||||
line.drop(12).dropLast(2).split('.').forEach {
|
|
||||||
if (it.isNotBlank()) sb.appendCodePoint(it.toInt(16))
|
|
||||||
}
|
|
||||||
sb
|
|
||||||
}.toString()
|
|
||||||
|
|
||||||
val urls = Jsoup.parse(htmlData)
|
|
||||||
.select("img[^data-]:not([style])")
|
|
||||||
.map {
|
|
||||||
it.attributes()
|
|
||||||
.first { it.key.startsWith("data-") }
|
|
||||||
.value
|
|
||||||
}
|
|
||||||
|
|
||||||
val title = doc.getElementsByClass("toon-title").first()!!.ownText()
|
|
||||||
|
|
||||||
val listingItemID = doc.select("a:contains(전체목록)").first()!!.attr("href").takeLastWhile { it != '/' }
|
|
||||||
|
|
||||||
onReader(
|
|
||||||
ReaderInfo(
|
|
||||||
itemID,
|
|
||||||
title,
|
|
||||||
urls,
|
|
||||||
listingItemID
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
val titleBlock = doc.selectFirst("div.view-title")!!
|
runCatching {
|
||||||
|
waitForRateLimit()
|
||||||
|
val content: String = get("https://manatoki116.net/comic/$itemID")
|
||||||
|
|
||||||
val title = titleBlock.select("div.view-content:not([itemprop])").first()!!.text()
|
val doc = Jsoup.parse(content)
|
||||||
|
|
||||||
val author =
|
yield()
|
||||||
titleBlock
|
|
||||||
.select("div.view-content:not([itemprop]):contains(작가)")
|
|
||||||
.first()!!
|
|
||||||
.getElementsByTag("a")
|
|
||||||
.first()!!
|
|
||||||
.text()
|
|
||||||
|
|
||||||
val tags =
|
if (doc.getElementsByClass("serial-list").size == 0) {
|
||||||
titleBlock
|
val htmlData = doc
|
||||||
.select("div.view-content:not([itemprop]):contains(분류)")
|
.selectFirst(".view-padding > script")!!
|
||||||
.first()!!
|
.data()
|
||||||
.getElementsByTag("a")
|
.splitToSequence('\n')
|
||||||
.map { it.text() }
|
.fold(StringBuilder()) { sb, line ->
|
||||||
|
if (!line.startsWith("html_data")) return@fold sb
|
||||||
|
|
||||||
val type =
|
line.drop(12).dropLast(2).split('.').forEach {
|
||||||
titleBlock
|
if (it.isNotBlank()) sb.appendCodePoint(it.toInt(16))
|
||||||
.select("div.view-content:not([itemprop]):contains(발행구분)")
|
}
|
||||||
.first()!!
|
sb
|
||||||
.getElementsByTag("a")
|
}.toString()
|
||||||
.first()!!
|
|
||||||
.text()
|
|
||||||
|
|
||||||
val thumbnail =
|
val urls = Jsoup.parse(htmlData)
|
||||||
titleBlock.getElementsByTag("img").first()!!.attr("src")
|
.select("img[^data-]:not([style])")
|
||||||
|
.map {
|
||||||
|
it.attributes()
|
||||||
|
.first { it.key.startsWith("data-") }
|
||||||
|
.value
|
||||||
|
}
|
||||||
|
|
||||||
val thumbsUpCount =
|
val title = doc.getElementsByClass("toon-title").first()!!.ownText()
|
||||||
titleBlock.select("i.fa-thumbs-up + b").text().toInt()
|
|
||||||
|
|
||||||
val entries =
|
val listingItemID = doc.select("a:contains(전체목록)").first()!!.attr("href")
|
||||||
doc.select("div.serial-list .list-item").map {
|
.takeLastWhile { it != '/' }
|
||||||
val episode = it.getElementsByClass("wr-num").first()!!.text().toInt()
|
|
||||||
val (itemID, title) = it.getElementsByClass("item-subject").first()!!.let { subject ->
|
|
||||||
subject.attr("href").dropLastWhile { it != '?' }.dropLast(1).takeLastWhile { it != '/' } to subject.ownText()
|
|
||||||
}
|
|
||||||
val starRating = it.getElementsByClass("wr-star").first()!!.text().drop(1).takeWhile { it != ')' }.toFloat()
|
|
||||||
val date = it.getElementsByClass("wr-date").first()!!.text()
|
|
||||||
val viewCount = it.getElementsByClass("wr-hit").first()!!.text().replace(",", "").toInt()
|
|
||||||
val thumbsUpCount = it.getElementsByClass("wr-good").first()!!.text().replace(",", "").toInt()
|
|
||||||
|
|
||||||
MangaListingEntry(
|
val readerInfo = ReaderInfo(
|
||||||
itemID,
|
itemID,
|
||||||
episode,
|
|
||||||
title,
|
title,
|
||||||
starRating,
|
urls,
|
||||||
date,
|
listingItemID
|
||||||
viewCount,
|
|
||||||
thumbsUpCount
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
onListing(
|
synchronized(cache) {
|
||||||
MangaListing(
|
cache.put(itemID, readerInfo)
|
||||||
itemID,
|
}
|
||||||
title,
|
|
||||||
thumbnail,
|
onReader(readerInfo)
|
||||||
author,
|
} else {
|
||||||
tags,
|
val titleBlock = doc.selectFirst("div.view-title")!!
|
||||||
type,
|
|
||||||
thumbsUpCount,
|
val title = titleBlock.select("div.view-content:not([itemprop])").first()!!.text()
|
||||||
entries
|
|
||||||
)
|
val author =
|
||||||
)
|
titleBlock
|
||||||
|
.select("div.view-content:not([itemprop]):contains(작가)")
|
||||||
|
.first()!!
|
||||||
|
.getElementsByTag("a")
|
||||||
|
.first()!!
|
||||||
|
.text()
|
||||||
|
|
||||||
|
val tags =
|
||||||
|
titleBlock
|
||||||
|
.select("div.view-content:not([itemprop]):contains(분류)")
|
||||||
|
.first()!!
|
||||||
|
.getElementsByTag("a")
|
||||||
|
.map { it.text() }
|
||||||
|
|
||||||
|
val type =
|
||||||
|
titleBlock
|
||||||
|
.select("div.view-content:not([itemprop]):contains(발행구분)")
|
||||||
|
.first()!!
|
||||||
|
.getElementsByTag("a")
|
||||||
|
.first()!!
|
||||||
|
.text()
|
||||||
|
|
||||||
|
val thumbnail =
|
||||||
|
titleBlock.getElementsByTag("img").first()!!.attr("src")
|
||||||
|
|
||||||
|
val thumbsUpCount =
|
||||||
|
titleBlock.select("i.fa-thumbs-up + b").text().toInt()
|
||||||
|
|
||||||
|
val entries =
|
||||||
|
doc.select("div.serial-list .list-item").map {
|
||||||
|
val episode = it.getElementsByClass("wr-num").first()!!.text().toInt()
|
||||||
|
val (itemID, title) = it.getElementsByClass("item-subject").first()!!
|
||||||
|
.let { subject ->
|
||||||
|
subject.attr("href").dropLastWhile { it != '?' }.dropLast(1)
|
||||||
|
.takeLastWhile { it != '/' } to subject.ownText()
|
||||||
|
}
|
||||||
|
val starRating = it.getElementsByClass("wr-star").first()!!.text().drop(1)
|
||||||
|
.takeWhile { it != ')' }.toFloat()
|
||||||
|
val date = it.getElementsByClass("wr-date").first()!!.text()
|
||||||
|
val viewCount =
|
||||||
|
it.getElementsByClass("wr-hit").first()!!.text().replace(",", "")
|
||||||
|
.toInt()
|
||||||
|
val thumbsUpCount =
|
||||||
|
it.getElementsByClass("wr-good").first()!!.text().replace(",", "")
|
||||||
|
.toInt()
|
||||||
|
|
||||||
|
MangaListingEntry(
|
||||||
|
itemID,
|
||||||
|
episode,
|
||||||
|
title,
|
||||||
|
starRating,
|
||||||
|
date,
|
||||||
|
viewCount,
|
||||||
|
thumbsUpCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaListing = MangaListing(
|
||||||
|
itemID,
|
||||||
|
title,
|
||||||
|
thumbnail,
|
||||||
|
author,
|
||||||
|
tags,
|
||||||
|
type,
|
||||||
|
thumbsUpCount,
|
||||||
|
entries
|
||||||
|
)
|
||||||
|
|
||||||
|
synchronized(cache) {
|
||||||
|
cache.put(itemID, mangaListing)
|
||||||
|
}
|
||||||
|
|
||||||
|
onListing(mangaListing)
|
||||||
|
}
|
||||||
|
}.onFailure(onError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user