This commit is contained in:
AwkwardPeak7 2025-02-02 13:35:52 +05:00 committed by Draff
parent e2c7543d3e
commit 2e45708568
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
11 changed files with 258 additions and 323 deletions

View File

@ -109,7 +109,9 @@ abstract class Madara(
protected open val useLoadMoreRequest = LoadMoreStrategy.AutoDetect protected open val useLoadMoreRequest = LoadMoreStrategy.AutoDetect
enum class LoadMoreStrategy { enum class LoadMoreStrategy {
AutoDetect, Always, Never AutoDetect,
Always,
Never,
} }
/** /**
@ -118,7 +120,9 @@ abstract class Madara(
private var loadMoreRequestDetected = LoadMoreDetection.Pending private var loadMoreRequestDetected = LoadMoreDetection.Pending
private enum class LoadMoreDetection { private enum class LoadMoreDetection {
Pending, True, False Pending,
True,
False,
} }
protected fun detectLoadMore(document: Document) { protected fun detectLoadMore(document: Document) {
@ -132,12 +136,10 @@ abstract class Madara(
} }
} }
protected fun useLoadMoreRequest(): Boolean { protected fun useLoadMoreRequest(): Boolean = when (useLoadMoreRequest) {
return when (useLoadMoreRequest) { LoadMoreStrategy.Always -> true
LoadMoreStrategy.Always -> true LoadMoreStrategy.Never -> false
LoadMoreStrategy.Never -> false else -> loadMoreRequestDetected == LoadMoreDetection.True
else -> loadMoreRequestDetected == LoadMoreDetection.True
}
} }
// Popular Manga // Popular Manga
@ -176,19 +178,17 @@ abstract class Madara(
return manga return manga
} }
override fun popularMangaRequest(page: Int): Request = override fun popularMangaRequest(page: Int): Request = if (useLoadMoreRequest()) {
if (useLoadMoreRequest()) { loadMoreRequest(page, popular = true)
loadMoreRequest(page, popular = true) } else {
} else { GET("$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=views", headers)
GET("$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=views", headers) }
}
override fun popularMangaNextPageSelector(): String? = override fun popularMangaNextPageSelector(): String? = if (useLoadMoreRequest()) {
if (useLoadMoreRequest()) { "body:not(:has(.no-posts))"
"body:not(:has(.no-posts))" } else {
} else { "div.nav-previous, nav.navigation-ajax, a.nextpostslink"
"div.nav-previous, nav.navigation-ajax, a.nextpostslink" }
}
// Latest Updates // Latest Updates
@ -199,12 +199,11 @@ abstract class Madara(
return popularMangaFromElement(element) return popularMangaFromElement(element)
} }
override fun latestUpdatesRequest(page: Int): Request = override fun latestUpdatesRequest(page: Int): Request = if (useLoadMoreRequest()) {
if (useLoadMoreRequest()) { loadMoreRequest(page, popular = false)
loadMoreRequest(page, popular = false) } else {
} else { GET("$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=latest", headers)
GET("$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=latest", headers) }
}
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector() override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
@ -251,20 +250,16 @@ abstract class Madara(
return super.fetchSearchManga(page, query, filters) return super.fetchSearchManga(page, query, filters)
} }
protected open fun searchPage(page: Int): String { protected open fun searchPage(page: Int): String = if (page == 1) {
return if (page == 1) { ""
"" } else {
} else { "page/$page/"
"page/$page/"
}
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = if (useLoadMoreRequest()) {
return if (useLoadMoreRequest()) { searchLoadMoreRequest(page, query, filters)
searchLoadMoreRequest(page, query, filters) } else {
} else { searchRequest(page, query, filters)
searchRequest(page, query, filters)
}
} }
protected open fun searchRequest(page: Int, query: String, filters: FilterList): Request { protected open fun searchRequest(page: Int, query: String, filters: FilterList): Request {
@ -310,7 +305,9 @@ abstract class Madara(
filter.state filter.state
.filter { it.state } .filter { it.state }
.let { list -> .let { list ->
if (list.isNotEmpty()) { list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) } } if (list.isNotEmpty()) {
list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) }
}
} }
} }
else -> {} else -> {}
@ -489,8 +486,7 @@ abstract class Madara(
intl["adult_content_filter_only"] to "1", intl["adult_content_filter_only"] to "1",
) )
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>, state: Int = 0) : open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>, state: Int = 0) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
} }
@ -499,21 +495,21 @@ abstract class Madara(
protected class AuthorFilter(title: String) : Filter.Text(title) protected class AuthorFilter(title: String) : Filter.Text(title)
protected class ArtistFilter(title: String) : Filter.Text(title) protected class ArtistFilter(title: String) : Filter.Text(title)
protected class YearFilter(title: String) : Filter.Text(title) protected class YearFilter(title: String) : Filter.Text(title)
protected class StatusFilter(title: String, status: List<Tag>) : protected class StatusFilter(title: String, status: List<Tag>) : Filter.Group<Tag>(title, status)
Filter.Group<Tag>(title, status)
protected class OrderByFilter(title: String, options: List<Pair<String, String>>, state: Int = 0) : protected class OrderByFilter(title: String, options: List<Pair<String, String>>, state: Int = 0) : UriPartFilter(title, options.toTypedArray(), state)
UriPartFilter(title, options.toTypedArray(), state)
protected class GenreConditionFilter(title: String, options: List<Pair<String, String>>) : UriPartFilter( protected class GenreConditionFilter(title: String, options: List<Pair<String, String>>) :
title, UriPartFilter(
options.toTypedArray(), title,
) options.toTypedArray(),
)
protected class AdultContentFilter(title: String, options: List<Pair<String, String>>) : UriPartFilter( protected class AdultContentFilter(title: String, options: List<Pair<String, String>>) :
title, UriPartFilter(
options.toTypedArray(), title,
) options.toTypedArray(),
)
protected class GenreList(title: String, genres: List<Genre>) : Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) }) protected class GenreList(title: String, genres: List<Genre>) : Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) })
class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name) class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
@ -757,32 +753,24 @@ abstract class Madara(
open val altName = intl["alt_names_heading"] open val altName = intl["alt_names_heading"]
open val updatingRegex = "Updating|Atualizando".toRegex(RegexOption.IGNORE_CASE) open val updatingRegex = "Updating|Atualizando".toRegex(RegexOption.IGNORE_CASE)
fun String.notUpdating(): Boolean { fun String.notUpdating(): Boolean = this.contains(updatingRegex).not()
return this.contains(updatingRegex).not()
}
private fun String.containsIn(array: Array<String>): Boolean { private fun String.containsIn(array: Array<String>): Boolean = this.lowercase() in array.map { it.lowercase() }
return this.lowercase() in array.map { it.lowercase() }
}
protected open fun imageFromElement(element: Element): String? { protected open fun imageFromElement(element: Element): String? = when {
return when { element.hasAttr("data-src") -> element.attr("abs:data-src")
element.hasAttr("data-src") -> element.attr("abs:data-src") element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src")
element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src") element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage()
element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage() element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc")
element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc") else -> element.attr("abs:src")
else -> element.attr("abs:src")
}
} }
/** /**
* Get the best image quality available from srcset * Get the best image quality available from srcset
*/ */
protected fun String.getSrcSetImage(): String? { protected fun String.getSrcSetImage(): String? = this.split(" ")
return this.split(" ") .filter(URL_REGEX::matches)
.filter(URL_REGEX::matches) .maxOfOrNull(String::toString)
.maxOfOrNull(String::toString)
}
/** /**
* Set it to true if the source uses the new AJAX endpoint to * Set it to true if the source uses the new AJAX endpoint to
@ -807,9 +795,7 @@ abstract class Madara(
return POST("$baseUrl/wp-admin/admin-ajax.php", xhrHeaders, form) return POST("$baseUrl/wp-admin/admin-ajax.php", xhrHeaders, form)
} }
protected open fun xhrChaptersRequest(mangaUrl: String): Request { protected open fun xhrChaptersRequest(mangaUrl: String): Request = POST("$mangaUrl/ajax/chapters", xhrHeaders)
return POST("$mangaUrl/ajax/chapters", xhrHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
@ -880,12 +866,10 @@ abstract class Madara(
open fun parseChapterDate(date: String?): Long { open fun parseChapterDate(date: String?): Long {
date ?: return 0 date ?: return 0
fun SimpleDateFormat.tryParse(string: String): Long { fun SimpleDateFormat.tryParse(string: String): Long = try {
return try { parse(string)?.time ?: 0
parse(string)?.time ?: 0 } catch (_: ParseException) {
} catch (_: ParseException) { 0
0
}
} }
return when { return when {
@ -1006,9 +990,7 @@ abstract class Madara(
} }
} }
override fun imageRequest(page: Page): Request { override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers.newBuilder().set("Referer", page.url).build())
return GET(page.imageUrl!!, headers.newBuilder().set("Referer", page.url).build())
}
override fun imageUrlParse(document: Document) = "" override fun imageUrlParse(document: Document) = ""
@ -1083,26 +1065,22 @@ abstract class Madara(
/** /**
* The request to the search page (or another one) that have the genres list. * The request to the search page (or another one) that have the genres list.
*/ */
protected open fun genresRequest(): Request { protected open fun genresRequest(): Request = GET("$baseUrl/?s=genre&post_type=wp-manga", headers)
return GET("$baseUrl/?s=genre&post_type=wp-manga", headers)
}
/** /**
* Get the genres from the search page document. * Get the genres from the search page document.
* *
* @param document The search page document * @param document The search page document
*/ */
protected open fun parseGenres(document: Document): List<Genre> { protected open fun parseGenres(document: Document): List<Genre> = document.selectFirst("div.checkbox-group")
return document.selectFirst("div.checkbox-group") ?.select("div.checkbox")
?.select("div.checkbox") .orEmpty()
.orEmpty() .map { li ->
.map { li -> Genre(
Genre( li.selectFirst("label")!!.text(),
li.selectFirst("label")!!.text(), li.selectFirst("input[type=checkbox]")!!.`val`(),
li.selectFirst("input[type=checkbox]")!!.`val`(), )
) }
}
}
// https://stackoverflow.com/a/66614516 // https://stackoverflow.com/a/66614516
protected fun String.decodeHex(): ByteArray { protected fun String.decodeHex(): ByteArray {

View File

@ -6,13 +6,11 @@ import org.jsoup.nodes.Element
class CoffeeManga : Madara("Coffee Manga", "https://coffeemanga.io", "en") { class CoffeeManga : Madara("Coffee Manga", "https://coffeemanga.io", "en") {
override val useNewChapterEndpoint = false override val useNewChapterEndpoint = false
override fun imageFromElement(element: Element): String? { override fun imageFromElement(element: Element): String? = when {
return when { element.attr("data-src").isNotBlank() -> element.attr("abs:data-src")
element.attr("data-src").isNotBlank() -> element.attr("abs:data-src") element.attr("data-lazy-src").isNotBlank() -> element.attr("abs:data-lazy-src")
element.attr("data-lazy-src").isNotBlank() -> element.attr("abs:data-lazy-src") element.attr("srcset").isNotBlank() -> element.attr("abs:srcset").getSrcSetImage()
element.attr("srcset").isNotBlank() -> element.attr("abs:srcset").getSrcSetImage() element.attr("data-cfsrc").isNotBlank() -> element.attr("abs:data-cfsrc")
element.attr("data-cfsrc").isNotBlank() -> element.attr("abs:data-cfsrc") else -> element.attr("abs:src")
else -> element.attr("abs:src")
}
} }
} }

View File

@ -2,11 +2,12 @@ package eu.kanade.tachiyomi.extension.en.luascans
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms import eu.kanade.tachiyomi.multisrc.heancms.HeanCms
class LuaScans : HeanCms( class LuaScans :
"Lua Scans", HeanCms(
"https://luacomic.org", "Lua Scans",
"en", "https://luacomic.org",
) { "en",
) {
// Moved from Keyoapp to HeanCms // Moved from Keyoapp to HeanCms
override val versionId = 3 override val versionId = 3

View File

@ -2,11 +2,12 @@ package eu.kanade.tachiyomi.extension.en.retsu
import eu.kanade.tachiyomi.multisrc.madara.Madara import eu.kanade.tachiyomi.multisrc.madara.Madara
class Retsu : Madara( class Retsu :
"Retsu", Madara(
"https://retsu.org", "Retsu",
"en", "https://retsu.org",
) { "en",
) {
override fun popularMangaSelector() = "div.manga__item" override fun popularMangaSelector() = "div.manga__item"
override val popularMangaUrlSelector = "h4 a" override val popularMangaUrlSelector = "h4 a"

View File

@ -12,12 +12,13 @@ import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class SirenKomik : MangaThemesia( class SirenKomik :
"Siren Komik", MangaThemesia(
"https://sirenkomik.my.id", "Siren Komik",
"id", "https://sirenkomik.my.id",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("id")), "id",
) { dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("id")),
) {
override val id = 8457447675410081142 override val id = 8457447675410081142
override val hasProjectPage = true override val hasProjectPage = true

View File

@ -32,7 +32,9 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class SussyToons : HttpSource(), ConfigurableSource { class SussyToons :
HttpSource(),
ConfigurableSource {
override val name = "Sussy Toons" override val name = "Sussy Toons"
@ -102,9 +104,7 @@ class SussyToons : HttpSource(), ConfigurableSource {
// ============================= Popular ================================== // ============================= Popular ==================================
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request = GET("$apiUrl/obras/top5", headers)
return GET("$apiUrl/obras/top5", headers)
}
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<WrapperDto<List<MangaDto>>>() val dto = response.parseAs<WrapperDto<List<MangaDto>>>()
@ -152,8 +152,7 @@ class SussyToons : HttpSource(), ConfigurableSource {
return GET(url, headers) return GET(url, headers)
} }
override fun mangaDetailsParse(response: Response) = override fun mangaDetailsParse(response: Response) = response.parseAs<WrapperDto<MangaDto>>().results.toSManga()
response.parseAs<WrapperDto<MangaDto>>().results.toSManga()
private val SManga.id: String get() { private val SManga.id: String get() {
val mangaUrl = apiUrl.toHttpUrl().newBuilder() val mangaUrl = apiUrl.toHttpUrl().newBuilder()
@ -166,18 +165,16 @@ class SussyToons : HttpSource(), ConfigurableSource {
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> = response.parseAs<WrapperDto<WrapperChapterDto>>().results.chapters.map {
return response.parseAs<WrapperDto<WrapperChapterDto>>().results.chapters.map { SChapter.create().apply {
SChapter.create().apply { name = it.name
name = it.name it.chapterNumber?.let {
it.chapterNumber?.let { chapter_number = it
chapter_number = it
}
setUrlWithoutDomain("$baseUrl/capitulo/${it.id}")
date_upload = it.updateAt.toDate()
} }
}.sortedBy(SChapter::chapter_number).reversed() setUrlWithoutDomain("$baseUrl/capitulo/${it.id}")
} date_upload = it.updateAt.toDate()
}
}.sortedBy(SChapter::chapter_number).reversed()
// ============================= Pages ==================================== // ============================= Pages ====================================
@ -208,30 +205,22 @@ class SussyToons : HttpSource(), ConfigurableSource {
Page(index, imageUrl = imageUrl.toString()) Page(index, imageUrl = imageUrl.toString())
} }
} }
private fun pageListParse(document: Document): List<Page> { private fun pageListParse(document: Document): List<Page> = document.select(pageUrlSelector).mapIndexed { index, element ->
return document.select(pageUrlSelector).mapIndexed { index, element -> Page(index, document.location(), element.absUrl("src"))
Page(index, document.location(), element.absUrl("src"))
}
}
private fun extractScriptData(document: Document): String {
return document.select("script").map(Element::data)
.firstOrNull(pageRegex::containsMatchIn)
?: throw Exception("Failed to load pages: Script data not found")
} }
private fun extractScriptData(document: Document): String = document.select("script").map(Element::data)
.firstOrNull(pageRegex::containsMatchIn)
?: throw Exception("Failed to load pages: Script data not found")
private fun extractJsonContent(scriptData: String): String { private fun extractJsonContent(scriptData: String): String = pageRegex.find(scriptData)
return pageRegex.find(scriptData) ?.groups?.get(1)?.value
?.groups?.get(1)?.value ?.let { json.decodeFromString<String>("\"$it\"") }
?.let { json.decodeFromString<String>("\"$it\"") } ?: throw Exception("Failed to extract JSON from script")
?: throw Exception("Failed to extract JSON from script")
}
private fun parseJsonToChapterPageDto(jsonContent: String): ChapterPageDto { private fun parseJsonToChapterPageDto(jsonContent: String): ChapterPageDto = try {
return try { jsonContent.parseAs<WrapperDto<ChapterPageDto>>().results
jsonContent.parseAs<WrapperDto<ChapterPageDto>>().results } catch (e: Exception) {
} catch (e: Exception) { throw Exception("Failed to load pages: ${e.message}")
throw Exception("Failed to load pages: ${e.message}")
}
} }
override fun imageUrlParse(response: Response): String = "" override fun imageUrlParse(response: Response): String = ""
@ -345,12 +334,13 @@ class SussyToons : HttpSource(), ConfigurableSource {
return json.decodeFromStream(body.byteStream()) return json.decodeFromStream(body.byteStream())
} }
private inline fun <reified T> String.parseAs(): T { private inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
return json.decodeFromString(this)
}
private fun String.toDate() = private fun String.toDate() = try {
try { dateFormat.parse(this)!!.time } catch (_: Exception) { 0L } dateFormat.parse(this)!!.time
} catch (_: Exception) {
0L
}
/** /**
* Normalizes path segments: * Normalizes path segments:

View File

@ -41,13 +41,11 @@ class MangaDto(
@SerialName("stt_nome") @SerialName("stt_nome")
val value: String?, val value: String?,
) { ) {
fun toStatus(): Int { fun toStatus(): Int = when (value?.lowercase()) {
return when (value?.lowercase()) { "em andamento" -> SManga.ONGOING
"em andamento" -> SManga.ONGOING "completo" -> SManga.COMPLETED
"completo" -> SManga.COMPLETED "hiato" -> SManga.ON_HIATUS
"hiato" -> SManga.ON_HIATUS else -> SManga.UNKNOWN
else -> SManga.UNKNOWN
}
} }
} }
} }

View File

@ -93,8 +93,7 @@ class HattoriManga : HttpSource() {
csrfToken = document.selectFirst("meta[name=csrf-token]")!!.attr("content") csrfToken = document.selectFirst("meta[name=csrf-token]")!!.attr("content")
} }
override fun chapterListParse(response: Response) = override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val slug = manga.url.substringAfterLast('/') val slug = manga.url.substringAfterLast('/')
@ -116,10 +115,9 @@ class HattoriManga : HttpSource() {
return Observable.just(chapters) return Observable.just(chapters)
} }
private fun fetchChapterPageableList(slug: String, page: Int, manga: SManga): HMChapterDto = private fun fetchChapterPageableList(slug: String, page: Int, manga: SManga): HMChapterDto = client.newCall(GET("$baseUrl/load-more-chapters/$slug?page=$page", headers))
client.newCall(GET("$baseUrl/load-more-chapters/$slug?page=$page", headers)) .execute()
.execute() .parseAs<HMChapterDto>()
.parseAs<HMChapterDto>()
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-chapters") override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-chapters")
@ -149,24 +147,20 @@ class HattoriManga : HttpSource() {
} }
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> = response.asJsoup().select(".image-wrapper img").mapIndexed { index, element ->
return response.asJsoup().select(".image-wrapper img").mapIndexed { index, element -> Page(index, imageUrl = "$baseUrl${element.attr("data-src")}")
Page(index, imageUrl = "$baseUrl${element.attr("data-src")}") }.takeIf { it.isNotEmpty() } ?: throw Exception("Oturum açmanız, WebView'ı açmanız ve oturum açmanız gerekir")
}.takeIf { it.isNotEmpty() } ?: throw Exception("Oturum açmanız, WebView'ı açmanız ve oturum açmanız gerekir")
}
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage = response.use {
return response.use { val mangas = it.parseAs<HMLatestUpdateDto>().chapters.map {
val mangas = it.parseAs<HMLatestUpdateDto>().chapters.map { SManga.create().apply {
SManga.create().apply { val manga = it.manga
val manga = it.manga title = manga.title
title = manga.title thumbnail_url = "$baseUrl/storage/${manga.thumbnail}"
thumbnail_url = "$baseUrl/storage/${manga.thumbnail}" url = "/manga/${manga.slug}"
url = "/manga/${manga.slug}" }
} }.distinctBy { manga -> manga.title }
}.distinctBy { manga -> manga.title } MangasPage(mangas, false)
MangasPage(mangas, false)
}
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
@ -263,19 +257,18 @@ class HattoriManga : HttpSource() {
setUrlWithoutDomain(REGEX_MANGA_URL.find(script)!!.groups[1]!!.value) setUrlWithoutDomain(REGEX_MANGA_URL.find(script)!!.groups[1]!!.value)
} }
private fun parseGenres(document: Document): List<Genre> { private fun parseGenres(document: Document): List<Genre> = document.select(".tags-blog a")
return document.select(".tags-blog a") .map { element -> Genre(element.text()) }
.map { element -> Genre(element.text()) }
}
private inline fun <reified T> Response.parseAs(): T { private inline fun <reified T> Response.parseAs(): T = json.decodeFromString(body.string())
return json.decodeFromString(body.string())
}
private fun Response.isPageExpired() = code == 419 private fun Response.isPageExpired() = code == 419
private fun String.toDate(): Long = private fun String.toDate(): Long = try {
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L } dateFormat.parse(trim())!!.time
} catch (_: Exception) {
0L
}
class GenreList(title: String, genres: List<Genre>) : Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) }) class GenreList(title: String, genres: List<Genre>) : Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) })

View File

@ -25,7 +25,9 @@ import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class LxHentai : ParsedHttpSource(), ConfigurableSource { class LxHentai :
ParsedHttpSource(),
ConfigurableSource {
override val name = "LXHentai" override val name = "LXHentai"
@ -41,38 +43,30 @@ class LxHentai : ParsedHttpSource(), ConfigurableSource {
override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", baseUrl) override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", baseUrl)
override fun popularMangaRequest(page: Int) = override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList(SortBy(3)))
searchMangaRequest(page, "", FilterList(SortBy(3)))
override fun popularMangaSelector() = searchMangaSelector() override fun popularMangaSelector() = searchMangaSelector()
override fun popularMangaFromElement(element: Element) = override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
searchMangaFromElement(element)
override fun popularMangaNextPageSelector() = override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
searchMangaNextPageSelector()
override fun latestUpdatesRequest(page: Int) = override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(SortBy(0)))
searchMangaRequest(page, "", FilterList(SortBy(0)))
override fun latestUpdatesSelector() = searchMangaSelector() override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesFromElement(element: Element) = override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
searchMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
searchMangaNextPageSelector()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = when {
return when { query.startsWith(PREFIX_ID_SEARCH) -> {
query.startsWith(PREFIX_ID_SEARCH) -> { val slug = query.substringAfter(PREFIX_ID_SEARCH)
val slug = query.substringAfter(PREFIX_ID_SEARCH) val mangaUrl = "/truyen/$slug"
val mangaUrl = "/truyen/$slug" fetchMangaDetails(SManga.create().apply { url = mangaUrl })
fetchMangaDetails(SManga.create().apply { url = mangaUrl }) .map { MangasPage(listOf(it), false) }
.map { MangasPage(listOf(it), false) }
}
else -> super.fetchSearchManga(page, query, filters)
} }
else -> super.fetchSearchManga(page, query, filters)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -168,32 +162,33 @@ class LxHentai : ParsedHttpSource(), ConfigurableSource {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>, state: Int = 0) : private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>, state: Int = 0) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
} }
private class SortBy(state: Int = 0) : UriPartFilter( private class SortBy(state: Int = 0) :
"Sắp xếp theo", UriPartFilter(
arrayOf( "Sắp xếp theo",
Pair("Mới cập nhật", "-updated_at"), arrayOf(
Pair("Mới nhất", "-created_at"), Pair("Mới cập nhật", "-updated_at"),
Pair("Cũ nhất", "created_at"), Pair("Mới nhất", "-created_at"),
Pair("Xem nhiều", "-views"), Pair("Cũ nhất", "created_at"),
Pair("A-Z", "name"), Pair("Xem nhiều", "-views"),
Pair("Z-A", "-name"), Pair("A-Z", "name"),
), Pair("Z-A", "-name"),
state, ),
) state,
)
private class Status : UriPartFilter( private class Status :
"Trạng thái", UriPartFilter(
arrayOf( "Trạng thái",
Pair("Tất cả", "1,2"), arrayOf(
Pair("Đang tiến hành", "2"), Pair("Tất cả", "1,2"),
Pair("Đã hoàn thành", "1"), Pair("Đang tiến hành", "2"),
), Pair("Đã hoàn thành", "1"),
) ),
)
private class Genre(name: String, val id: Int) : Filter.TriState(name) private class Genre(name: String, val id: Int) : Filter.TriState(name)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Thể loại", genres) private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Thể loại", genres)

View File

@ -34,39 +34,31 @@ class TruyenTranh3Q : ParsedHttpSource() {
.rateLimit(3) .rateLimit(3)
.build() .build()
override fun headersBuilder(): Headers.Builder { override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", "$baseUrl/")
return super.headersBuilder().add("Referer", "$baseUrl/")
}
private val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.US) private val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.US)
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/danh-sach/truyen-yeu-thich?page=$page", headers)
return GET("$baseUrl/danh-sach/truyen-yeu-thich?page=$page", headers)
}
override fun popularMangaSelector(): String = "ul.list_grid.grid > li" override fun popularMangaSelector(): String = "ul.list_grid.grid > li"
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
return SManga.create().apply { element.select("h3 a").let {
element.select("h3 a").let { title = it.text()
title = it.text() setUrlWithoutDomain(it.attr("abs:href"))
setUrlWithoutDomain(it.attr("abs:href"))
}
thumbnail_url = element.selectFirst(".book_avatar a img")
?.absUrl("src")
?.let { url ->
url.toHttpUrlOrNull()
?.queryParameter("url")
?: url
}
} }
thumbnail_url = element.selectFirst(".book_avatar a img")
?.absUrl("src")
?.let { url ->
url.toHttpUrlOrNull()
?.queryParameter("url")
?: url
}
} }
override fun popularMangaNextPageSelector(): String? = ".page_redirect > a:last-child > p:not(.active)" override fun popularMangaNextPageSelector(): String? = ".page_redirect > a:last-child > p:not(.active)"
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/danh-sach/truyen-moi-cap-nhat?page=$page", headers)
return GET("$baseUrl/danh-sach/truyen-moi-cap-nhat?page=$page", headers)
}
// same as popularManga // same as popularManga
override fun latestUpdatesSelector(): String = popularMangaSelector() override fun latestUpdatesSelector(): String = popularMangaSelector()
@ -121,21 +113,19 @@ class TruyenTranh3Q : ParsedHttpSource() {
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector(): String? = popularMangaNextPageSelector() override fun searchMangaNextPageSelector(): String? = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
return SManga.create().apply { document.selectFirst(".book_info > .book_other")?.let { info ->
document.selectFirst(".book_info > .book_other")?.let { info -> title = info.selectFirst("h1[itemprop=name]")!!.text()
title = info.selectFirst("h1[itemprop=name]")!!.text() author = info.selectFirst("ul.list-info li.author p.col-xs-9")?.text()
author = info.selectFirst("ul.list-info li.author p.col-xs-9")?.text() status = when (info.selectFirst("ul.list-info li.status p.col-xs-9")?.text()) {
status = when (info.selectFirst("ul.list-info li.status p.col-xs-9")?.text()) { "Đang Cập Nhật" -> SManga.ONGOING
"Đang Cập Nhật" -> SManga.ONGOING "Hoàn Thành" -> SManga.COMPLETED
"Hoàn Thành" -> SManga.COMPLETED else -> SManga.UNKNOWN
else -> SManga.UNKNOWN
}
genre = info.select(".list01 li a").joinToString { it.text() }
} }
description = document.selectFirst(".book_detail > .story-detail-info")?.text() genre = info.select(".list01 li a").joinToString { it.text() }
thumbnail_url = document.selectFirst(".book_detail > .book_info > .book_avatar > img")?.attr("abs:src")
} }
description = document.selectFirst(".book_detail > .story-detail-info")?.text()
thumbnail_url = document.selectFirst(".book_detail > .book_info > .book_avatar > img")?.attr("abs:src")
} }
// chapters // chapters
@ -183,23 +173,19 @@ class TruyenTranh3Q : ParsedHttpSource() {
} }
} }
override fun chapterFromElement(element: Element): SChapter { override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
return SChapter.create().apply { element.selectFirst(".name-chap > a")?.let {
element.selectFirst(".name-chap > a")?.let { name = it.text()
name = it.text() setUrlWithoutDomain(it.attr("abs:href"))
setUrlWithoutDomain(it.attr("abs:href"))
}
date_upload = parseChapterDate(element.selectFirst(".time-chap")?.text() ?: "")
} }
date_upload = parseChapterDate(element.selectFirst(".time-chap")?.text() ?: "")
} }
// parse pages // parse pages
private val pageListSelector = ".chapter_content .page-chapter img" private val pageListSelector = ".chapter_content .page-chapter img"
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> = document.select(pageListSelector).mapIndexed { idx, it ->
return document.select(pageListSelector).mapIndexed { idx, it -> Page(idx, imageUrl = it.absUrl("data-src"))
Page(idx, imageUrl = it.absUrl("data-src"))
}
} }
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
@ -248,10 +234,8 @@ class TruyenTranh3Q : ParsedHttpSource() {
private fun genresRequest() = GET("$baseUrl/$searchPath", headers) private fun genresRequest() = GET("$baseUrl/$searchPath", headers)
private fun parseGenres(document: Document): List<Genre> { private fun parseGenres(document: Document): List<Genre> = document.select(".genre-item").mapIndexed { index, element ->
return document.select(".genre-item").mapIndexed { index, element -> Genre(element.text(), index + 1)
Genre(element.text(), index + 1)
}
} }
private fun fetchGenres() { private fun fetchGenres() {

View File

@ -36,7 +36,9 @@ import java.io.IOException
import java.net.URLDecoder import java.net.URLDecoder
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class YuriNeko : HttpSource(), ConfigurableSource { class YuriNeko :
HttpSource(),
ConfigurableSource {
override val name = "YuriNeko" override val name = "YuriNeko"
@ -107,22 +109,20 @@ class YuriNeko : HttpSource(), ConfigurableSource {
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = when {
return when { query.startsWith(PREFIX_ID_SEARCH) -> {
query.startsWith(PREFIX_ID_SEARCH) -> { val id = query.removePrefix(PREFIX_ID_SEARCH).trim()
val id = query.removePrefix(PREFIX_ID_SEARCH).trim() if (id.toIntOrNull() == null) {
if (id.toIntOrNull() == null) { throw Exception("ID tìm kiếm không hợp lệ (phải là một số).")
throw Exception("ID tìm kiếm không hợp lệ (phải là một số).")
}
fetchMangaDetails(
SManga.create().apply {
url = "/manga/$id"
},
)
.map { MangasPage(listOf(it), false) }
} }
else -> super.fetchSearchManga(page, query, filters) fetchMangaDetails(
SManga.create().apply {
url = "/manga/$id"
},
)
.map { MangasPage(listOf(it), false) }
} }
else -> super.fetchSearchManga(page, query, filters)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -182,15 +182,13 @@ class YuriNeko : HttpSource(), ConfigurableSource {
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = override fun fetchMangaDetails(manga: SManga): Observable<SManga> = client.newCall(GET("$apiUrl${manga.url}"))
client.newCall(GET("$apiUrl${manga.url}")) .asObservableSuccess()
.asObservableSuccess() .map { mangaDetailsParse(it) }
.map { mangaDetailsParse(it) }
override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl${manga.url}") override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl${manga.url}")
override fun mangaDetailsParse(response: Response): SManga = override fun mangaDetailsParse(response: Response): SManga = response.parseAs<MangaDto>().toSManga(storageUrl)
response.parseAs<MangaDto>().toSManga(storageUrl)
override fun chapterListRequest(manga: SManga): Request = GET("$apiUrl${manga.url}") override fun chapterListRequest(manga: SManga): Request = GET("$apiUrl${manga.url}")
@ -202,13 +200,11 @@ class YuriNeko : HttpSource(), ConfigurableSource {
override fun pageListRequest(chapter: SChapter): Request = GET("$apiUrl${chapter.url}") override fun pageListRequest(chapter: SChapter): Request = GET("$apiUrl${chapter.url}")
override fun pageListParse(response: Response): List<Page> = override fun pageListParse(response: Response): List<Page> = response.parseAs<ReadResponseDto>().toPageList(storageUrl)
response.parseAs<ReadResponseDto>().toPageList(storageUrl)
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) : open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
} }