|
|
|
|
@ -1,238 +1,354 @@
|
|
|
|
|
package org.koitharu.kotatsu.parsers.site.all
|
|
|
|
|
|
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
import org.jsoup.nodes.Document
|
|
|
|
|
import org.jsoup.nodes.Element
|
|
|
|
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
|
|
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
|
|
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
|
|
|
import org.koitharu.kotatsu.parsers.core.PagedMangaParser
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.*
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.*
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.Manga
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.generateUid
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.parseHtml
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.urlEncoded
|
|
|
|
|
import java.text.SimpleDateFormat
|
|
|
|
|
import java.util.EnumSet
|
|
|
|
|
import java.util.Locale
|
|
|
|
|
import java.util.regex.Pattern
|
|
|
|
|
import java.util.*
|
|
|
|
|
|
|
|
|
|
@MangaSourceParser("MYREADINGMANGA", "MyReadingManga")
|
|
|
|
|
internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MYREADINGMANGA, 20) {
|
|
|
|
|
@MangaSourceParser("MYREADINGMANGA", "MyReadingManga", type = ContentType.HENTAI)
|
|
|
|
|
internal class MyReadingManga(context: MangaLoaderContext) :
|
|
|
|
|
PagedMangaParser(context, MangaParserSource.MYREADINGMANGA, 18) {
|
|
|
|
|
|
|
|
|
|
override val configKeyDomain = ConfigKey.Domain("myreadingmanga.info")
|
|
|
|
|
override val configKeyDomain = ConfigKey.Domain("myreadingmanga.info")
|
|
|
|
|
|
|
|
|
|
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
|
|
|
|
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {
|
|
|
|
|
super.onCreateConfig(keys)
|
|
|
|
|
keys.add(userAgentKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override val filterCapabilities: MangaListFilterCapabilities
|
|
|
|
|
get() = MangaListFilterCapabilities(
|
|
|
|
|
isMultipleTagsSupported = true,
|
|
|
|
|
isTagsExclusionSupported = false,
|
|
|
|
|
isSearchSupported = true,
|
|
|
|
|
isOriginalLocaleSupported = true,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
|
|
|
|
SortOrder.UPDATED,
|
|
|
|
|
SortOrder.POPULARITY,
|
|
|
|
|
SortOrder.ALPHABETICAL,
|
|
|
|
|
SortOrder.NEWEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
override suspend fun getFilterOptions() = MangaListFilterOptions(
|
|
|
|
|
availableTags = fetchTags(),
|
|
|
|
|
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
|
|
|
|
|
availableContentRating = EnumSet.of(ContentRating.ADULT),
|
|
|
|
|
availableLocales = setOf(
|
|
|
|
|
Locale.ENGLISH, Locale.JAPANESE, Locale.CHINESE, Locale.GERMAN, Locale.ITALIAN,
|
|
|
|
|
Locale("ru"), Locale("es"), Locale("pt", "BR"), Locale("tr"), Locale("vi"),
|
|
|
|
|
Locale("ar"), Locale("id"), Locale("ko"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private fun getLanguageForFilter(locale: Locale): String {
|
|
|
|
|
return when (locale.language) {
|
|
|
|
|
"en" -> "English"
|
|
|
|
|
"ja" -> "Japanese"
|
|
|
|
|
"zh" -> "Chinese"
|
|
|
|
|
"de" -> "German"
|
|
|
|
|
"it" -> "Italian"
|
|
|
|
|
"ru" -> "Russian"
|
|
|
|
|
"es" -> "Spanish"
|
|
|
|
|
"pt" -> "Portuguese"
|
|
|
|
|
"tr" -> "Turkish"
|
|
|
|
|
"vi" -> "Vietnamese"
|
|
|
|
|
"ar" -> "Arabic"
|
|
|
|
|
"id" -> "Indonesia"
|
|
|
|
|
"ko" -> "Korean"
|
|
|
|
|
else -> "English"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
override val filterCapabilities: MangaListFilterCapabilities
|
|
|
|
|
get() = MangaListFilterCapabilities(
|
|
|
|
|
isSearchSupported = true,
|
|
|
|
|
isOriginalLocaleSupported = true,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
|
|
|
|
SortOrder.UPDATED,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
override suspend fun getFilterOptions() = MangaListFilterOptions(
|
|
|
|
|
availableTags = fetchTags(),
|
|
|
|
|
availableStates = EnumSet.of(
|
|
|
|
|
MangaState.ONGOING,
|
|
|
|
|
MangaState.FINISHED,
|
|
|
|
|
),
|
|
|
|
|
availableContentRating = EnumSet.of(ContentRating.ADULT),
|
|
|
|
|
availableLocales = setOf(
|
|
|
|
|
Locale.ENGLISH,
|
|
|
|
|
Locale.FRENCH,
|
|
|
|
|
Locale.JAPANESE,
|
|
|
|
|
Locale.CHINESE,
|
|
|
|
|
Locale.GERMAN,
|
|
|
|
|
Locale.ITALIAN,
|
|
|
|
|
Locale.KOREAN,
|
|
|
|
|
Locale.TRADITIONAL_CHINESE,
|
|
|
|
|
Locale("es"), // Spanish
|
|
|
|
|
Locale("pt"), // Portuguese
|
|
|
|
|
Locale("ru"), // Russian
|
|
|
|
|
Locale("tr"), // Turkish
|
|
|
|
|
Locale("vi"), // Vietnamese
|
|
|
|
|
Locale("ar"), // Arabic
|
|
|
|
|
Locale("id"), // Indonesian (Bahasa)
|
|
|
|
|
Locale("th"), // Thai
|
|
|
|
|
Locale("pl"), // Polish
|
|
|
|
|
Locale("sv"), // Swedish
|
|
|
|
|
Locale("nl"), // Dutch (Flemish Dutch)
|
|
|
|
|
Locale("hu"), // Hungarian
|
|
|
|
|
Locale("hi"), // Hindi
|
|
|
|
|
Locale("he"), // Hebrew
|
|
|
|
|
Locale("el"), // Greek
|
|
|
|
|
Locale("fi"), // Finnish
|
|
|
|
|
Locale("fil"), // Filipino
|
|
|
|
|
Locale("da"), // Danish
|
|
|
|
|
Locale("cs"), // Czech
|
|
|
|
|
Locale("hr"), // Croatian
|
|
|
|
|
Locale("bg"), // Bulgarian
|
|
|
|
|
Locale("zh", "HK"), // Cantonese
|
|
|
|
|
Locale("fa"), // Persian
|
|
|
|
|
Locale("sk"), // Slovak
|
|
|
|
|
Locale("ro"), // Romanian
|
|
|
|
|
Locale("no"), // Norwegian
|
|
|
|
|
Locale("ms"), // Malay
|
|
|
|
|
Locale("lt"), // Lithuanian
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private fun getLanguageSlug(locale: Locale?): String? {
|
|
|
|
|
return when {
|
|
|
|
|
locale?.language == "fr" -> "french"
|
|
|
|
|
locale?.language == "ja" -> "jp"
|
|
|
|
|
locale?.language == "zh" && locale.country == "TW" -> "traditional-chinese"
|
|
|
|
|
locale?.language == "zh" && locale.country == "HK" -> "cantonese"
|
|
|
|
|
locale?.language == "zh" -> "chinese"
|
|
|
|
|
locale?.language == "de" -> "german"
|
|
|
|
|
locale?.language == "it" -> "italian"
|
|
|
|
|
locale?.language == "ko" -> "korean"
|
|
|
|
|
locale?.language == "es" -> "spanish"
|
|
|
|
|
locale?.language == "pt" -> "portuguese"
|
|
|
|
|
locale?.language == "ru" -> "russian"
|
|
|
|
|
locale?.language == "tr" -> "turkish"
|
|
|
|
|
locale?.language == "vi" -> "vietnamese"
|
|
|
|
|
locale?.language == "ar" -> "arabic"
|
|
|
|
|
locale?.language == "id" -> "bahasa"
|
|
|
|
|
locale?.language == "th" -> "thai"
|
|
|
|
|
locale?.language == "pl" -> "polish"
|
|
|
|
|
locale?.language == "sv" -> "swedish"
|
|
|
|
|
locale?.language == "nl" -> "flemish-dutch"
|
|
|
|
|
locale?.language == "hu" -> "hungarian"
|
|
|
|
|
locale?.language == "hi" -> "hindi"
|
|
|
|
|
locale?.language == "he" -> "hebrew"
|
|
|
|
|
locale?.language == "el" -> "greek"
|
|
|
|
|
locale?.language == "fi" -> "finnish"
|
|
|
|
|
locale?.language == "fil" -> "filipino"
|
|
|
|
|
locale?.language == "da" -> "danish"
|
|
|
|
|
locale?.language == "cs" -> "czech"
|
|
|
|
|
locale?.language == "hr" -> "croatian"
|
|
|
|
|
locale?.language == "bg" -> "bulgarian"
|
|
|
|
|
locale?.language == "fa" -> "persian"
|
|
|
|
|
locale?.language == "sk" -> "slovak"
|
|
|
|
|
locale?.language == "ro" -> "romanian"
|
|
|
|
|
locale?.language == "no" -> "norwegian-bokmal"
|
|
|
|
|
locale?.language == "ms" -> "malay"
|
|
|
|
|
locale?.language == "lt" -> "lithuanian"
|
|
|
|
|
else -> null //all
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
|
|
|
|
val url = buildString {
|
|
|
|
|
append("https://")
|
|
|
|
|
append(domain)
|
|
|
|
|
|
|
|
|
|
// Add language path if specified
|
|
|
|
|
val langSlug = getLanguageSlug(filter.locale)
|
|
|
|
|
if (langSlug != null) {
|
|
|
|
|
append("/lang/")
|
|
|
|
|
append(langSlug)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
|
|
|
|
|
val url = buildString {
|
|
|
|
|
append("https://")
|
|
|
|
|
append(domain)
|
|
|
|
|
append("/search/?wpsolr_page=")
|
|
|
|
|
append(page)
|
|
|
|
|
|
|
|
|
|
when {
|
|
|
|
|
!filter.query.isNullOrEmpty() -> {
|
|
|
|
|
append("&wpsolr_q=")
|
|
|
|
|
append(filter.query.replace(' ', '+'))
|
|
|
|
|
}
|
|
|
|
|
else -> {
|
|
|
|
|
append("&wpsolr_sort=")
|
|
|
|
|
when (order) {
|
|
|
|
|
SortOrder.UPDATED -> append("sort_by_date_desc")
|
|
|
|
|
SortOrder.POPULARITY -> append("sort_by_relevancy_desc")
|
|
|
|
|
SortOrder.ALPHABETICAL -> append("sort_by_title_asc")
|
|
|
|
|
SortOrder.NEWEST -> append("sort_by_date_desc")
|
|
|
|
|
else -> append("sort_by_random")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var paramIndex = 0
|
|
|
|
|
|
|
|
|
|
filter.locale?.let {
|
|
|
|
|
append(
|
|
|
|
|
buildString {
|
|
|
|
|
append("&wpsolr_fq[")
|
|
|
|
|
append(paramIndex)
|
|
|
|
|
append("]=lang_str:")
|
|
|
|
|
when {
|
|
|
|
|
!filter.query.isNullOrEmpty() -> {
|
|
|
|
|
// Search with language: /lang/french/page/2/?s=example
|
|
|
|
|
if (page > 1) {
|
|
|
|
|
append("/page/")
|
|
|
|
|
append(page)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
append(getLanguageForFilter(it))
|
|
|
|
|
paramIndex++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filter.tags.isNotEmpty()) {
|
|
|
|
|
filter.tags.forEach { tag ->
|
|
|
|
|
append("&wpsolr_fq[$paramIndex]=")
|
|
|
|
|
append("genre_str:${tag.key}")
|
|
|
|
|
paramIndex++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filter.states.oneOrThrowIfMany()?.let {
|
|
|
|
|
append("&wpsolr_fq[$paramIndex]=status:")
|
|
|
|
|
append(
|
|
|
|
|
when (it) {
|
|
|
|
|
MangaState.ONGOING -> "Ongoing"
|
|
|
|
|
MangaState.FINISHED -> "Completed"
|
|
|
|
|
else -> "Ongoing"
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
paramIndex++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
append("/?s=")
|
|
|
|
|
append(filter.query.urlEncoded())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val doc = webClient.httpGet(url).parseHtml()
|
|
|
|
|
return parseMangaList(doc)
|
|
|
|
|
}
|
|
|
|
|
filter.tags.isNotEmpty() -> {
|
|
|
|
|
// Genre filtering doesn't work with language, so we ignore language for genre
|
|
|
|
|
if (langSlug == null) {
|
|
|
|
|
append("/genre/")
|
|
|
|
|
append(filter.tags.first().key)
|
|
|
|
|
append("/page/")
|
|
|
|
|
append(page)
|
|
|
|
|
append("/")
|
|
|
|
|
} else {
|
|
|
|
|
// If both language and genre are selected, just use language
|
|
|
|
|
append("/page/")
|
|
|
|
|
append(page)
|
|
|
|
|
append("/")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun parseMangaList(doc: Document): List<Manga> {
|
|
|
|
|
return doc.select("div.results-by-facets div[id*=res]").map { element ->
|
|
|
|
|
val titleElement = element.selectFirst("a") ?: element.parseFailed("No title element found")
|
|
|
|
|
val thumbnailElement = element.selectFirst("img")
|
|
|
|
|
|
|
|
|
|
Manga(
|
|
|
|
|
id = generateUid(titleElement.attr("href")),
|
|
|
|
|
title = titleElement.text().replace(titleRegex.toRegex(), "").substringBeforeLast("(").trim(),
|
|
|
|
|
altTitles = emptySet(),
|
|
|
|
|
url = titleElement.attrAsRelativeUrl("href"),
|
|
|
|
|
publicUrl = titleElement.absUrl("href"),
|
|
|
|
|
rating = RATING_UNKNOWN,
|
|
|
|
|
contentRating = ContentRating.ADULT,
|
|
|
|
|
coverUrl = findImageSrc(thumbnailElement),
|
|
|
|
|
tags = emptySet(),
|
|
|
|
|
state = null,
|
|
|
|
|
authors = emptySet(),
|
|
|
|
|
source = source,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
filter.states.isNotEmpty() -> {
|
|
|
|
|
// Status filtering doesn't work with language either
|
|
|
|
|
if (langSlug == null) {
|
|
|
|
|
append("/status/")
|
|
|
|
|
append(
|
|
|
|
|
when (filter.states.first()) {
|
|
|
|
|
MangaState.ONGOING -> "ongoing"
|
|
|
|
|
MangaState.FINISHED -> "completed"
|
|
|
|
|
else -> "ongoing"
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
append("/page/")
|
|
|
|
|
append(page)
|
|
|
|
|
append("/")
|
|
|
|
|
} else {
|
|
|
|
|
// If both language and status are selected, just use language
|
|
|
|
|
append("/page/")
|
|
|
|
|
append(page)
|
|
|
|
|
append("/")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga {
|
|
|
|
|
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
|
|
|
|
val title = doc.selectFirst("h1")?.text() ?: manga.title
|
|
|
|
|
val author = title.substringAfter("[").substringBefore("]").trim()
|
|
|
|
|
val genres = doc.select(".entry-header p a[href*=genre], [href*=tag], span.entry-categories a")
|
|
|
|
|
.mapToSet {
|
|
|
|
|
MangaTag(
|
|
|
|
|
title = it.text().toTitleCase(),
|
|
|
|
|
key = it.attr("href").substringAfterLast("/").substringBefore("/"),
|
|
|
|
|
source = source,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val scanlatedBy = doc.select(".entry-terms:has(a[href*=group])")
|
|
|
|
|
.firstOrNull()?.select("a[href*=group]")
|
|
|
|
|
?.joinToString(prefix = "Scanlated by: ") { it.text() }
|
|
|
|
|
val extendedDescription = doc.select(".entry-content p:not(p:containsOwn(|)):not(.chapter-class + p)")
|
|
|
|
|
.joinToString("\n") { it.text() }
|
|
|
|
|
val description = listOfNotNull(title, scanlatedBy, extendedDescription).joinToString("\n").trim()
|
|
|
|
|
|
|
|
|
|
val state = when (doc.select("a[href*=status]").firstOrNull()?.text()) {
|
|
|
|
|
"Ongoing" -> MangaState.ONGOING
|
|
|
|
|
"Completed" -> MangaState.FINISHED
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
else -> {
|
|
|
|
|
// Regular browsing with or without language
|
|
|
|
|
append("/page/")
|
|
|
|
|
append(page)
|
|
|
|
|
append("/")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val chapters = parseChapters(doc)
|
|
|
|
|
val doc = webClient.httpGet(url).parseHtml()
|
|
|
|
|
return parseMangaList(doc)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return manga.copy(
|
|
|
|
|
description = description,
|
|
|
|
|
tags = genres,
|
|
|
|
|
state = state,
|
|
|
|
|
authors = setOfNotNull(author.takeIf { it.isNotEmpty() }),
|
|
|
|
|
chapters = chapters,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
private fun parseMangaList(doc: Document): List<Manga> {
|
|
|
|
|
return doc.select("div.content-archive article.post:not(.category-video)").mapNotNull { element ->
|
|
|
|
|
val titleElement = element.selectFirst("h2.entry-title a") ?: return@mapNotNull null
|
|
|
|
|
val thumbnailElement = element.selectFirst("a.entry-image-link img")
|
|
|
|
|
|
|
|
|
|
Manga(
|
|
|
|
|
id = generateUid(titleElement.attr("href")),
|
|
|
|
|
title = titleElement.text().replace(titleRegex.toRegex(), "").substringBeforeLast("(").trim(),
|
|
|
|
|
altTitles = emptySet(),
|
|
|
|
|
url = titleElement.attrAsRelativeUrl("href"),
|
|
|
|
|
publicUrl = titleElement.absUrl("href"),
|
|
|
|
|
rating = RATING_UNKNOWN,
|
|
|
|
|
contentRating = ContentRating.ADULT,
|
|
|
|
|
coverUrl = findImageSrc(thumbnailElement),
|
|
|
|
|
tags = emptySet(),
|
|
|
|
|
state = null,
|
|
|
|
|
authors = emptySet(),
|
|
|
|
|
source = source,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
|
|
|
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
|
|
|
|
override suspend fun getDetails(manga: Manga): Manga {
|
|
|
|
|
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
|
|
|
|
|
val title = doc.selectFirst("h1.entry-title")?.text() ?: manga.title
|
|
|
|
|
|
|
|
|
|
val altTitles = mutableSetOf<String>()
|
|
|
|
|
val altTitleElement = doc.selectFirst("p.alt-title-class")
|
|
|
|
|
if (altTitleElement != null) {
|
|
|
|
|
var nextElement = altTitleElement.nextElementSibling()
|
|
|
|
|
while (nextElement != null && nextElement.tagName() == "p" &&
|
|
|
|
|
!nextElement.hasClass("info-class") && !nextElement.hasClass("chapter-class")
|
|
|
|
|
) {
|
|
|
|
|
val altTitle = nextElement.text().trim()
|
|
|
|
|
if (altTitle.isNotEmpty()) {
|
|
|
|
|
altTitles.add(altTitle)
|
|
|
|
|
}
|
|
|
|
|
nextElement = nextElement.nextElementSibling()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var description = ""
|
|
|
|
|
val descriptionElement = doc.selectFirst("p.info-class")
|
|
|
|
|
if (descriptionElement != null) {
|
|
|
|
|
var nextElement = descriptionElement.nextElementSibling()
|
|
|
|
|
val descParts = mutableListOf<String>()
|
|
|
|
|
while (nextElement != null && nextElement.tagName() == "p" &&
|
|
|
|
|
!nextElement.hasClass("chapter-class") && !nextElement.hasClass("alt-title-class")
|
|
|
|
|
) {
|
|
|
|
|
val text = nextElement.text()
|
|
|
|
|
if (text.isNotEmpty()) {
|
|
|
|
|
descParts.add(text)
|
|
|
|
|
}
|
|
|
|
|
nextElement = nextElement.nextElementSibling()
|
|
|
|
|
}
|
|
|
|
|
description = descParts.joinToString("\n\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (description.isEmpty()) {
|
|
|
|
|
description = doc.select("div.entry-content p strong")
|
|
|
|
|
.joinToString("\n") { it.text() }
|
|
|
|
|
.trim()
|
|
|
|
|
.ifEmpty { title }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val authorFromTitle = title.substringAfter("[").substringBefore("]").trim()
|
|
|
|
|
val authorFromTag = doc.select("span.entry-tags a[href*='/tag/']")
|
|
|
|
|
.firstOrNull { it.text().contains("(") && it.text().contains(")") }
|
|
|
|
|
?.text()?.trim()
|
|
|
|
|
val author = authorFromTag ?: authorFromTitle
|
|
|
|
|
|
|
|
|
|
val genres = mutableSetOf<MangaTag>()
|
|
|
|
|
|
|
|
|
|
doc.select("span.entry-terms:has(span:contains(Genres)) a").forEach {
|
|
|
|
|
genres.add(
|
|
|
|
|
MangaTag(
|
|
|
|
|
title = it.text(),
|
|
|
|
|
key = it.attr("href").substringAfterLast("/genre/").substringBefore("/"),
|
|
|
|
|
source = source,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val state = when (doc.select("a[href*=status]").firstOrNull()?.text()) {
|
|
|
|
|
"Ongoing" -> MangaState.ONGOING
|
|
|
|
|
"Completed" -> MangaState.FINISHED
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val chapters = parseChapters(doc)
|
|
|
|
|
|
|
|
|
|
return manga.copy(
|
|
|
|
|
altTitles = altTitles,
|
|
|
|
|
description = description,
|
|
|
|
|
tags = genres,
|
|
|
|
|
state = state,
|
|
|
|
|
authors = setOfNotNull(author.takeIf { it.isNotEmpty() && it != title }),
|
|
|
|
|
chapters = chapters,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val images = (doc.select("div.entry-content img") + doc.select("div.separator img[data-src]"))
|
|
|
|
|
.mapNotNull { findImageSrc(it) }
|
|
|
|
|
.distinct()
|
|
|
|
|
|
|
|
|
|
return images.mapIndexed { _, url ->
|
|
|
|
|
MangaPage(
|
|
|
|
|
id = generateUid(url),
|
|
|
|
|
url = url,
|
|
|
|
|
preview = null,
|
|
|
|
|
source = source,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
|
|
|
|
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
|
|
|
|
|
|
|
|
|
|
val images = doc.select("div.entry-content img.img-myreadingmanga, div.entry-content div > img")
|
|
|
|
|
.filter { element ->
|
|
|
|
|
val src = findImageSrc(element)
|
|
|
|
|
src != null && !src.contains("GH-") && !src.contains("nucarnival") &&
|
|
|
|
|
!src.contains("/wp-content/uploads/202") // Exclude old uploads that might be ads
|
|
|
|
|
}
|
|
|
|
|
.mapNotNull { findImageSrc(it) }
|
|
|
|
|
.distinct()
|
|
|
|
|
|
|
|
|
|
return images.mapIndexed { index, url ->
|
|
|
|
|
MangaPage(
|
|
|
|
|
id = generateUid(url),
|
|
|
|
|
url = url,
|
|
|
|
|
preview = null,
|
|
|
|
|
source = source,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun fetchTags(): Set<MangaTag> {
|
|
|
|
|
val doc = webClient.httpGet("https://${domain}/search-6/").parseHtml()
|
|
|
|
|
|
|
|
|
|
return doc.select("div.select_opt").mapNotNull { element ->
|
|
|
|
|
val dataAttr = element.attr("data-wpsolr-facet-data")
|
|
|
|
|
val itemValue = dataAttr
|
|
|
|
|
.takeIf { it.isNotBlank() }
|
|
|
|
|
?.let { json ->
|
|
|
|
|
try {
|
|
|
|
|
val js = JSONObject(json)
|
|
|
|
|
val id = js.getString("id")
|
|
|
|
|
if (id != "genre_str") return@mapNotNull null
|
|
|
|
|
else js.getString("item_value")
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
val doc = webClient.httpGet("https://$domain/").parseHtml()
|
|
|
|
|
return doc.select("h4.widget-title.widgettitle:contains(Genres) + .tagcloud a")
|
|
|
|
|
.mapToSet { element ->
|
|
|
|
|
|
|
|
|
|
itemValue?.let { value ->
|
|
|
|
|
MangaTag(
|
|
|
|
|
title = value,
|
|
|
|
|
key = value,
|
|
|
|
|
title = element.text().substringBefore(" ("),
|
|
|
|
|
key = element.attr("href").trimEnd('/').substringAfterLast('/'),
|
|
|
|
|
source = source,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}.toSet()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val titleRegex = Pattern.compile("""\[[^]]*]""")
|
|
|
|
|
@ -254,44 +370,82 @@ internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(co
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun parseChapters(document: Document): List<MangaChapter> {
|
|
|
|
|
val chapters = mutableListOf<MangaChapter>()
|
|
|
|
|
val mangaUrl = document.baseUri()
|
|
|
|
|
val date = parseDate(document.select(".entry-time").text())
|
|
|
|
|
val chFirstName = document.select(".chapter-class a[href*=$mangaUrl]").firstOrNull()?.text()
|
|
|
|
|
?.ifEmpty { "Ch. 1" }?.replaceFirstChar { it.uppercase() } ?: "Ch. 1"
|
|
|
|
|
|
|
|
|
|
chapters.add(importChapter("1", mangaUrl, date, chFirstName))
|
|
|
|
|
|
|
|
|
|
val lastChapterNumber = document.select("a[class=page-numbers]").lastOrNull()?.text()?.toIntOrNull()
|
|
|
|
|
if (lastChapterNumber != null && lastChapterNumber > 1) {
|
|
|
|
|
for (i in 2..lastChapterNumber) {
|
|
|
|
|
chapters.add(importChapter(i.toString(), mangaUrl, date, "Ch. $i"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return chapters
|
|
|
|
|
}
|
|
|
|
|
private fun parseChapters(document: Document): List<MangaChapter> {
|
|
|
|
|
val chapters = mutableListOf<MangaChapter>()
|
|
|
|
|
val mangaUrl = document.baseUri().removeSuffix("/")
|
|
|
|
|
val date = parseDate(document.select("time.entry-time").text())
|
|
|
|
|
|
|
|
|
|
// Look for chapter information
|
|
|
|
|
val chapterClass = document.selectFirst("div.chapter-class")
|
|
|
|
|
|
|
|
|
|
// Check if there's a chapter title after the chapter-class div
|
|
|
|
|
var chapterTitle: String? = null
|
|
|
|
|
if (chapterClass != null) {
|
|
|
|
|
var nextElement = chapterClass.nextElementSibling()
|
|
|
|
|
while (nextElement != null && nextElement.tagName() != "div") {
|
|
|
|
|
if (nextElement.tagName() == "p" && nextElement.text().contains("Chapter", ignoreCase = true)) {
|
|
|
|
|
chapterTitle = nextElement.text().trim()
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
nextElement = nextElement.nextElementSibling()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for pagination
|
|
|
|
|
val paginationInContent =
|
|
|
|
|
document.select("div.entry-pagination a.page-numbers, div.chapter-class .entry-pagination a.page-numbers")
|
|
|
|
|
.mapNotNull { it.text().toIntOrNull() }
|
|
|
|
|
.maxOrNull()
|
|
|
|
|
|
|
|
|
|
if (paginationInContent != null && paginationInContent > 1) {
|
|
|
|
|
// Multi-page manga with chapters
|
|
|
|
|
for (i in 1..paginationInContent) {
|
|
|
|
|
val title = when {
|
|
|
|
|
chapterTitle != null && i == 1 -> chapterTitle
|
|
|
|
|
chapterTitle != null -> chapterTitle.replace("1", i.toString())
|
|
|
|
|
else -> "Chapter $i"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun parseDate(date: String): Long {
|
|
|
|
|
return try {
|
|
|
|
|
SimpleDateFormat("MMM dd, yyyy", Locale.US).parse(date)?.time ?: 0
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
chapters.add(
|
|
|
|
|
MangaChapter(
|
|
|
|
|
id = generateUid("$mangaUrl/$i"),
|
|
|
|
|
title = title,
|
|
|
|
|
number = i.toFloat(),
|
|
|
|
|
url = if (i == 1) mangaUrl else "$mangaUrl/$i/",
|
|
|
|
|
uploadDate = date,
|
|
|
|
|
source = source,
|
|
|
|
|
scanlator = null,
|
|
|
|
|
branch = null,
|
|
|
|
|
volume = 0,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Single page manga or no pagination found
|
|
|
|
|
chapters.add(
|
|
|
|
|
MangaChapter(
|
|
|
|
|
id = generateUid(mangaUrl),
|
|
|
|
|
title = chapterTitle ?: "Complete",
|
|
|
|
|
number = 1f,
|
|
|
|
|
url = mangaUrl,
|
|
|
|
|
uploadDate = date,
|
|
|
|
|
source = source,
|
|
|
|
|
scanlator = null,
|
|
|
|
|
branch = null,
|
|
|
|
|
volume = 0,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return chapters
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun importChapter(pageNumber: String, mangaUrl: String, date: Long, chapterName: String): MangaChapter {
|
|
|
|
|
return MangaChapter(
|
|
|
|
|
id = generateUid("$mangaUrl/$pageNumber"),
|
|
|
|
|
title = chapterName,
|
|
|
|
|
number = pageNumber.toFloatOrNull() ?: 0f,
|
|
|
|
|
url = "$mangaUrl/$pageNumber",
|
|
|
|
|
uploadDate = date,
|
|
|
|
|
source = source,
|
|
|
|
|
scanlator = null,
|
|
|
|
|
branch = null,
|
|
|
|
|
volume = 0,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
private fun parseDate(date: String): Long {
|
|
|
|
|
return try {
|
|
|
|
|
SimpleDateFormat("MMMM dd, yyyy", Locale.US).parse(date)?.time ?: 0
|
|
|
|
|
} catch (_: Exception) {
|
|
|
|
|
0L
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|