diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt index d1045dcb6..b8a2e0004 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/ContentType.kt @@ -27,4 +27,5 @@ public enum class ContentType { * Use this type if no other suits your needs. For example, for an indie manga */ OTHER, + ONE_SHOT, } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt index 51b52f800..6d32d8bbe 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/model/Demographic.kt @@ -5,5 +5,6 @@ public enum class Demographic { SHOUJO, SEINEN, JOSEI, + KODOMO, NONE, } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt index a2fc3f5d5..e11b0d83d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/ComickFunParser.kt @@ -52,7 +52,13 @@ internal class ComickFunParser(context: MangaLoaderContext) : ContentType.MANHUA, ContentType.OTHER, ), - availableDemographics = EnumSet.allOf(Demographic::class.java), + availableDemographics = EnumSet.of( + Demographic.SHOUNEN, + Demographic.SHOUJO, + Demographic.SEINEN, + Demographic.JOSEI, + Demographic.NONE, + ), ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { @@ -132,6 +138,7 @@ internal class ComickFunParser(context: MangaLoaderContext) : Demographic.SEINEN -> "3" Demographic.JOSEI -> "4" Demographic.NONE -> "5" + else -> "" }, ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt index 4c61d6fcb..d7e770abf 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/all/MangaDexParser.kt @@ -82,7 +82,13 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context MangaState.ABANDONED, ), availableContentRating = EnumSet.allOf(ContentRating::class.java), - availableDemographics = EnumSet.allOf(Demographic::class.java), + availableDemographics = EnumSet.of( + Demographic.SHOUNEN, + Demographic.SHOUJO, + Demographic.SEINEN, + Demographic.JOSEI, + Demographic.NONE, + ), availableLocales = localesDeferred.await(), ) } @@ -164,6 +170,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context Demographic.SEINEN -> "seinen" Demographic.JOSEI -> "josei" Demographic.NONE -> "none" + else -> "" }, ) } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TempleScanEsp.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TempleScanEsp.kt index ae3b7de20..c0974de4d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TempleScanEsp.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TempleScanEsp.kt @@ -1,21 +1,23 @@ package org.koitharu.kotatsu.parsers.site.es import kotlinx.coroutines.coroutineScope -import org.jsoup.nodes.Document import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaSourceParser -import org.koitharu.kotatsu.parsers.PagedMangaParser +import org.koitharu.kotatsu.parsers.SinglePageMangaParser import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.model.* import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.util.json.mapJSON +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* @MangaSourceParser("TEMPLESCANESP", "TempleScanEsp", "es", ContentType.HENTAI) internal class TempleScanEsp(context: MangaLoaderContext) : - PagedMangaParser(context, MangaParserSource.TEMPLESCANESP, pageSize = 15) { + SinglePageMangaParser(context, MangaParserSource.TEMPLESCANESP) { - override val availableSortOrders: Set = EnumSet.of(SortOrder.NEWEST, SortOrder.UPDATED) + override val availableSortOrders: Set = EnumSet.of(SortOrder.NEWEST_ASC) override val configKeyDomain = ConfigKey.Domain("templescanesp.net") @@ -31,32 +33,17 @@ internal class TempleScanEsp(context: MangaLoaderContext) : override suspend fun getFilterOptions() = MangaListFilterOptions() - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val url = buildString { - append("https://") - append(domain) - if (order == SortOrder.NEWEST) { - append("/comics?page=") - append(page) - } else { - if (page > 1) { - return emptyList() - } - } - } - - val doc = webClient.httpGet(url).parseHtml() - return doc.select("div.grid figure").ifEmpty { - doc.requireElementById("projectsDiv").select("figure") - }.map { div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") + override suspend fun getList(order: SortOrder, filter: MangaListFilter): List { + val json = webClient.httpGet("https://apis.$domain/api/searchProject").parseJson().getJSONArray("response") + return json.mapJSON { + val href = "https://$domain/ver/${it.getString("slug")}" Manga( id = generateUid(href), url = href, - publicUrl = href.toAbsoluteUrl(div.host ?: domain), - coverUrl = div.selectFirst("img")?.src().orEmpty(), - title = div.selectFirst("figcaption")?.text().orEmpty(), - altTitle = null, + publicUrl = href, + coverUrl = it.getString("urlImg").orEmpty(), + title = it.getString("name").orEmpty(), + altTitle = it.getString("alternativeName").orEmpty(), rating = RATING_UNKNOWN, tags = emptySet(), author = null, @@ -70,36 +57,34 @@ internal class TempleScanEsp(context: MangaLoaderContext) : override suspend fun getDetails(manga: Manga): Manga = coroutineScope { val fullUrl = manga.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val chaptersDeferred = getChapters(doc) + val dateFormat = SimpleDateFormat("dd/mm/yyyy", sourceLocale) manga.copy( - description = doc.requireElementById("section-sinopsis").html(), - chapters = chaptersDeferred, + description = doc.selectFirst(".infoProject_projectInfo__786qu")?.text().orEmpty(), + chapters = doc.body().select(".contenedor a") + .mapChapters(reversed = true) { i, a -> + val href = a.attrAsRelativeUrl("href") + MangaChapter( + id = generateUid(href), + name = a.selectFirst("span")?.text() ?: "Capítulo ${i + 1f}", + number = i + 1f, + volume = 0, + url = href, + uploadDate = parseChapterDate( + dateFormat, + a.selectFirst(".infoProject_dateChapter__BIuU7")?.text(), + ), + source = source, + scanlator = null, + branch = null, + ) + }, ) } - private fun getChapters(doc: Document): List { - return doc.body().select("div.grid-capitulos div.contenedor-capitulo-miniatura") - .mapChapters(reversed = true) { i, div -> - val href = div.selectFirstOrThrow("a").attrAsRelativeUrl("href") - val date = parseUploadDate(div.selectFirstOrThrow("time").text()) - MangaChapter( - id = generateUid(href), - name = div.requireElementById("name").text(), - number = i + 1f, - volume = 0, - url = href, - uploadDate = date, - source = source, - scanlator = null, - branch = null, - ) - } - } - override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - return doc.select("main.contenedor-imagen img").map { url -> + return doc.select("main.contenedor img.readChapter_image__450v_").map { url -> val img = url.src()?.toRelativeUrl(domain) ?: url.parseFailed("Image src not found") MangaPage( id = generateUid(img), @@ -110,23 +95,41 @@ internal class TempleScanEsp(context: MangaLoaderContext) : } } - private fun parseUploadDate(timeStr: String?): Long { - timeStr ?: return 0 - val timeWords = timeStr.split(' ') - if (timeWords.size != 3) return 0 - val timeWord = timeWords[1] - val timeAmount = timeWords[0].toIntOrNull() ?: return 0 - val timeUnit = when (timeWord) { - "minute", "minutes" -> Calendar.MINUTE - "hour", "hours" -> Calendar.HOUR - "day", "days" -> Calendar.DAY_OF_YEAR - "week", "weeks" -> Calendar.WEEK_OF_YEAR - "month", "months" -> Calendar.MONTH - "year", "years" -> Calendar.YEAR - else -> return 0 + private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { + val d = date?.lowercase() ?: return 0 + return when { + + WordSet("há ", "hace ").startsWith(d) -> { + parseRelativeDate(d) + } + + else -> dateFormat.tryParse(date) } + } + + private fun parseRelativeDate(date: String): Long { + val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val cal = Calendar.getInstance() - cal.add(timeUnit, -timeAmount) - return cal.time.time + return when { + WordSet("segundo", "second") + .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis + + WordSet("minuto") + .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis + + WordSet("hora", "horas") + .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis + + WordSet("día", "días") + .anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis + + WordSet("meses", "mes") + .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis + + WordSet("year") + .anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis + + else -> 0 + } } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt index 30ff75cea..1fd7af85f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/es/TuMangaOnlineParser.kt @@ -40,66 +40,113 @@ internal class TuMangaOnlineParser(context: MangaLoaderContext) : PagedMangaPars override val filterCapabilities: MangaListFilterCapabilities get() = MangaListFilterCapabilities( - isMultipleTagsSupported = true, isSearchSupported = true, + isSearchWithFiltersSupported = true, + isTagsExclusionSupported = true, + isMultipleTagsSupported = true, ) override suspend fun getFilterOptions() = MangaListFilterOptions( availableTags = fetchAvailableTags(), availableContentRating = EnumSet.of(ContentRating.SAFE, ContentRating.ADULT), + availableDemographics = EnumSet.of( + Demographic.SHOUNEN, + Demographic.SHOUJO, + Demographic.SEINEN, + Demographic.JOSEI, + Demographic.KODOMO, + ), + availableContentTypes = EnumSet.of( + ContentType.MANGA, + ContentType.MANHWA, + ContentType.MANHUA, + ContentType.NOVEL, + ContentType.ONE_SHOT, + ContentType.HENTAI, + ContentType.OTHER, + ), ) override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { val url = buildString { append("https://") append(domain) - append("/library") - when { + append("/library?_pg=1&page=") + append(page.toString()) - !filter.query.isNullOrEmpty() -> { - append("?title=") - append(filter.query.urlEncoded()) - } + filter.query?.let { + append("&title=") + append(filter.query.urlEncoded()) + } - else -> { - append("?order_item=") - append( - when (order) { - SortOrder.POPULARITY -> "likes_count&order_dir=desc" - SortOrder.POPULARITY_ASC -> "likes_count&order_dir=asc" - SortOrder.UPDATED -> "release_date&order_dir=desc" - SortOrder.UPDATED_ASC -> "release_date&order_dir=asc" - SortOrder.NEWEST -> "creation&order_dir=desc" - SortOrder.NEWEST_ASC -> "creation&order_dir=asc" - SortOrder.ALPHABETICAL -> "alphabetically&order_dir=asc" - SortOrder.ALPHABETICAL_DESC -> "alphabetically&order_dir=desc" - SortOrder.RATING -> "score&order_dir=desc" - SortOrder.RATING_ASC -> "score&order_dir=asc" - else -> "release_date&order_dir=desc" - }, - ) - append("&filter_by=title") - if (filter.tags.isNotEmpty()) { - for (tag in filter.tags) { - append("&genders[]=") - append(tag.key) - } - } - - filter.contentRating.oneOrThrowIfMany()?.let { - append("&erotic=") - append( - when (it) { - ContentRating.SAFE -> "false" - ContentRating.ADULT -> "true" - else -> "" - }, - ) - } - } + append("&order_item=") + append( + when (order) { + SortOrder.POPULARITY -> "likes_count&order_dir=desc" + SortOrder.POPULARITY_ASC -> "likes_count&order_dir=asc" + SortOrder.UPDATED -> "release_date&order_dir=desc" + SortOrder.UPDATED_ASC -> "release_date&order_dir=asc" + SortOrder.NEWEST -> "creation&order_dir=desc" + SortOrder.NEWEST_ASC -> "creation&order_dir=asc" + SortOrder.ALPHABETICAL -> "alphabetically&order_dir=asc" + SortOrder.ALPHABETICAL_DESC -> "alphabetically&order_dir=desc" + SortOrder.RATING -> "score&order_dir=desc" + SortOrder.RATING_ASC -> "score&order_dir=asc" + else -> "release_date&order_dir=desc" + }, + ) + append("&filter_by=title") + + filter.tags.forEach { + append("&genders[]=") + append(it.key) + } + + filter.tagsExclude.forEach { + append("&exclude_genders[]=") + append(it.key) + } + + append("&type=") + filter.types.forEach { + append( + when (it) { + ContentType.MANGA -> "manga" + ContentType.MANHWA -> "manhwa" + ContentType.MANHUA -> "manhua" + ContentType.NOVEL -> "novel" + ContentType.ONE_SHOT -> "one_shot" + ContentType.HENTAI -> "doujinshi" + ContentType.OTHER -> "oel" + else -> "" + }, + ) + } + + filter.demographics.forEach { + append("&demography=") + append( + when (it) { + Demographic.SHOUNEN -> "shounen" + Demographic.SHOUJO -> "shoujo" + Demographic.SEINEN -> "seinen" + Demographic.JOSEI -> "josei" + Demographic.KODOMO -> "kodomo" + else -> "" + }, + ) + } + + filter.contentRating.oneOrThrowIfMany()?.let { + append("&erotic=") + append( + when (it) { + ContentRating.SAFE -> "false" + ContentRating.ADULT -> "true" + else -> "" + }, + ) } - append("&_pg=1&page=") - append(page.toString()) } val doc = webClient.httpGet(url, getRequestHeaders()).parseHtml() val items = doc.body().select("div.element") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt index c5ec00180..42687275f 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/MadaraParser.kt @@ -791,10 +791,10 @@ internal abstract class MadaraParser( WordSet("jam", "saat", "heure", "hora", "horas", "hour", "hours", "h", "ساعات", "ساعة") .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis - WordSet("hari", "gün", "jour", "día", "dia", "day", "days", "d", "день") + WordSet("hari", "gün", "jour", "día", "dia", "day", "días", "days", "d", "день") .anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis - WordSet("month", "months", "أشهر", "mois") + WordSet("month", "months", "أشهر", "mois", "meses", "mes") .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis WordSet("year")