From a6a72064bb17c726770e0a38e3e486dcb57e2c0a Mon Sep 17 00:00:00 2001 From: fuckpdf Date: Thu, 10 Jul 2025 10:26:49 +0300 Subject: [PATCH] [TenshiManga] Add source (#1933) * [UzayManga + ElderManga] Update order * [TenshiManga] Add source --------- Co-authored-by: Draken <131387159+dragonx943@users.noreply.github.com> --- .github/summary.yaml | 2 +- .../kotatsu/parsers/site/tr/ElderManga.kt | 2 + .../kotatsu/parsers/site/tr/TenshiManga.kt | 204 ++++++++++++++++++ .../kotatsu/parsers/site/tr/UzayManga.kt | 2 + 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt diff --git a/.github/summary.yaml b/.github/summary.yaml index abeb1dee..dbb6ee83 100644 --- a/.github/summary.yaml +++ b/.github/summary.yaml @@ -1 +1 @@ -total: 1234 \ No newline at end of file +total: 1235 diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt index d8fbc601..dc53d211 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt @@ -28,6 +28,7 @@ internal class ElderManga(context: MangaLoaderContext): SortOrder.ALPHABETICAL_DESC, SortOrder.NEWEST, SortOrder.POPULARITY, + SortOrder.UPDATED, ) override val filterCapabilities: MangaListFilterCapabilities @@ -103,6 +104,7 @@ internal class ElderManga(context: MangaLoaderContext): SortOrder.ALPHABETICAL_DESC -> "2" SortOrder.NEWEST -> "3" SortOrder.POPULARITY -> "4" + SortOrder.UPDATED -> "5" else -> "1" } ) diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt new file mode 100644 index 00000000..c65eae79 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/TenshiManga.kt @@ -0,0 +1,204 @@ +package org.koitharu.kotatsu.parsers.site.tr + +import org.json.JSONArray +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.LegacyPagedMangaParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.util.json.* +import java.text.SimpleDateFormat +import java.util.* + +@MangaSourceParser("TENSHIMANGA", "Tenshi Manga", "tr") +internal class TenshiManga(context: MangaLoaderContext): + LegacyPagedMangaParser(context, MangaParserSource.TENSHIMANGA, 25) { + + override val configKeyDomain = ConfigKey.Domain("tenshimanga.com") + private val cdnSuffix = "cdn1.$domain" + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.ALPHABETICAL, + SortOrder.ALPHABETICAL_DESC, + SortOrder.NEWEST, + SortOrder.POPULARITY, + SortOrder.UPDATED, + ) + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isSearchSupported = true, + isMultipleTagsSupported = true, + isSearchWithFiltersSupported = true, + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchTags(), + availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.PAUSED), + availableContentTypes = EnumSet.of( + ContentType.MANGA, + ContentType.MANHWA, + ContentType.MANHUA, + ContentType.COMICS, + ), + ) + + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + append(domain) + append("/search") + append("?page=") + append(page.toString()) + + if (!filter.query.isNullOrEmpty()) { + append("&search=") + append(filter.query.urlEncoded()) + } + + if (filter.tags.isNotEmpty()) { + append("&categories=") + filter.tags.joinTo(this, ",") { it.key } + } + + if (filter.states.isNotEmpty()) { + append("&publicStatus=") + filter.states.oneOrThrowIfMany()?.let { + append( + when (it) { + MangaState.ONGOING -> "1" + MangaState.FINISHED -> "2" + MangaState.ABANDONED -> "3" + MangaState.PAUSED -> "4" + else -> "" + }, + ) + } + } + + if (filter.types.isNotEmpty()) { + append("&country=") + filter.types.oneOrThrowIfMany()?.let { + append( + when (it) { + ContentType.MANHUA -> "1" + ContentType.MANHWA -> "2" + ContentType.MANGA -> "3" + ContentType.COMICS -> "4" + else -> "" + }, + ) + } + } + + append("&order=") + append( + when (order) { + SortOrder.ALPHABETICAL -> "1" + SortOrder.ALPHABETICAL_DESC -> "2" + SortOrder.NEWEST -> "3" + SortOrder.POPULARITY -> "4" + SortOrder.UPDATED, -> "5" + else -> "1" + } + ) + } + + val doc = webClient.httpGet(url).parseHtml() + return doc.select("section[aria-label='series area'] .card").map { card -> + val href = card.selectFirstOrThrow("a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + title = card.selectFirst("h2")?.text().orEmpty(), + altTitles = emptySet(), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = card.selectFirst("img")?.attrAsAbsoluteUrlOrNull("src"), + tags = emptySet(), + state = null, + authors = emptySet(), + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val statusText = doc.selectFirst("span:contains(Durum) + span")?.text().orEmpty() + return manga.copy( + tags = doc.select("a[href^='search?categories']").mapToSet { + val key = it.attr("href").substringAfter("?categories=") + MangaTag( + key = key, + title = it.text(), + source = source, + ) + }, + description = doc.selectFirst("div.grid h2 + p")?.text(), + state = when (statusText) { + "Devam Ediyor" -> MangaState.ONGOING + "Birakildi" -> MangaState.ONGOING + "Tamamlandi" -> MangaState.FINISHED + else -> null + }, + chapters = doc.select("div.list-episode a").mapChapters(reversed = true) { i, el -> + val href = el.attrAsRelativeUrl("href") + val dateFormat = SimpleDateFormat("MMM d ,yyyy", Locale("tr")) + MangaChapter( + id = generateUid(href), + title = el.selectFirstOrThrow("h3").text(), + number = (i + 1).toFloat(), + volume = 0, + url = href, + scanlator = null, + uploadDate = el.selectFirst("span")?.text()?.let { dateFormat.tryParse(it) } ?: 0L, + branch = null, + source = source, + ) + }, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + val pageRegex = Regex("\\\\\"path\\\\\":\\\\\"([^\"]+)\\\\\"") + val script = doc.select("script").find { it.html().contains(pageRegex) }?.html() ?: return emptyList() + return pageRegex.findAll(script).mapNotNull { result -> + result.groups[1]?.value?.let { url -> + MangaPage( + id = generateUid(url), + url = "https://$cdnSuffix/upload/series/$url", + preview = null, + source = source, + ) + } + }.toList() + } + + private suspend fun fetchTags(): Set { + val doc = webClient.httpGet("https://$domain/search").parseHtml() + val script = doc.select("script").find { it.html().contains("self.__next_f.push([1,\"10:[\\\"\\$,\\\"section") }?.html() + ?: return emptySet() + + val jsonStr = script.substringAfter("\"category\":[") + .substringBefore("],\"searchParams\":{}") + .replace("\\", "") + + val jsonArray = JSONArray("[$jsonStr]") + return jsonArray.mapJSONToSet { jo -> + MangaTag( + key = jo.getString("id"), + title = jo.getString("name"), + source = source + ) + } + } +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt index 43efbb54..c6af7c3d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/UzayManga.kt @@ -28,6 +28,7 @@ internal class UzayManga(context: MangaLoaderContext): SortOrder.ALPHABETICAL_DESC, SortOrder.NEWEST, SortOrder.POPULARITY, + SortOrder.UPDATED, ) override val filterCapabilities: MangaListFilterCapabilities @@ -103,6 +104,7 @@ internal class UzayManga(context: MangaLoaderContext): SortOrder.ALPHABETICAL_DESC -> "2" SortOrder.NEWEST -> "3" SortOrder.POPULARITY -> "4" + SortOrder.UPDATED -> "5" else -> "1" } )