MyReadingManga: Update site structure (#2054)

master
Naga 9 months ago committed by GitHub
parent b5512e7574
commit 91ec95448c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,20 +1,38 @@
package org.koitharu.kotatsu.parsers.site.all package org.koitharu.kotatsu.parsers.site.all
import org.json.JSONObject
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.PagedMangaParser import org.koitharu.kotatsu.parsers.core.PagedMangaParser
import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.util.* 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.text.SimpleDateFormat
import java.util.EnumSet
import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
import java.util.*
@MangaSourceParser("MYREADINGMANGA", "MyReadingManga") @MangaSourceParser("MYREADINGMANGA", "MyReadingManga", type = ContentType.HENTAI)
internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.MYREADINGMANGA, 20) { internal class MyReadingManga(context: MangaLoaderContext) :
PagedMangaParser(context, MangaParserSource.MYREADINGMANGA, 18) {
override val configKeyDomain = ConfigKey.Domain("myreadingmanga.info") override val configKeyDomain = ConfigKey.Domain("myreadingmanga.info")
@ -25,105 +43,170 @@ internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(co
override val filterCapabilities: MangaListFilterCapabilities override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities( get() = MangaListFilterCapabilities(
isMultipleTagsSupported = true,
isTagsExclusionSupported = false,
isSearchSupported = true, isSearchSupported = true,
isOriginalLocaleSupported = true, isOriginalLocaleSupported = true,
) )
override val availableSortOrders: Set<SortOrder> = EnumSet.of( override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED, SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
) )
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchTags(), availableTags = fetchTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), availableStates = EnumSet.of(
MangaState.ONGOING,
MangaState.FINISHED,
),
availableContentRating = EnumSet.of(ContentRating.ADULT), availableContentRating = EnumSet.of(ContentRating.ADULT),
availableLocales = setOf( availableLocales = setOf(
Locale.ENGLISH, Locale.JAPANESE, Locale.CHINESE, Locale.GERMAN, Locale.ITALIAN, Locale.ENGLISH,
Locale("ru"), Locale("es"), Locale("pt", "BR"), Locale("tr"), Locale("vi"), Locale.FRENCH,
Locale("ar"), Locale("id"), Locale("ko"), 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 getLanguageForFilter(locale: Locale): String { private fun getLanguageSlug(locale: Locale?): String? {
return when (locale.language) { return when {
"en" -> "English" locale?.language == "fr" -> "french"
"ja" -> "Japanese" locale?.language == "ja" -> "jp"
"zh" -> "Chinese" locale?.language == "zh" && locale.country == "TW" -> "traditional-chinese"
"de" -> "German" locale?.language == "zh" && locale.country == "HK" -> "cantonese"
"it" -> "Italian" locale?.language == "zh" -> "chinese"
"ru" -> "Russian" locale?.language == "de" -> "german"
"es" -> "Spanish" locale?.language == "it" -> "italian"
"pt" -> "Portuguese" locale?.language == "ko" -> "korean"
"tr" -> "Turkish" locale?.language == "es" -> "spanish"
"vi" -> "Vietnamese" locale?.language == "pt" -> "portuguese"
"ar" -> "Arabic" locale?.language == "ru" -> "russian"
"id" -> "Indonesia" locale?.language == "tr" -> "turkish"
"ko" -> "Korean" locale?.language == "vi" -> "vietnamese"
else -> "English" 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> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
append("/search/?wpsolr_page=")
append(page) // Add language path if specified
val langSlug = getLanguageSlug(filter.locale)
if (langSlug != null) {
append("/lang/")
append(langSlug)
}
when { when {
!filter.query.isNullOrEmpty() -> { !filter.query.isNullOrEmpty() -> {
append("&wpsolr_q=") // Search with language: /lang/french/page/2/?s=example
append(filter.query.replace(' ', '+')) if (page > 1) {
append("/page/")
append(page)
} }
else -> { append("/?s=")
append("&wpsolr_sort=") append(filter.query.urlEncoded())
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")
} }
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("/")
} }
} }
var paramIndex = 0 filter.states.isNotEmpty() -> {
// Status filtering doesn't work with language either
filter.locale?.let { if (langSlug == null) {
append("/status/")
append( append(
buildString { when (filter.states.first()) {
append("&wpsolr_fq[") MangaState.ONGOING -> "ongoing"
append(paramIndex) MangaState.FINISHED -> "completed"
append("]=lang_str:") else -> "ongoing"
} },
) )
append(getLanguageForFilter(it)) append("/page/")
paramIndex++ append(page)
} append("/")
} else {
if (filter.tags.isNotEmpty()) { // If both language and status are selected, just use language
filter.tags.forEach { tag -> append("/page/")
append("&wpsolr_fq[$paramIndex]=") append(page)
append("genre_str:${tag.key}") append("/")
paramIndex++
} }
} }
filter.states.oneOrThrowIfMany()?.let { else -> {
append("&wpsolr_fq[$paramIndex]=status:") // Regular browsing with or without language
append( append("/page/")
when (it) { append(page)
MangaState.ONGOING -> "Ongoing" append("/")
MangaState.FINISHED -> "Completed" }
else -> "Ongoing"
},
)
paramIndex++
} }
} }
@ -132,9 +215,9 @@ internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(co
} }
private fun parseMangaList(doc: Document): List<Manga> { private fun parseMangaList(doc: Document): List<Manga> {
return doc.select("div.results-by-facets div[id*=res]").map { element -> return doc.select("div.content-archive article.post:not(.category-video)").mapNotNull { element ->
val titleElement = element.selectFirst("a") ?: element.parseFailed("No title element found") val titleElement = element.selectFirst("h2.entry-title a") ?: return@mapNotNull null
val thumbnailElement = element.selectFirst("img") val thumbnailElement = element.selectFirst("a.entry-image-link img")
Manga( Manga(
id = generateUid(titleElement.attr("href")), id = generateUid(titleElement.attr("href")),
@ -155,24 +238,65 @@ internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(co
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val title = doc.selectFirst("h1")?.text() ?: manga.title val title = doc.selectFirst("h1.entry-title")?.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") val altTitles = mutableSetOf<String>()
.mapToSet { 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( MangaTag(
title = it.text().toTitleCase(), title = it.text(),
key = it.attr("href").substringAfterLast("/").substringBefore("/"), key = it.attr("href").substringAfterLast("/genre/").substringBefore("/"),
source = source, 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()) { val state = when (doc.select("a[href*=status]").firstOrNull()?.text()) {
"Ongoing" -> MangaState.ONGOING "Ongoing" -> MangaState.ONGOING
"Completed" -> MangaState.FINISHED "Completed" -> MangaState.FINISHED
@ -182,22 +306,29 @@ internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(co
val chapters = parseChapters(doc) val chapters = parseChapters(doc)
return manga.copy( return manga.copy(
altTitles = altTitles,
description = description, description = description,
tags = genres, tags = genres,
state = state, state = state,
authors = setOfNotNull(author.takeIf { it.isNotEmpty() }), authors = setOfNotNull(author.takeIf { it.isNotEmpty() && it != title }),
chapters = chapters, chapters = chapters,
) )
} }
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml()
val images = (doc.select("div.entry-content img") + doc.select("div.separator img[data-src]")) 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) } .mapNotNull { findImageSrc(it) }
.distinct() .distinct()
return images.mapIndexed { _, url -> return images.mapIndexed { index, url ->
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,
@ -208,31 +339,16 @@ internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(co
} }
private suspend fun fetchTags(): Set<MangaTag> { private suspend fun fetchTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://${domain}/search-6/").parseHtml() val doc = webClient.httpGet("https://$domain/").parseHtml()
return doc.select("h4.widget-title.widgettitle:contains(Genres) + .tagcloud a")
return doc.select("div.select_opt").mapNotNull { element -> .mapToSet { 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
}
}
itemValue?.let { value ->
MangaTag( MangaTag(
title = value, title = element.text().substringBefore(" ("),
key = value, key = element.attr("href").trimEnd('/').substringAfterLast('/'),
source = source, source = source,
) )
} }
}.toSet()
} }
private val titleRegex = Pattern.compile("""\[[^]]*]""") private val titleRegex = Pattern.compile("""\[[^]]*]""")
@ -256,18 +372,69 @@ internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(co
private fun parseChapters(document: Document): List<MangaChapter> { private fun parseChapters(document: Document): List<MangaChapter> {
val chapters = mutableListOf<MangaChapter>() val chapters = mutableListOf<MangaChapter>()
val mangaUrl = document.baseUri() val mangaUrl = document.baseUri().removeSuffix("/")
val date = parseDate(document.select(".entry-time").text()) val date = parseDate(document.select("time.entry-time").text())
val chFirstName = document.select(".chapter-class a[href*=$mangaUrl]").firstOrNull()?.text()
?.ifEmpty { "Ch. 1" }?.replaceFirstChar { it.uppercase() } ?: "Ch. 1" // Look for chapter information
val chapterClass = document.selectFirst("div.chapter-class")
chapters.add(importChapter("1", mangaUrl, date, chFirstName))
// Check if there's a chapter title after the chapter-class div
val lastChapterNumber = document.select("a[class=page-numbers]").lastOrNull()?.text()?.toIntOrNull() var chapterTitle: String? = null
if (lastChapterNumber != null && lastChapterNumber > 1) { if (chapterClass != null) {
for (i in 2..lastChapterNumber) { var nextElement = chapterClass.nextElementSibling()
chapters.add(importChapter(i.toString(), mangaUrl, date, "Ch. $i")) 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"
}
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 return chapters
@ -275,23 +442,10 @@ internal class MyReadingManga(context: MangaLoaderContext) : PagedMangaParser(co
private fun parseDate(date: String): Long { private fun parseDate(date: String): Long {
return try { return try {
SimpleDateFormat("MMM dd, yyyy", Locale.US).parse(date)?.time ?: 0 SimpleDateFormat("MMMM dd, yyyy", Locale.US).parse(date)?.time ?: 0
} catch (e: Exception) { } catch (_: Exception) {
0 0L
} }
} }
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,
)
}
} }

Loading…
Cancel
Save