diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt new file mode 100644 index 00000000..9d5e7684 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/RoliaScan.kt @@ -0,0 +1,171 @@ +package org.koitharu.kotatsu.parsers.site.en + +import androidx.collection.arraySetOf +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 java.util.* + +@MangaSourceParser("ROLIASCAN", "Rolia Scan", "en") +internal class RoliaScan(context: MangaLoaderContext) : LegacyPagedMangaParser(context, MangaParserSource.ROLIASCAN, 25) { + + override val configKeyDomain = ConfigKey.Domain("roliascan.com") + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.NEWEST, + SortOrder.POPULARITY, + SortOrder.ALPHABETICAL, + SortOrder.ALPHABETICAL_DESC + ) + + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isSearchSupported = true, + isSearchWithFiltersSupported = true, + isMultipleTagsSupported = true + ) + + override suspend fun getFilterOptions() = MangaListFilterOptions( + availableTags = fetchTags(), + availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), + 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("/manga") + append("?page=$page") + if (!filter.query.isNullOrEmpty()) { + append("&_post_type_search_box=${filter.query.urlEncoded()}") + } + append("&_sort_posts=") + append( + when (order) { + SortOrder.NEWEST -> "update_oldest" + SortOrder.POPULARITY -> "" + SortOrder.ALPHABETICAL -> "title_a_z" + SortOrder.ALPHABETICAL_DESC -> "title_z_a" + else -> "" + } + ) + if (filter.tags.isNotEmpty()) { + append("&_genres=") + append(filter.tags.joinToString(",") { it.key }) + } + if (filter.states.isNotEmpty()) { + append("&status=") + append( + when (filter.states.oneOrThrowIfMany()) { + MangaState.ONGOING -> "publishing" + MangaState.FINISHED -> "completed" + else -> "" + } + ) + } + if (filter.types.isNotEmpty()) { + append("&type=") + append( + when (filter.types.oneOrThrowIfMany()) { + ContentType.MANGA -> "manga" + ContentType.MANHWA -> "manhwa" + ContentType.MANHUA -> "manhua" + ContentType.COMICS -> "comics" + else -> "" + } + ) + } + } + + val doc = webClient.httpGet(url.toAbsoluteUrl(domain)).parseHtml() + return doc.select("div.post").map { element -> + val href = element.selectFirstOrThrow("h6 a").attrAsRelativeUrl("href") + Manga( + id = generateUid(href), + title = element.selectFirst("h6 a")?.text().orEmpty(), + altTitles = emptySet(), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + contentRating = null, + coverUrl = element.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("tr:has(th:contains(Status)) > td")?.text().orEmpty() + val chapterListUrl = manga.url.toAbsoluteUrl(domain).removeSuffix("/") + "/chapterlist/" + val chapterDoc = webClient.httpGet(chapterListUrl).parseHtml() + return manga.copy( + tags = doc.select("a[href*=genres]").mapToSet { + MangaTag( + key = it.attr("href").substringAfterLast("/"), + title = it.text(), + source = source + ) + }, + description = doc.select("div.card-body:has(h5:contains(Synopsis)) p") + .filter { p -> p.text().isNotBlank() } + .joinToString("\n") { it.text() }, + state = when { + statusText.contains("publishing", true) -> MangaState.ONGOING + statusText.contains("completed", true) -> MangaState.FINISHED + else -> null + }, + chapters = chapterDoc.select("a.seenchapter").mapChapters(reversed = true) { i, el -> + val href = el.attrAsRelativeUrl("href") + MangaChapter( + id = generateUid(href), + title = el.text(), + number = i + 1f, + volume = 0, + url = href, + scanlator = null, + uploadDate = 0L, + branch = null, + source = source + ) + } + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + return doc.select(".manga-child-the-content img").map { + val url = it.requireSrc() + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source + ) + } + } + + private fun fetchTags() = arraySetOf( + MangaTag("Action", "action", source), + MangaTag("Adventure", "adventure", source), + MangaTag("Comedy", "comedy", source), + MangaTag("Crime", "crime", source), + MangaTag("Drama", "drama", source), + MangaTag("Fantasy", "fantasy", source), + MangaTag("High School", "high-school", source), + MangaTag("Sports", "sports", source), + MangaTag("Shonen", "shonen", source), + MangaTag("Martial Arts", "martial-arts", source), + MangaTag("Romance", "romance", source), + ) +} 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 new file mode 100644 index 00000000..d8fbc601 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/tr/ElderManga.kt @@ -0,0 +1,202 @@ +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("ELDERMANGA", "Elder Manga", "tr") +internal class ElderManga(context: MangaLoaderContext): + LegacyPagedMangaParser(context, MangaParserSource.ELDERMANGA, 25) { + + override val configKeyDomain = ConfigKey.Domain("eldermanga.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, + ) + + 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" + 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 57f33465..43efbb54 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 @@ -57,7 +57,7 @@ internal class UzayManga(context: MangaLoaderContext): append(page.toString()) if (!filter.query.isNullOrEmpty()) { - append("&search") + append("&search=") append(filter.query.urlEncoded()) } @@ -199,4 +199,4 @@ internal class UzayManga(context: MangaLoaderContext): ) } } -} \ No newline at end of file +}