From 1b1ef9274b679718e7743c367cec778df6931e13 Mon Sep 17 00:00:00 2001 From: Secozzi <49240133+Secozzi@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:10:10 +0100 Subject: [PATCH] fix(en/mangafire): rework mangafire extension (#7625) * fix(en/mangafire): rework mangafire extension * oops * remove non-null assert * small fixes --- src/all/mangafire/build.gradle | 2 +- .../extension/all/mangafire/Filters.kt | 310 ++++++++------- .../extension/all/mangafire/MangaFire.kt | 353 +++++++++--------- 3 files changed, 339 insertions(+), 326 deletions(-) diff --git a/src/all/mangafire/build.gradle b/src/all/mangafire/build.gradle index e5ccaa62d..485abff5d 100644 --- a/src/all/mangafire/build.gradle +++ b/src/all/mangafire/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'MangaFire' extClass = '.MangaFireFactory' - extVersionCode = 8 + extVersionCode = 9 isNsfw = true } diff --git a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/Filters.kt b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/Filters.kt index a5ccc3e0e..f29d1dc9d 100644 --- a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/Filters.kt +++ b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/Filters.kt @@ -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, -) : Filter.Group(name, values) + private val param: String, + private val vals: Array>, + defaultValue: String? = null, +) : Filter.Select( + 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, -) : Filter.Select(name, valuesMap.keys.toTypedArray()) { - open val selection: String - get() = valuesMap[values[state]]!! + private val param: String, + private val vals: Array>, +) : Filter.Group(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 - 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>, +) : Filter.Group(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", 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 - 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 - 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> = 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 - 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, +) diff --git a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt index 7b429301f..37c3ea92d 100644 --- a/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt +++ b/src/all/mangafire/src/eu/kanade/tachiyomi/extension/all/mangafire/MangaFire.kt @@ -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().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 { throw UnsupportedOperationException() } - override fun fetchChapterList(manga: SManga): Observable> = - 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> { + 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>().result + .toBodyFragment() + .select(if (isVolume) ".vol-list > .item" else "li") + + val ajaxReadList = client.newCall(getAjaxRequest("read", mangaId, type)) + .execute().parseAs>().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 { - val result = json.decodeFromString>(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>(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) { - val request = chapterListRequest(manga.url, chapterType) - val response = client.newCall(request).execute() - val result = json.decodeFromString>(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 { - val result = json.decodeFromString>(response.body.string()).result + val result = response.parseAs>().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( - 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( + val result: T, + ) + + private inline fun 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"