BookWalker Global: high-res thumbnails and details from api (#11654)
* thumbnails * get details from api * typo * adjustments * use select
This commit is contained in:
parent
e595780f24
commit
eec57bbf65
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'BookWalker Global'
|
extName = 'BookWalker Global'
|
||||||
extClass = '.BookWalker'
|
extClass = '.BookWalker'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import androidx.preference.EditTextPreference
|
|||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.extension.en.bookwalker.dto.BookUpdateDto
|
||||||
import eu.kanade.tachiyomi.extension.en.bookwalker.dto.HoldBookEntityDto
|
import eu.kanade.tachiyomi.extension.en.bookwalker.dto.HoldBookEntityDto
|
||||||
import eu.kanade.tachiyomi.extension.en.bookwalker.dto.HoldBooksInfoDto
|
import eu.kanade.tachiyomi.extension.en.bookwalker.dto.HoldBooksInfoDto
|
||||||
import eu.kanade.tachiyomi.extension.en.bookwalker.dto.SeriesDto
|
import eu.kanade.tachiyomi.extension.en.bookwalker.dto.SeriesDto
|
||||||
@ -31,6 +32,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import kotlinx.serialization.modules.plus
|
import kotlinx.serialization.modules.plus
|
||||||
@ -52,13 +54,18 @@ import java.util.regex.PatternSyntaxException
|
|||||||
class BookWalker : ConfigurableSource, ParsedHttpSource(), BookWalkerPreferences {
|
class BookWalker : ConfigurableSource, ParsedHttpSource(), BookWalkerPreferences {
|
||||||
|
|
||||||
override val name = "BookWalker Global"
|
override val name = "BookWalker Global"
|
||||||
|
private val domain = "bookwalker.jp"
|
||||||
|
|
||||||
override val baseUrl = "https://global.bookwalker.jp"
|
override val baseUrl = "https://global.$domain"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val rimgUrl = "https://rimg.$domain"
|
||||||
|
private val cUrl = "https://c.$domain"
|
||||||
|
private val memberApiUrl = "https://member-app.$domain/api"
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
override val client = network.client.newBuilder()
|
||||||
.addInterceptor(BookWalkerImageRequestInterceptor(this))
|
.addInterceptor(BookWalkerImageRequestInterceptor(this))
|
||||||
.build()
|
.build()
|
||||||
@ -318,22 +325,31 @@ class BookWalker : ConfigurableSource, ParsedHttpSource(), BookWalkerPreferences
|
|||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
if (showLibraryInPopular) {
|
if (showLibraryInPopular) {
|
||||||
val manga = response.parseAs<HoldBooksInfoDto>().holdBookList.entities
|
val manga = runBlocking(Dispatchers.IO) {
|
||||||
.map {
|
response.parseAs<HoldBooksInfoDto>().holdBookList.entities
|
||||||
when (it) {
|
.mapNotNull { entity ->
|
||||||
is SeriesDto -> SManga.create().apply {
|
when (entity) {
|
||||||
url = "/series/${it.seriesId}/"
|
is SeriesDto -> {
|
||||||
title = it.seriesName.cleanTitle()
|
SManga.create().apply {
|
||||||
thumbnail_url = it.imageUrl
|
url = "/series/${entity.seriesId}/"
|
||||||
}
|
title = entity.seriesName.cleanTitle()
|
||||||
is SingleDto -> SManga.create().apply {
|
thumbnail_url = getHiResCoverFromLegacyUrl(entity.imageUrl)
|
||||||
url = it.detailUrl.substring(baseUrl.length)
|
}
|
||||||
title = it.title.cleanTitle()
|
}
|
||||||
thumbnail_url = it.imageUrl
|
is SingleDto -> {
|
||||||
author = it.authors.joinToString { a -> a.authorName }
|
val bookUpdate = fetchBookUpdate(entity.uuid)
|
||||||
|
bookUpdate?.let { bookUpdate ->
|
||||||
|
SManga.create().apply {
|
||||||
|
url = "/de${entity.uuid}/"
|
||||||
|
title = bookUpdate.seriesName?.cleanTitle() ?: bookUpdate.productName.cleanTitle()
|
||||||
|
thumbnail_url = bookUpdate.coverImageUrl
|
||||||
|
author = bookUpdate.authors.joinToString { it.authorName }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MangasPage(manga, false)
|
return MangasPage(manga, false)
|
||||||
}
|
}
|
||||||
return super.popularMangaParse(response)
|
return super.popularMangaParse(response)
|
||||||
@ -351,8 +367,9 @@ class BookWalker : ConfigurableSource, ParsedHttpSource(), BookWalkerPreferences
|
|||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
url = titleElt.attr("href").substring(baseUrl.length)
|
url = titleElt.attr("href").substring(baseUrl.length)
|
||||||
title = titleElt.attr("title").cleanTitle()
|
title = titleElt.attr("title").cleanTitle()
|
||||||
thumbnail_url = element.select(".a-tile-thumb-img > img")
|
thumbnail_url = getHiResCoverFromLegacyUrl(
|
||||||
.attr("data-srcset").getHighestQualitySrcset()
|
element.selectFirst(".a-tile-thumb-img > img")?.attr("data-srcset")?.getHighestQualitySrcset(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,43 +434,48 @@ class BookWalker : ConfigurableSource, ParsedHttpSource(), BookWalkerPreferences
|
|||||||
.asJsoup()
|
.asJsoup()
|
||||||
.let { validateLogin(it) }
|
.let { validateLogin(it) }
|
||||||
|
|
||||||
val mangaDetails = SManga.create().apply {
|
|
||||||
updateDetailsFromSeriesPage(seriesPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// It generally doesn't matter which chapter we take the description from, but for
|
// It generally doesn't matter which chapter we take the description from, but for
|
||||||
// series that release in volumes we want the earliest one, which will _usually_ be the
|
// series that release in volumes we want the earliest one, which will _usually_ be the
|
||||||
// last one on the page. With that said, it's not worth it to paginate in order to find
|
// last one on the page. With that said, it's not worth it to paginate in order to find
|
||||||
// the earliest volume, and volume releases don't usually have 60+ volumes anyways.
|
// the earliest volume, and volume releases don't usually have 60+ volumes anyways.
|
||||||
val chapterUrl = seriesPage
|
val firstItem = seriesPage.selectFirst(".o-tile:not(:has(.a-ribbon-pre-order)) .a-tile-ttl a")
|
||||||
.select(".o-tile .a-tile-ttl a").last()
|
val uuid = firstItem!!.absUrl("href").substringAfter("/de").substringBefore("/")
|
||||||
?.attr("href")
|
val bookUpdate = fetchBookUpdate(uuid)
|
||||||
?: return@rxSingle mangaDetails
|
|
||||||
|
|
||||||
val chapterPage = client.newCall(GET(chapterUrl, callHeaders)).await().asJsoup()
|
SManga.create().apply {
|
||||||
|
title = bookUpdate!!.seriesName?.cleanTitle() ?: bookUpdate.productName.cleanTitle()
|
||||||
mangaDetails.apply {
|
author = bookUpdate.authors.joinToString { it.authorName }
|
||||||
description = getDescriptionFromChapterPage(chapterPage)
|
description = listOfNotNull(bookUpdate.productExplanationShort, bookUpdate.productExplanationDetails)
|
||||||
|
.joinToString("\n\n")
|
||||||
|
.trim()
|
||||||
|
thumbnail_url = bookUpdate.coverImageUrl
|
||||||
|
genre = getAvailableFilterNames(seriesPage, "side-genre").joinToString()
|
||||||
|
val statusIndicators = seriesPage.select("ul.side-others > li > a").map { it.ownText() }
|
||||||
|
status = parseStatus(statusIndicators)
|
||||||
}
|
}
|
||||||
}.toObservable()
|
}.toObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchSingleMangaDetails(manga: SManga): Observable<SManga> {
|
private fun fetchSingleMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return rxSingle {
|
return rxSingle {
|
||||||
val document = client.newCall(GET(baseUrl + manga.url, callHeaders))
|
val uuid = manga.url.substringAfter("/de").substringBefore("/")
|
||||||
.awaitSuccess()
|
val bookUpdate = fetchBookUpdate(uuid)
|
||||||
.asJsoup()
|
|
||||||
|
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = getTitleFromChapterPage(document)?.cleanTitle().orEmpty()
|
title = bookUpdate!!.seriesName?.cleanTitle() ?: bookUpdate.productName.cleanTitle()
|
||||||
|
author = bookUpdate.authors.joinToString { it.authorName }
|
||||||
|
description = listOfNotNull(bookUpdate.productExplanationShort, bookUpdate.productExplanationDetails)
|
||||||
|
.joinToString("\n\n")
|
||||||
|
.trim()
|
||||||
|
thumbnail_url = bookUpdate.coverImageUrl
|
||||||
|
|
||||||
description = getDescriptionFromChapterPage(document)
|
|
||||||
// From the browse pages we can't distinguish between a true one-shot and a
|
// From the browse pages we can't distinguish between a true one-shot and a
|
||||||
// serial manga with only one chapter, but we can detect if there's a series
|
// serial manga with only one chapter, but we can detect if there's a series
|
||||||
// reference in the chapter page. If there is, we should let the user know that
|
// reference in the chapter page. If there is, we should let the user know that
|
||||||
// they may need to take some action in the future to correct the error.
|
// they may need to take some action in the future to correct the error.
|
||||||
|
val document = client.newCall(GET(baseUrl + manga.url, callHeaders)).awaitSuccess().asJsoup()
|
||||||
if (document.selectFirst(".product-detail th:contains(Series Title)") != null) {
|
if (document.selectFirst(".product-detail th:contains(Series Title)") != null) {
|
||||||
description = (
|
this.description = (
|
||||||
"WARNING: This entry is being treated as a one-shot but appears to " +
|
"WARNING: This entry is being treated as a one-shot but appears to " +
|
||||||
"have an associated series. If another chapter is released in " +
|
"have an associated series. If another chapter is released in " +
|
||||||
"the future, you will likely need to migrate this to itself." +
|
"the future, you will likely need to migrate this to itself." +
|
||||||
@ -464,44 +486,22 @@ class BookWalker : ConfigurableSource, ParsedHttpSource(), BookWalkerPreferences
|
|||||||
}.toObservable()
|
}.toObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SManga.updateDetailsFromSeriesPage(document: Document) = run {
|
private fun parseStatus(statusIndicators: List<String>): Int {
|
||||||
// Take the thumbnail from the first chapter that is not on pre-order.
|
return if (statusIndicators.any { it.startsWith("Completed") }) {
|
||||||
// Pre-order chapters often just have a gray rectangle with "NOW PRINTING" as their
|
if (statusIndicators.any { it.startsWith("Pre-Order") }) {
|
||||||
// thumbnail, which doesn't look very pretty for the catalog.
|
SManga.PUBLISHING_FINISHED
|
||||||
thumbnail_url = document
|
|
||||||
.select(".o-tile:not(:has(.a-ribbon-pre-order)) .a-tile-thumb-img > img")
|
|
||||||
.attr("data-srcset")
|
|
||||||
.getHighestQualitySrcset()
|
|
||||||
title = document.selectFirst(".title-main-inner")!!.ownText().cleanTitle()
|
|
||||||
author = getAvailableFilterNames(document, "side-author").joinToString()
|
|
||||||
genre = getAvailableFilterNames(document, "side-genre").joinToString()
|
|
||||||
|
|
||||||
val statusIndicators = document.select("ul.side-others > li > a").map { it.ownText() }
|
|
||||||
|
|
||||||
status =
|
|
||||||
if (statusIndicators.any { it.startsWith("Completed") }) {
|
|
||||||
if (statusIndicators.any { it.startsWith("Pre-Order") }) {
|
|
||||||
SManga.PUBLISHING_FINISHED
|
|
||||||
} else {
|
|
||||||
SManga.COMPLETED
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
SManga.ONGOING
|
SManga.COMPLETED
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
SManga.ONGOING
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTitleFromChapterPage(document: Document): String? {
|
private fun getTitleFromChapterPage(document: Document): String? {
|
||||||
return document.selectFirst(".detail-book-title-box h1[itemprop='name']")?.ownText()
|
return document.selectFirst(".detail-book-title-box h1[itemprop='name']")?.ownText()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDescriptionFromChapterPage(document: Document): String {
|
|
||||||
return buildString {
|
|
||||||
append(document.select(".synopsis-lead").text())
|
|
||||||
append("\n\n")
|
|
||||||
append(document.select(".synopsis-text").text())
|
|
||||||
}.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return rxSingle {
|
return rxSingle {
|
||||||
if (!manga.url.startsWith("/series/")) {
|
if (!manga.url.startsWith("/series/")) {
|
||||||
@ -750,6 +750,41 @@ class BookWalker : ConfigurableSource, ParsedHttpSource(), BookWalkerPreferences
|
|||||||
return srcsetPairs.maxByOrNull { it.first ?: 0 }?.second
|
return srcsetPairs.maxByOrNull { it.first ?: 0 }?.second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function works if the cover is from before mid-dec'23 (non-hexadecimal).
|
||||||
|
// If it's a newer cover, it will fall back to the low-res version.
|
||||||
|
private fun getHiResCoverFromLegacyUrl(url: String?): String? {
|
||||||
|
if (url.isNullOrEmpty()) return url
|
||||||
|
return try {
|
||||||
|
val extension = url.substringAfterLast(".")
|
||||||
|
val numericId = when {
|
||||||
|
url.startsWith(rimgUrl) -> {
|
||||||
|
val id = url.substringAfter("$rimgUrl/").substringBefore('/')
|
||||||
|
id.reversed().toLongOrNull()
|
||||||
|
}
|
||||||
|
// For legacy covers of "series" from the user's library.
|
||||||
|
url.contains("thumbnailImage_") -> {
|
||||||
|
val id = url.substringAfter("thumbnailImage_").substringBefore(".$extension")
|
||||||
|
id.toLongOrNull()
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
numericId?.let { "$cUrl/coverImage_${it - 1}.$extension" } ?: url
|
||||||
|
} catch (e: Exception) {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch manga details form api.
|
||||||
|
private suspend fun fetchBookUpdate(uuid: String): BookUpdateDto? {
|
||||||
|
val apiUrl = "$memberApiUrl/books/updates".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("fileType", "EPUB")
|
||||||
|
.addQueryParameter(uuid, "0")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return client.newCall(GET(apiUrl, headers)).awaitSuccess()
|
||||||
|
.parseAs<List<BookUpdateDto>>().firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun String.parseChapterNumber(): Pair<String, Float>? {
|
private fun String.parseChapterNumber(): Pair<String, Float>? {
|
||||||
for (pattern in CHAPTER_NUMBER_PATTERNS) {
|
for (pattern in CHAPTER_NUMBER_PATTERNS) {
|
||||||
val match = pattern.find(this)
|
val match = pattern.find(this)
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.bookwalker.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BookUpdateDto(
|
||||||
|
val productName: String,
|
||||||
|
val seriesName: String?,
|
||||||
|
val productExplanationShort: String?,
|
||||||
|
val productExplanationDetails: String,
|
||||||
|
val coverImageUrl: String,
|
||||||
|
val authors: List<AuthorUpdateDto>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class AuthorUpdateDto(
|
||||||
|
val authorName: String,
|
||||||
|
)
|
||||||
@ -6,13 +6,5 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("normal")
|
@SerialName("normal")
|
||||||
class SingleDto(
|
class SingleDto(
|
||||||
val detailUrl: String,
|
val uuid: String,
|
||||||
val title: String,
|
|
||||||
val imageUrl: String,
|
|
||||||
val authors: List<AuthorDto>,
|
|
||||||
) : HoldBookEntityDto()
|
) : HoldBookEntityDto()
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class AuthorDto(
|
|
||||||
val authorName: String,
|
|
||||||
)
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user