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:
Secozzi 2025-02-14 00:10:10 +01:00 committed by Draff
parent 17300b3bd0
commit 1b1ef9274b
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 339 additions and 326 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'MangaFire' extName = 'MangaFire'
extClass = '.MangaFireFactory' extClass = '.MangaFireFactory'
extVersionCode = 8 extVersionCode = 9
isNsfw = true isNsfw = true
} }

View File

@ -1,166 +1,190 @@
package eu.kanade.tachiyomi.extension.all.mangafire package eu.kanade.tachiyomi.extension.all.mangafire
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
import java.util.Calendar
class Entry(name: String, val id: String) : Filter.CheckBox(name) { interface UriFilter {
constructor(name: String) : this(name, name) fun addToUri(builder: HttpUrl.Builder)
} }
sealed class Group( open class UriPartFilter(
name: String, name: String,
val param: String, private val param: String,
values: List<Entry>, private val vals: Array<Pair<String, String>>,
) : Filter.Group<Entry>(name, values) 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, name: String,
val param: String, private val param: String,
private val valuesMap: Map<String, String>, private val vals: Array<Pair<String, String>>,
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) { ) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
open val selection: String override fun addToUri(builder: HttpUrl.Builder) {
get() = valuesMap[values[state]]!! 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> open class UriTriSelectFilter(
get() = listOf( name: String,
Entry("Manga", "manga"), private val param: String,
Entry("One-Shot", "one_shot"), private val vals: Array<Pair<String, String>>,
Entry("Doujinshi", "doujinshi"), ) : Filter.Group<UriTriSelectOption>(name, vals.map { UriTriSelectOption(it.first, it.second) }), UriFilter {
Entry("Light-Novel", "light_novel"), override fun addToUri(builder: HttpUrl.Builder) {
Entry("Novel", "novel"), state.forEach { s ->
Entry("Manhwa", "manhwa"), when (s.state) {
Entry("Manhua", "manhua"), TriState.STATE_INCLUDE -> builder.addQueryParameter(param, s.value)
) TriState.STATE_EXCLUDE -> builder.addQueryParameter(param, "-${s.value}")
}
class Genre(name: String, val id: String) : Filter.TriState(name) { }
val selection: String }
get() = (if (isExcluded()) "-" else "") + id
} }
class GenresFilter : Filter.Group<Genre>("Genre", genres) { class TypeFilter : UriPartFilter(
val param = "genre[]" "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 class GenreFilter : UriTriSelectFilter(
get() = state.filter { !it.isIgnored() }.size > 1 "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> class StatusFilter : UriMultiSelectFilter(
get() = listOf( "Status",
Genre("Action", "1"), "status[]",
Genre("Adventure", "78"), arrayOf(
Genre("Avant Garde", "3"), Pair("Completed", "completed"),
Genre("Boys Love", "4"), Pair("Releasing", "releasing"),
Genre("Comedy", "5"), Pair("On Hiatus", "on_hiatus"),
Genre("Demons", "77"), Pair("Discontinued", "discontinued"),
Genre("Drama", "6"), Pair("Not Yet Published", "info"),
Genre("Ecchi", "7"), ),
Genre("Fantasy", "79"), )
Genre("Girls Love", "9"),
Genre("Gourmet", "10"), class YearFilter : UriMultiSelectFilter(
Genre("Harem", "11"), "Year",
Genre("Horror", "530"), "year[]",
Genre("Isekai", "13"), years,
Genre("Iyashikei", "531"), ) {
Genre("Josei", "15"), companion object {
Genre("Kids", "532"), private val currentYear by lazy {
Genre("Magic", "539"), Calendar.getInstance()[Calendar.YEAR]
Genre("Mahou Shoujo", "533"), }
Genre("Martial Arts", "534"),
Genre("Mecha", "19"), private val years: Array<Pair<String, String>> = buildList(29) {
Genre("Military", "535"), addAll(
Genre("Music", "21"), (currentYear downTo (currentYear - 20)).map(Int::toString),
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 : Group("Status", "status[]", statuses) addAll(
(2000 downTo 1930 step 10).map { "${it}s" },
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"),
) )
}.map { Pair(it, it) }.toTypedArray()
}
}
class YearFilter : Group("Year", "year[]", years) 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")
private val years: List<Entry> builder.addQueryParameter("minchap", value.toString())
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 ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts) class SortFilter(defaultValue: String? = null) : UriPartFilter(
"Sort",
private val chapterCounts "sort",
get() = mapOf( arrayOf(
"Any" to "", Pair("Most relevance", "most_relevance"),
"At least 1 chapter" to "1", Pair("Recently updated", "recently_updated"),
"At least 3 chapters" to "3", Pair("Recently added", "recently_added"),
"At least 5 chapters" to "5", Pair("Release date", "release_date"),
"At least 10 chapters" to "10", Pair("Trending", "trending"),
"At least 20 chapters" to "20", Pair("Name A-Z", "title_az"),
"At least 30 chapters" to "30", Pair("Scores", "scores"),
"At least 50 chapters" to "50", Pair("MAL scores", "mal_scores"),
) Pair("Most viewed", "most_viewed"),
Pair("Most favourited", "most_favourited"),
class SortFilter : Select("Sort", "sort", orders) ),
defaultValue,
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",
)

View File

@ -5,7 +5,6 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
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.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -24,11 +23,11 @@ import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -50,72 +49,50 @@ class MangaFire(
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build() override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
override fun popularMangaRequest(page: Int) = // ============================== Popular ===============================
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(defaultValue = "most_viewed")),
)
}
override fun popularMangaParse(response: Response) = searchMangaParse(response) override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int) = // =============================== Latest ===============================
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(defaultValue = "recently_updated")),
)
}
override fun latestUpdatesParse(response: Response) = searchMangaParse(response) override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder() val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("filter")
if (query.isNotBlank()) { if (query.isNotBlank()) {
urlBuilder.addPathSegment("filter").apply {
addQueryParameter("keyword", query) addQueryParameter("keyword", query)
addQueryParameter("page", page.toString())
} }
} else {
urlBuilder.addPathSegment("filter").apply { val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
addQueryParameter("language[]", langCode) addQueryParameter("language[]", langCode)
addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
filters.ifEmpty(::getFilterList).forEach { filter -> }.build()
when (filter) {
is Group -> {
filter.state.forEach {
if (it.state) {
addQueryParameter(filter.param, it.id)
}
}
}
is Select -> { return GET(url, headers)
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 -> {}
}
}
}
}
return GET(urlBuilder.build(), headers)
}
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()
}
element.selectFirst(Evaluator.Tag("img"))!!.let {
thumbnail_url = it.attr("src")
}
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
@ -135,68 +112,128 @@ class MangaFire(
return MangasPage(entries, hasNextPage) 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 getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup() return mangaDetailsParse(response.asJsoup()).apply {
val manga = mangaDetailsParse(document)
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) { if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
manga.title = VOLUME_TITLE_PREFIX + manga.title title = VOLUME_TITLE_PREFIX + title
}
} }
return manga
} }
private fun mangaDetailsParse(document: Document) = SManga.create().apply { private fun mangaDetailsParse(document: Document) = SManga.create().apply {
val root = document.selectFirst(".info")!! with(document.selectFirst(".main-inner:not(.manga-bottom)")!!) {
val mangaTitle = root.child(1).ownText() title = selectFirst("h1")!!.text()
title = mangaTitle thumbnail_url = selectFirst(".poster img")?.attr("src")
description = document.run { status = selectFirst(".info > p").parseStatus()
val description = selectFirst(Evaluator.Class("description"))!!.ownText() description = buildString {
when (val altTitle = root.child(2).ownText()) { document.selectFirst("#synopsis .modal-content")?.textNodes()?.let {
"", mangaTitle -> description append(it.joinToString("\n\n"))
else -> "$description\n\nAlternative Title: $altTitle"
} }
selectFirst("h6")?.let {
append("\n\nAlternative title: ${it.text()}")
} }
thumbnail_url = document.selectFirst(".poster")!!.selectFirst("img")!!.attr("src") }.trim()
status = when (root.child(0).ownText()) {
"Completed" -> SManga.COMPLETED selectFirst(".meta")?.let {
"Releasing" -> SManga.ONGOING author = it.selectFirst("span:contains(Author:) + span")?.text()
"On_hiatus" -> SManga.ON_HIATUS val type = it.selectFirst("span:contains(Type:) + span")?.text()
"Discontinued" -> SManga.CANCELLED val genres = it.selectFirst("span:contains(Genres:) + span")?.text()
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() genre = listOfNotNull(type, genres).joinToString()
} }
} }
}
private val chapterType get() = "chapter" private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
private val volumeType get() = "volume" "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> { override fun chapterListParse(response: Response): List<SChapter> {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
Observable.fromCallable {
val path = manga.url val path = manga.url
val mangaId = path.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast(".")
val isVolume = path.endsWith(VOLUME_URL_SUFFIX) 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()
val type = if (isVolume) "volume" else "chapter"
val abbrPrefix = if (isVolume) "Vol" else "Chap" val abbrPrefix = if (isVolume) "Vol" else "Chap"
val fullPrefix = if (isVolume) "Volume" else "Chapter" 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 link = element.selectFirst(linkSelector)!! 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 { name = run {
val name = link.text() val name = link.text()
val prefix = "$abbrPrefix $number: " val prefix = "$abbrPrefix $number: "
@ -204,80 +241,27 @@ class MangaFire(
val realName = name.removePrefix(prefix) val realName = name.removePrefix(prefix)
if (realName.contains(number)) realName else "$fullPrefix $number: $realName" if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
} }
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
}
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
}
private fun chapterListRequest(mangaUrl: String, type: String): Request { date_upload = try {
val id = mangaUrl.substringAfterLast('.') dateFormat.parse(dateStr)!!.time
return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers) } catch (_: ParseException) {
} 0L
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"))
}
}
return elements.toList()
}
@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 { return Observable.just(chapterList)
throw UnsupportedOperationException()
} }
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val typeAndId = chapter.url.substringAfterLast('#') val typeAndId = chapter.url.substringAfterLast('#')
return GET("$baseUrl/ajax/read/$typeAndId", headers) return GET("$baseUrl/ajax/read/$typeAndId", headers)
} }
override fun pageListParse(response: Response): List<Page> { 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 -> return result.pages.mapIndexed { index, image ->
val url = image.url val url = image.url
@ -298,22 +282,11 @@ class MangaFire(
class Image(val url: String, val offset: Int) class Image(val url: String, val offset: Int)
@Serializable override fun imageUrlParse(response: Response): String {
class ResponseDto<T>( throw UnsupportedOperationException()
val result: T, }
val status: Int,
)
override fun getFilterList() = FilterList( // ============================ Preferences =============================
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
TypeFilter(),
GenresFilter(),
StatusFilter(),
YearFilter(),
ChapterCountFilter(),
SortFilter(),
)
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
@ -323,7 +296,23 @@ class MangaFire(
}.let(screen::addPreference) }.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 { companion object {
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
private const val SHOW_VOLUME_PREF = "show_volume" private const val SHOW_VOLUME_PREF = "show_volume"
private const val VOLUME_URL_FRAGMENT = "vol" private const val VOLUME_URL_FRAGMENT = "vol"