Comicextra: Fixes and Update Filters (#6894)

* Comicextra: Fixes and Update Filters

Filter updates
- Add status filter (ongoing, completed)
- Search types can now be combined

* Update latest title selector

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* remove latest update thumbnail

* update parsers

* update date parsing

* remove non-null assert in date

* remove LEOMACS filter

* update parsers

* update urlEl

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
suhaien 2025-01-03 23:53:18 +00:00 committed by Draff
parent 6b8b650004
commit 06dd598b45
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
2 changed files with 128 additions and 128 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'ComicExtra' extName = 'ComicExtra'
extClass = '.ComicExtra' extClass = '.ComicExtra'
extVersionCode = 16 extVersionCode = 17
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -23,7 +23,7 @@ class ComicExtra : ParsedHttpSource() {
override val name = "ComicExtra" override val name = "ComicExtra"
override val baseUrl = "https://comixextra.com" override val baseUrl = "https://azcomix.me"
override val lang = "en" override val lang = "en"
@ -31,67 +31,68 @@ class ComicExtra : ParsedHttpSource() {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
override fun popularMangaSelector() = "div.cartoon-box:has(> div.mb-right)" override fun popularMangaSelector() = "div.eg-box"
override fun latestUpdatesSelector() = "div.hl-box" override fun latestUpdatesSelector() = "ul.line-list"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/popular-comic/$page", headers) override fun popularMangaRequest(page: Int) = GET("$baseUrl/popular-comics?page=$page", headers)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/comic-updates/$page", headers) override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/comic-updates?page=$page", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotBlank()) { val url = "$baseUrl/advanced-search".toHttpUrl().newBuilder().apply {
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply { if (query.isNotBlank()) addQueryParameter("key", query)
addQueryParameter("keyword", query)
if (page > 1) addQueryParameter("page", page.toString()) if (page > 1) addQueryParameter("page", page.toString())
}.build()
GET(url, headers)
} else {
var url = baseUrl
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
is GenreFilter -> url += "/${filter.toUriPart()}" is GenreGroupFilter -> {
with(filter) {
addQueryParameter("wg", included.joinToString("%20"))
addQueryParameter("wog", excluded.joinToString("%20"))
}
}
is StatusFilter -> addQueryParameter("status", filter.selected)
else -> {} else -> {}
} }
} }
GET(url + if (page > 1) "/$page" else "", headers) }.build()
} return GET(url, headers)
} }
override fun popularMangaFromElement(element: Element) = SManga.create().apply { override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select("div.mb-right > h3 > a").attr("href")) setUrlWithoutDomain(element.selectFirst("a.eg-image")!!.absUrl("href"))
title = element.select("div.mb-right > h3 > a").text() title = element.selectFirst("div.egb-right a")!!.text()
thumbnail_url = element.select("img").attr("src") element.selectFirst("img")?.also { thumbnail_url = it.absUrl("src") }
} }
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select("div.hlb-t > a").attr("href")) setUrlWithoutDomain(element.selectFirst("a.big-link")!!.absUrl("href"))
title = element.select("div.hlb-t > a").text() title = element.selectFirst(".big-link")!!.text()
thumbnail_url = fetchThumbnailURL(element.select("div.hlb-t > a").attr("href")) thumbnail_url = "https://azcomix.me/images/sites/default.jpg"
} }
private fun fetchThumbnailURL(url: String) = client.newCall(GET(url, headers)).execute().asJsoup().select("div.movie-l-img > img").attr("src")
private fun fetchPagesFromNav(url: String) = client.newCall(GET(url, headers)).execute().asJsoup()
override fun popularMangaNextPageSelector() = "div.general-nav > a:contains(Next)" override fun popularMangaNextPageSelector() = "div.general-nav > a:contains(Next)"
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = "div.dl-box"
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a.dlb-image")!!.absUrl("href"))
title = element.selectFirst("div.dlb-right a.dlb-title")!!.text()
element.selectFirst("a.dlb-image img")?.also { thumbnail_url = it.absUrl("src") }
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply { return SManga.create().apply {
title = document.select("div.movie-detail span.title-1").text() title = document.selectFirst("h1")!!.text()
thumbnail_url = document.select("div.movie-l-img > img").attr("src") thumbnail_url = document.selectFirst("div.anime-image > img")?.absUrl("src")
status = parseStatus(document.select("dt:contains(Status:) + dd").text()) document.selectFirst(".status a")?.also { status = parseStatus(it.text()) }
author = document.select("dt:contains(Author:) + dd").text() document.selectFirst("td:contains(Author:) + td")?.also { author = it.text() }
description = document.select("div#film-content").text() document.selectFirst("div.detail-desc-content p")?.also { description = it.text() }
genre = document.select("dt.movie-dt:contains(Genres:) + dd a").joinToString { it.text() } genre = document.select("ul.anime-genres > li + li").joinToString { it.text() }
} }
} }
@ -103,46 +104,28 @@ class ComicExtra : ParsedHttpSource() {
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
val nav = document.getElementsByClass("general-nav").first()
val chapters = ArrayList<SChapter>() val chapters = ArrayList<SChapter>()
document.select(chapterListSelector()).forEach { document.select(chapterListSelector()).forEach {
chapters.add(chapterFromElement(it)) chapters.add(chapterFromElement(it))
} }
if (nav == null) {
return chapters return chapters
} }
val pg2url = nav.select("a:contains(next)").attr("href") override fun chapterListSelector() = "ul.basic-list li"
// recursively build the chapter list
fun parseChapters(nextURL: String) {
val newpage = fetchPagesFromNav(nextURL)
newpage.select(chapterListSelector()).forEach {
chapters.add(chapterFromElement(it))
}
val newURL = newpage.select(".general-nav a:contains(next)")?.attr("href")
if (!newURL.isNullOrBlank()) parseChapters(newURL)
}
parseChapters(pg2url)
return chapters
}
override fun chapterListSelector() = "table.table > tbody#list > tr:has(td)"
override fun chapterFromElement(element: Element): SChapter { override fun chapterFromElement(element: Element): SChapter {
val urlEl = element.select("td:nth-of-type(1) > a").first()!! val urlEl = element.selectFirst("a")
val dateEl = element.select("td:nth-of-type(2)") val dateEl = element.selectFirst("span")
val chapter = SChapter.create() return SChapter.create().apply {
chapter.setUrlWithoutDomain(urlEl.attr("href").replace(" ", "%20")) urlEl!!.also {
chapter.name = urlEl.text() setUrlWithoutDomain(it.absUrl("href").replace(" ", "%20"))
chapter.date_upload = dateParse(dateEl.text()) name = it.text()
return chapter }
dateEl?.also { date_upload = dateParse(it.text()) }
}
} }
private fun dateParse(dateAsString: String): Long { private fun dateParse(dateAsString: String): Long {
@ -159,7 +142,7 @@ class ComicExtra : ParsedHttpSource() {
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
document.select("div.chapter-container img").forEachIndexed { i, img -> document.select("div.chapter-container img").forEachIndexed { i, img ->
pages.add(Page(i, "", img.attr("abs:src"))) pages.add(Page(i, "", img.absUrl("src")))
} }
return pages return pages
} }
@ -169,75 +152,92 @@ class ComicExtra : ParsedHttpSource() {
// Filters // Filters
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
Filter.Header("Note: can't combine search types"), Filter.Header("Note: can't leave both filters as default with a blank search string"),
Filter.Separator(), Filter.Separator(),
GenreFilter(getGenreList), StatusFilter(getStatusList, 0),
GenreGroupFilter(getGenreList()),
) )
private class GenreFilter(genrePairs: Array<Pair<String, String>>) : UriPartFilter("Category", genrePairs) class SelectFilterOption(val name: String, val value: String)
class TriStateFilterOption(val value: String, name: String, default: Int = 0) : Filter.TriState(name, default)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) : abstract class SelectFilter(name: String, private val options: List<SelectFilterOption>, default: Int = 0) : Filter.Select<String>(name, options.map { it.name }.toTypedArray(), default) {
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { val selected: String
fun toUriPart() = vals[state].second get() = options[state].value
} }
private val getGenreList = arrayOf( abstract class TriStateGroupFilter(name: String, options: List<TriStateFilterOption>) : Filter.Group<TriStateFilterOption>(name, options) {
Pair("Action", "action-comic"), val included: List<String>
Pair("Adventure", "adventure-comic"), get() = state.filter { it.isIncluded() }.map { it.value }
Pair("Anthology", "anthology-comic"),
Pair("Anthropomorphic", "anthropomorphic-comic"), val excluded: List<String>
Pair("Biography", "biography-comic"), get() = state.filter { it.isExcluded() }.map { it.value }
Pair("Black Mask Studios", "black-mask-studios-comic"), }
Pair("Children", "children-comic"),
Pair("Comedy", "comedy-comic"), class StatusFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Status", options, default)
Pair("Crime", "crime-comic"), class GenreGroupFilter(options: List<TriStateFilterOption>) : TriStateGroupFilter("Genre", options)
Pair("DC Comics", "dc-comics-comic"),
Pair("Dark Horse", "dark-horse-comic"), private val getStatusList = listOf(
Pair("Drama", "drama-comic"), SelectFilterOption("All", ""),
Pair("Family", "family-comic"), SelectFilterOption("Ongoing", "ONG"),
Pair("Fantasy", "fantasy-comic"), SelectFilterOption("Completed", "CMP"),
Pair("Fighting", "fighting-comic"), )
Pair("First Second Books", "first-second-books-comic"),
Pair("Graphic Novels", "graphic-novels-comic"), private fun getGenreList() = listOf(
Pair("Historical", "historical-comic"), TriStateFilterOption("Action", "Action"),
Pair("Horror", "horror-comic"), TriStateFilterOption("Adventure", "Adventure"),
Pair("LEOMACS", "a><span-class=-comic"), TriStateFilterOption("Anthology", "Anthology"),
Pair("LGBTQ", "lgbtq-comic"), TriStateFilterOption("Anthropomorphic", "Anthropomorphic"),
Pair("Leading Ladies", "leading-ladies-comic"), TriStateFilterOption("Biography", "Biography"),
Pair("Literature", "literature-comic"), TriStateFilterOption("Children", "Children"),
Pair("Manga", "manga-comic"), TriStateFilterOption("Comedy", "Comedy"),
Pair("Martial Arts", "martial-arts-comic"), TriStateFilterOption("Crime", "Crime"),
Pair("Marvel", "marvel-comic"), TriStateFilterOption("Cyborgs", "Cyborgs"),
Pair("Mature", "mature-comic"), TriStateFilterOption("DC Comics", "DC Comics"),
Pair("Military", "military-comic"), TriStateFilterOption("Dark Horse", "Dark Horse"),
Pair("Movie Cinematic Link", "movie-cinematic-link-comic"), TriStateFilterOption("Demons", "Demons"),
Pair("Movies & TV", "movies-&-tv-comic"), TriStateFilterOption("Drama", "Drama"),
Pair("Music", "music-comic"), TriStateFilterOption("Family", "Family"),
Pair("Mystery", "mystery-comic"), TriStateFilterOption("Fantasy", "Fantasy"),
Pair("Mythology", "mythology-comic"), TriStateFilterOption("Fighting", "Fighting"),
Pair("New", "new-comic"), TriStateFilterOption("Gore", "Gore"),
Pair("Personal", "personal-comic"), TriStateFilterOption("Graphic Novels", "Graphic Novels"),
Pair("Political", "political-comic"), TriStateFilterOption("Historical", "Historical"),
Pair("Post-Apocalyptic", "post-apocalyptic-comic"), TriStateFilterOption("Horror", "Horror"),
Pair("Psychological", "psychological-comic"), TriStateFilterOption("Leading Ladies", "Leading Ladies"),
Pair("Pulp", "pulp-comic"), TriStateFilterOption("Literature", "Literature"),
Pair("Religious", "religious-comic"), TriStateFilterOption("Magic", "Magic"),
Pair("Robots", "robots-comic"), TriStateFilterOption("Manga", "Manga"),
Pair("Romance", "romance-comic"), TriStateFilterOption("Martial Arts", "Martial Arts"),
Pair("School Life", "school-life-comic"), TriStateFilterOption("Marvel", "Marvel"),
Pair("Sci-Fi", "sci-fi-comic"), TriStateFilterOption("Mature", "Mature"),
Pair("Slice of Life", "slice-of-life-comic"), TriStateFilterOption("Mecha", "Mecha"),
Pair("Sport", "sport-comic"), TriStateFilterOption("Military", "Military"),
Pair("Spy", "spy-comic"), TriStateFilterOption("Movie Cinematic Link", "Movie Cinematic Link"),
Pair("Superhero", "superhero-comic"), TriStateFilterOption("Mystery", "Mystery"),
Pair("Supernatural", "supernatural-comic"), TriStateFilterOption("Mythology", "Mythology"),
Pair("Suspense", "suspense-comic"), TriStateFilterOption("Personal", "Personal"),
Pair("Thriller", "thriller-comic"), TriStateFilterOption("Political", "Political"),
Pair("Vampires", "vampires-comic"), TriStateFilterOption("Post-Apocalyptic", "Post-Apocalyptic"),
Pair("Video Games", "video-games-comic"), TriStateFilterOption("Psychological", "Psychological"),
Pair("War", "war-comic"), TriStateFilterOption("Pulp", "Pulp"),
Pair("Western", "western-comic"), TriStateFilterOption("Robots", "Robots"),
Pair("Zombies", "zombies-comic"), TriStateFilterOption("Romance", "Romance"),
Pair("Zulema Scotto Lavina", "zulema-scotto-lavina-comic"), TriStateFilterOption("Sci-Fi", "Sci-Fi"),
TriStateFilterOption("Science Fiction", "Science Fiction"),
TriStateFilterOption("Slice of Life", "Slice of Life"),
TriStateFilterOption("Sports", "Sports"),
TriStateFilterOption("Spy", "Spy"),
TriStateFilterOption("Superhero", "Superhero"),
TriStateFilterOption("Supernatural", "Supernatural"),
TriStateFilterOption("Suspense", "Suspense"),
TriStateFilterOption("Thriller", "Thriller"),
TriStateFilterOption("Tragedy", "Tragedy"),
TriStateFilterOption("Vampires", "Vampires"),
TriStateFilterOption("Vertigo", "Vertigo"),
TriStateFilterOption("Video Games", "Video Games"),
TriStateFilterOption("War", "War"),
TriStateFilterOption("Western", "Western"),
TriStateFilterOption("Zombies", "Zombies"),
) )
} }