fix(en/mangafire): rework mangafire extension (#7625)
* fix(en/mangafire): rework mangafire extension * oops * remove non-null assert * small fixes
This commit is contained in:
parent
17300b3bd0
commit
1b1ef9274b
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MangaFire'
|
||||
extClass = '.MangaFireFactory'
|
||||
extVersionCode = 8
|
||||
extVersionCode = 9
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,166 +1,190 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
import java.util.Calendar
|
||||
|
||||
class Entry(name: String, val id: String) : Filter.CheckBox(name) {
|
||||
constructor(name: String) : this(name, name)
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
sealed class Group(
|
||||
open class UriPartFilter(
|
||||
name: String,
|
||||
val param: String,
|
||||
values: List<Entry>,
|
||||
) : Filter.Group<Entry>(name, values)
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
defaultValue: String? = null,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
builder.addQueryParameter(param, vals[state].second)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Select(
|
||||
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
||||
|
||||
open class UriMultiSelectFilter(
|
||||
name: String,
|
||||
val param: String,
|
||||
private val valuesMap: Map<String, String>,
|
||||
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
|
||||
open val selection: String
|
||||
get() = valuesMap[values[state]]!!
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
val checked = state.filter { it.state }
|
||||
|
||||
checked.forEach {
|
||||
builder.addQueryParameter(param, it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilter : Group("Type", "type[]", types)
|
||||
open class UriTriSelectOption(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
private val types: List<Entry>
|
||||
get() = listOf(
|
||||
Entry("Manga", "manga"),
|
||||
Entry("One-Shot", "one_shot"),
|
||||
Entry("Doujinshi", "doujinshi"),
|
||||
Entry("Light-Novel", "light_novel"),
|
||||
Entry("Novel", "novel"),
|
||||
Entry("Manhwa", "manhwa"),
|
||||
Entry("Manhua", "manhua"),
|
||||
)
|
||||
|
||||
class Genre(name: String, val id: String) : Filter.TriState(name) {
|
||||
val selection: String
|
||||
get() = (if (isExcluded()) "-" else "") + id
|
||||
open class UriTriSelectFilter(
|
||||
name: String,
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
) : Filter.Group<UriTriSelectOption>(name, vals.map { UriTriSelectOption(it.first, it.second) }), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
state.forEach { s ->
|
||||
when (s.state) {
|
||||
TriState.STATE_INCLUDE -> builder.addQueryParameter(param, s.value)
|
||||
TriState.STATE_EXCLUDE -> builder.addQueryParameter(param, "-${s.value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenresFilter : Filter.Group<Genre>("Genre", genres) {
|
||||
val param = "genre[]"
|
||||
class TypeFilter : UriPartFilter(
|
||||
"Type",
|
||||
"type",
|
||||
arrayOf(
|
||||
Pair("Manga", "manga"),
|
||||
Pair("One-Shot", "one_shot"),
|
||||
Pair("Doujinshi", "doujinshi"),
|
||||
Pair("Novel", "novel"),
|
||||
Pair("Manhwa", "manhwa"),
|
||||
Pair("Manhua", "manhua"),
|
||||
),
|
||||
)
|
||||
|
||||
val combineMode: Boolean
|
||||
get() = state.filter { !it.isIgnored() }.size > 1
|
||||
class GenreFilter : UriTriSelectFilter(
|
||||
"Genres",
|
||||
"genre[]",
|
||||
arrayOf(
|
||||
Pair("Action", "1"),
|
||||
Pair("Adventure", "78"),
|
||||
Pair("Avant Garde", "3"),
|
||||
Pair("Boys Love", "4"),
|
||||
Pair("Comedy", "5"),
|
||||
Pair("Demons", "77"),
|
||||
Pair("Drama", "6"),
|
||||
Pair("Ecchi", "7"),
|
||||
Pair("Fantasy", "79"),
|
||||
Pair("Girls Love", "9"),
|
||||
Pair("Gourmet", "10"),
|
||||
Pair("Harem", "11"),
|
||||
Pair("Horror", "530"),
|
||||
Pair("Isekai", "13"),
|
||||
Pair("Iyashikei", "531"),
|
||||
Pair("Josei", "15"),
|
||||
Pair("Kids", "532"),
|
||||
Pair("Magic", "539"),
|
||||
Pair("Mahou Shoujo", "533"),
|
||||
Pair("Martial Arts", "534"),
|
||||
Pair("Mecha", "19"),
|
||||
Pair("Military", "535"),
|
||||
Pair("Music", "21"),
|
||||
Pair("Mystery", "22"),
|
||||
Pair("Parody", "23"),
|
||||
Pair("Psychological", "536"),
|
||||
Pair("Reverse Harem", "25"),
|
||||
Pair("Romance", "26"),
|
||||
Pair("School", "73"),
|
||||
Pair("Sci-Fi", "28"),
|
||||
Pair("Seinen", "537"),
|
||||
Pair("Shoujo", "30"),
|
||||
Pair("Shounen", "31"),
|
||||
Pair("Slice of Life", "538"),
|
||||
Pair("Space", "33"),
|
||||
Pair("Sports", "34"),
|
||||
Pair("Super Power", "75"),
|
||||
Pair("Supernatural", "76"),
|
||||
Pair("Suspense", "37"),
|
||||
Pair("Thriller", "38"),
|
||||
Pair("Vampire", "39"),
|
||||
),
|
||||
)
|
||||
|
||||
class GenreModeFilter : Filter.CheckBox("Must have all the selected genres"), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (state) {
|
||||
builder.addQueryParameter("genre_mode", "and")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val genres: List<Genre>
|
||||
get() = listOf(
|
||||
Genre("Action", "1"),
|
||||
Genre("Adventure", "78"),
|
||||
Genre("Avant Garde", "3"),
|
||||
Genre("Boys Love", "4"),
|
||||
Genre("Comedy", "5"),
|
||||
Genre("Demons", "77"),
|
||||
Genre("Drama", "6"),
|
||||
Genre("Ecchi", "7"),
|
||||
Genre("Fantasy", "79"),
|
||||
Genre("Girls Love", "9"),
|
||||
Genre("Gourmet", "10"),
|
||||
Genre("Harem", "11"),
|
||||
Genre("Horror", "530"),
|
||||
Genre("Isekai", "13"),
|
||||
Genre("Iyashikei", "531"),
|
||||
Genre("Josei", "15"),
|
||||
Genre("Kids", "532"),
|
||||
Genre("Magic", "539"),
|
||||
Genre("Mahou Shoujo", "533"),
|
||||
Genre("Martial Arts", "534"),
|
||||
Genre("Mecha", "19"),
|
||||
Genre("Military", "535"),
|
||||
Genre("Music", "21"),
|
||||
Genre("Mystery", "22"),
|
||||
Genre("Parody", "23"),
|
||||
Genre("Psychological", "536"),
|
||||
Genre("Reverse Harem", "25"),
|
||||
Genre("Romance", "26"),
|
||||
Genre("School", "73"),
|
||||
Genre("Sci-Fi", "28"),
|
||||
Genre("Seinen", "537"),
|
||||
Genre("Shoujo", "30"),
|
||||
Genre("Shounen", "31"),
|
||||
Genre("Slice of Life", "538"),
|
||||
Genre("Space", "33"),
|
||||
Genre("Sports", "34"),
|
||||
Genre("Super Power", "75"),
|
||||
Genre("Supernatural", "76"),
|
||||
Genre("Suspense", "37"),
|
||||
Genre("Thriller", "38"),
|
||||
Genre("Vampire", "39"),
|
||||
)
|
||||
class StatusFilter : UriMultiSelectFilter(
|
||||
"Status",
|
||||
"status[]",
|
||||
arrayOf(
|
||||
Pair("Completed", "completed"),
|
||||
Pair("Releasing", "releasing"),
|
||||
Pair("On Hiatus", "on_hiatus"),
|
||||
Pair("Discontinued", "discontinued"),
|
||||
Pair("Not Yet Published", "info"),
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : Group("Status", "status[]", statuses)
|
||||
class YearFilter : UriMultiSelectFilter(
|
||||
"Year",
|
||||
"year[]",
|
||||
years,
|
||||
) {
|
||||
companion object {
|
||||
private val currentYear by lazy {
|
||||
Calendar.getInstance()[Calendar.YEAR]
|
||||
}
|
||||
|
||||
private val statuses: List<Entry>
|
||||
get() = listOf(
|
||||
Entry("Completed", "completed"),
|
||||
Entry("Releasing", "releasing"),
|
||||
Entry("On Hiatus", "on_hiatus"),
|
||||
Entry("Discontinued", "discontinued"),
|
||||
Entry("Not Yet Published", "info"),
|
||||
)
|
||||
private val years: Array<Pair<String, String>> = buildList(29) {
|
||||
addAll(
|
||||
(currentYear downTo (currentYear - 20)).map(Int::toString),
|
||||
)
|
||||
|
||||
class YearFilter : Group("Year", "year[]", years)
|
||||
addAll(
|
||||
(2000 downTo 1930 step 10).map { "${it}s" },
|
||||
)
|
||||
}.map { Pair(it, it) }.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
private val years: List<Entry>
|
||||
get() = listOf(
|
||||
Entry("2023"),
|
||||
Entry("2022"),
|
||||
Entry("2021"),
|
||||
Entry("2020"),
|
||||
Entry("2019"),
|
||||
Entry("2018"),
|
||||
Entry("2017"),
|
||||
Entry("2016"),
|
||||
Entry("2015"),
|
||||
Entry("2014"),
|
||||
Entry("2013"),
|
||||
Entry("2012"),
|
||||
Entry("2011"),
|
||||
Entry("2010"),
|
||||
Entry("2009"),
|
||||
Entry("2008"),
|
||||
Entry("2007"),
|
||||
Entry("2006"),
|
||||
Entry("2005"),
|
||||
Entry("2004"),
|
||||
Entry("2003"),
|
||||
Entry("2000s"),
|
||||
Entry("1990s"),
|
||||
Entry("1980s"),
|
||||
Entry("1970s"),
|
||||
Entry("1960s"),
|
||||
Entry("1950s"),
|
||||
Entry("1940s"),
|
||||
)
|
||||
class MinChapterFilter : Filter.Text("Minimum chapter length"), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (state.isNotEmpty()) {
|
||||
val value = state.toIntOrNull()?.takeIf { it > 0 }
|
||||
?: throw IllegalArgumentException("Minimum chapter length must be a positive integer greater than 0")
|
||||
|
||||
class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
|
||||
builder.addQueryParameter("minchap", value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val chapterCounts
|
||||
get() = mapOf(
|
||||
"Any" to "",
|
||||
"At least 1 chapter" to "1",
|
||||
"At least 3 chapters" to "3",
|
||||
"At least 5 chapters" to "5",
|
||||
"At least 10 chapters" to "10",
|
||||
"At least 20 chapters" to "20",
|
||||
"At least 30 chapters" to "30",
|
||||
"At least 50 chapters" to "50",
|
||||
)
|
||||
|
||||
class SortFilter : Select("Sort", "sort", orders)
|
||||
|
||||
private val orders
|
||||
get() = mapOf(
|
||||
"Trending" to "trending",
|
||||
"Recently updated" to "recently_updated",
|
||||
"Recently added" to "recently_added",
|
||||
"Release date" to "release_date",
|
||||
"Name A-Z" to "title_az",
|
||||
"Score" to "scores",
|
||||
"MAL score" to "mal_scores",
|
||||
"Most viewed" to "most_viewed",
|
||||
"Most favourited" to "most_favourited",
|
||||
)
|
||||
class SortFilter(defaultValue: String? = null) : UriPartFilter(
|
||||
"Sort",
|
||||
"sort",
|
||||
arrayOf(
|
||||
Pair("Most relevance", "most_relevance"),
|
||||
Pair("Recently updated", "recently_updated"),
|
||||
Pair("Recently added", "recently_added"),
|
||||
Pair("Release date", "release_date"),
|
||||
Pair("Trending", "trending"),
|
||||
Pair("Name A-Z", "title_az"),
|
||||
Pair("Scores", "scores"),
|
||||
Pair("MAL scores", "mal_scores"),
|
||||
Pair("Most viewed", "most_viewed"),
|
||||
Pair("Most favourited", "most_favourited"),
|
||||
),
|
||||
defaultValue,
|
||||
)
|
||||
|
|
|
@ -5,7 +5,6 @@ import androidx.preference.PreferenceScreen
|
|||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -24,11 +23,11 @@ import okhttp3.Response
|
|||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
@ -50,72 +49,50 @@ class MangaFire(
|
|||
|
||||
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return searchMangaRequest(
|
||||
page,
|
||||
"",
|
||||
FilterList(SortFilter(defaultValue = "most_viewed")),
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return searchMangaRequest(
|
||||
page,
|
||||
"",
|
||||
FilterList(SortFilter(defaultValue = "recently_updated")),
|
||||
)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
if (query.isNotBlank()) {
|
||||
urlBuilder.addPathSegment("filter").apply {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("filter")
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
addQueryParameter("keyword", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
} else {
|
||||
urlBuilder.addPathSegment("filter").apply {
|
||||
addQueryParameter("language[]", langCode)
|
||||
addQueryParameter("page", page.toString())
|
||||
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||
when (filter) {
|
||||
is Group -> {
|
||||
filter.state.forEach {
|
||||
if (it.state) {
|
||||
addQueryParameter(filter.param, it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Select -> {
|
||||
addQueryParameter(filter.param, filter.selection)
|
||||
}
|
||||
|
||||
is GenresFilter -> {
|
||||
filter.state.forEach {
|
||||
if (it.state != 0) {
|
||||
addQueryParameter(filter.param, it.selection)
|
||||
}
|
||||
}
|
||||
if (filter.combineMode) {
|
||||
addQueryParameter("genre_mode", "and")
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
val filterList = filters.ifEmpty { getFilterList() }
|
||||
filterList.filterIsInstance<UriFilter>().forEach {
|
||||
it.addToUri(this)
|
||||
}
|
||||
}
|
||||
return GET(urlBuilder.build(), headers)
|
||||
}
|
||||
|
||||
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
|
||||
addQueryParameter("language[]", langCode)
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
|
||||
private fun searchMangaSelector() = ".original.card-lg .unit .inner"
|
||||
|
||||
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
element.selectFirst(".info > a")!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.ownText()
|
||||
}
|
||||
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
||||
thumbnail_url = it.attr("src")
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
|
@ -135,141 +112,148 @@ class MangaFire(
|
|||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
|
||||
|
||||
private fun searchMangaSelector() = ".original.card-lg .unit .inner"
|
||||
|
||||
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
element.selectFirst(".info > a")!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.ownText()
|
||||
}
|
||||
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
|
||||
}
|
||||
|
||||
// =============================== Filters ==============================
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TypeFilter(),
|
||||
GenreFilter(),
|
||||
GenreModeFilter(),
|
||||
StatusFilter(),
|
||||
YearFilter(),
|
||||
MinChapterFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
val manga = mangaDetailsParse(document)
|
||||
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
|
||||
manga.title = VOLUME_TITLE_PREFIX + manga.title
|
||||
return mangaDetailsParse(response.asJsoup()).apply {
|
||||
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
|
||||
title = VOLUME_TITLE_PREFIX + title
|
||||
}
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val root = document.selectFirst(".info")!!
|
||||
val mangaTitle = root.child(1).ownText()
|
||||
title = mangaTitle
|
||||
description = document.run {
|
||||
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
||||
when (val altTitle = root.child(2).ownText()) {
|
||||
"", mangaTitle -> description
|
||||
else -> "$description\n\nAlternative Title: $altTitle"
|
||||
with(document.selectFirst(".main-inner:not(.manga-bottom)")!!) {
|
||||
title = selectFirst("h1")!!.text()
|
||||
thumbnail_url = selectFirst(".poster img")?.attr("src")
|
||||
status = selectFirst(".info > p").parseStatus()
|
||||
description = buildString {
|
||||
document.selectFirst("#synopsis .modal-content")?.textNodes()?.let {
|
||||
append(it.joinToString("\n\n"))
|
||||
}
|
||||
|
||||
selectFirst("h6")?.let {
|
||||
append("\n\nAlternative title: ${it.text()}")
|
||||
}
|
||||
}.trim()
|
||||
|
||||
selectFirst(".meta")?.let {
|
||||
author = it.selectFirst("span:contains(Author:) + span")?.text()
|
||||
val type = it.selectFirst("span:contains(Type:) + span")?.text()
|
||||
val genres = it.selectFirst("span:contains(Genres:) + span")?.text()
|
||||
genre = listOfNotNull(type, genres).joinToString()
|
||||
}
|
||||
}
|
||||
thumbnail_url = document.selectFirst(".poster")!!.selectFirst("img")!!.attr("src")
|
||||
status = when (root.child(0).ownText()) {
|
||||
"Completed" -> SManga.COMPLETED
|
||||
"Releasing" -> SManga.ONGOING
|
||||
"On_hiatus" -> SManga.ON_HIATUS
|
||||
"Discontinued" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
with(document.selectFirst(Evaluator.Class("meta"))!!) {
|
||||
author = selectFirst("span:contains(Author:) + span")?.text()
|
||||
val type = selectFirst("span:contains(Type:) + span")?.text()
|
||||
val genres = selectFirst("span:contains(Genres:) + span")?.text()
|
||||
genre = listOfNotNull(type, genres).joinToString()
|
||||
}
|
||||
}
|
||||
|
||||
private val chapterType get() = "chapter"
|
||||
private val volumeType get() = "volume"
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"releasing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
"on_hiatus" -> SManga.ON_HIATUS
|
||||
"discontinued" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return baseUrl + chapter.url.substringBeforeLast("#")
|
||||
}
|
||||
|
||||
private fun getAjaxRequest(ajaxType: String, mangaId: String, chapterType: String): Request {
|
||||
return GET("$baseUrl/ajax/$ajaxType/$mangaId/$chapterType/$langCode", headers)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class AjaxReadDto(
|
||||
val html: String,
|
||||
)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||
Observable.fromCallable {
|
||||
val path = manga.url
|
||||
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
|
||||
val type = if (isVolume) volumeType else chapterType
|
||||
val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
|
||||
val response = client.newCall(request).execute()
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val path = manga.url
|
||||
val mangaId = path.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast(".")
|
||||
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
|
||||
|
||||
val abbrPrefix = if (isVolume) "Vol" else "Chap"
|
||||
val fullPrefix = if (isVolume) "Volume" else "Chapter"
|
||||
val linkSelector = Evaluator.Tag("a")
|
||||
parseChapterElements(response, isVolume).map { element ->
|
||||
SChapter.create().apply {
|
||||
val number = element.attr("data-number")
|
||||
chapter_number = number.toFloatOrNull() ?: -1f
|
||||
val type = if (isVolume) "volume" else "chapter"
|
||||
val abbrPrefix = if (isVolume) "Vol" else "Chap"
|
||||
val fullPrefix = if (isVolume) "Volume" else "Chapter"
|
||||
|
||||
val link = element.selectFirst(linkSelector)!!
|
||||
name = run {
|
||||
val name = link.text()
|
||||
val prefix = "$abbrPrefix $number: "
|
||||
if (!name.startsWith(prefix)) return@run name
|
||||
val realName = name.removePrefix(prefix)
|
||||
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
|
||||
}
|
||||
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
|
||||
val ajaxMangaList = client.newCall(getAjaxRequest("manga", mangaId, type))
|
||||
.execute().parseAs<ResponseDto<String>>().result
|
||||
.toBodyFragment()
|
||||
.select(if (isVolume) ".vol-list > .item" else "li")
|
||||
|
||||
val ajaxReadList = client.newCall(getAjaxRequest("read", mangaId, type))
|
||||
.execute().parseAs<ResponseDto<AjaxReadDto>>().result.html
|
||||
.toBodyFragment()
|
||||
.select("ul a")
|
||||
|
||||
val chapterList = ajaxMangaList.zip(ajaxReadList) { m, r ->
|
||||
val link = r.selectFirst("a")!!
|
||||
if (!r.attr("abs:href").toHttpUrl().pathSegments.last().contains(type)) {
|
||||
return Observable.just(emptyList())
|
||||
}
|
||||
|
||||
assert(m.attr("data-number") == r.attr("data-number")) {
|
||||
"Chapter count doesn't match. Try updating again."
|
||||
}
|
||||
|
||||
val number = m.attr("data-number")
|
||||
val dateStr = m.select("span").getOrNull(1)?.text() ?: ""
|
||||
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain("${link.attr("href")}#$type/${r.attr("data-id")}")
|
||||
chapter_number = number.toFloatOrNull() ?: -1f
|
||||
name = run {
|
||||
val name = link.text()
|
||||
val prefix = "$abbrPrefix $number: "
|
||||
if (!name.startsWith(prefix)) return@run name
|
||||
val realName = name.removePrefix(prefix)
|
||||
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
|
||||
}
|
||||
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
|
||||
}
|
||||
|
||||
private fun chapterListRequest(mangaUrl: String, type: String): Request {
|
||||
val id = mangaUrl.substringAfterLast('.')
|
||||
return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers)
|
||||
}
|
||||
|
||||
private fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
|
||||
val document = Jsoup.parse(result)
|
||||
val selector = if (isVolume) "div.unit" else "ul li"
|
||||
val elements = document.select(selector)
|
||||
if (elements.size > 0) {
|
||||
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
|
||||
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
|
||||
val type = if (isVolume) volumeType else chapterType
|
||||
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
|
||||
val response = client.newCall(request).execute()
|
||||
val res =
|
||||
json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
|
||||
val chapterInfoDocument = Jsoup.parse(res)
|
||||
val chapters = chapterInfoDocument.select("ul li")
|
||||
for ((i, it) in elements.withIndex()) {
|
||||
it.attr("data-id", chapters[i].select("a").attr("data-id"))
|
||||
date_upload = try {
|
||||
dateFormat.parse(dateStr)!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
return elements.toList()
|
||||
|
||||
return Observable.just(chapterList)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterIdsDto(
|
||||
val html: String,
|
||||
val title_format: String,
|
||||
)
|
||||
|
||||
private fun updateChapterList(manga: SManga, chapters: List<SChapter>) {
|
||||
val request = chapterListRequest(manga.url, chapterType)
|
||||
val response = client.newCall(request).execute()
|
||||
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
|
||||
val document = Jsoup.parse(result)
|
||||
|
||||
val elements = document.selectFirst(".scroll-sm")!!.children()
|
||||
val chapterCount = chapters.size
|
||||
if (elements.size != chapterCount) throw Exception("Chapter count doesn't match. Try updating again.")
|
||||
val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
|
||||
for (i in 0 until chapterCount) {
|
||||
val chapter = chapters[i]
|
||||
val element = elements[i]
|
||||
val number = element.attr("data-number").toFloatOrNull() ?: -1f
|
||||
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
|
||||
chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
|
||||
val date = element.select(Evaluator.Tag("span"))[1].ownText()
|
||||
chapter.date_upload = try {
|
||||
dateFormat.parse(date)!!.time
|
||||
} catch (_: Throwable) {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
// =============================== Pages ================================
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val typeAndId = chapter.url.substringAfterLast('#')
|
||||
|
@ -277,7 +261,7 @@ class MangaFire(
|
|||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).result
|
||||
val result = response.parseAs<ResponseDto<PageListDto>>().result
|
||||
|
||||
return result.pages.mapIndexed { index, image ->
|
||||
val url = image.url
|
||||
|
@ -298,22 +282,11 @@ class MangaFire(
|
|||
|
||||
class Image(val url: String, val offset: Int)
|
||||
|
||||
@Serializable
|
||||
class ResponseDto<T>(
|
||||
val result: T,
|
||||
val status: Int,
|
||||
)
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
TypeFilter(),
|
||||
GenresFilter(),
|
||||
StatusFilter(),
|
||||
YearFilter(),
|
||||
ChapterCountFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
// ============================ Preferences =============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
|
@ -323,7 +296,23 @@ class MangaFire(
|
|||
}.let(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
@Serializable
|
||||
class ResponseDto<T>(
|
||||
val result: T,
|
||||
)
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
private fun String.toBodyFragment(): Document {
|
||||
return Jsoup.parseBodyFragment(this, baseUrl)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
|
||||
private const val SHOW_VOLUME_PREF = "show_volume"
|
||||
|
||||
private const val VOLUME_URL_FRAGMENT = "vol"
|
||||
|
|
Loading…
Reference in New Issue