diff --git a/.github/summary.yaml b/.github/summary.yaml index 20da1370..52427c46 100644 --- a/.github/summary.yaml +++ b/.github/summary.yaml @@ -1 +1 @@ -total: 1231 \ No newline at end of file +total: 1232 \ No newline at end of file diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/SnowMTL.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/SnowMTL.kt new file mode 100644 index 00000000..5cbf9dc6 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/SnowMTL.kt @@ -0,0 +1,195 @@ +package org.koitharu.kotatsu.parsers.site.en + +import org.json.JSONArray +import org.json.JSONObject +import org.jsoup.nodes.Document +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery +import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities +import org.koitharu.kotatsu.parsers.model.search.SearchCapability +import org.koitharu.kotatsu.parsers.model.search.SearchableField +import org.koitharu.kotatsu.parsers.model.search.QueryCriteria.* +import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.core.PagedMangaParser +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.exception.ParseException +import java.text.SimpleDateFormat +import java.util.* + +@MangaSourceParser("SNOWMTL", "SnowMtl", "en", ContentType.OTHER) +internal class SnowMTL(context: MangaLoaderContext): + PagedMangaParser(context, MangaParserSource.SNOWMTL, 24) { + + override val configKeyDomain = ConfigKey.Domain("snowmtl.ru") + + override val availableSortOrders: Set = EnumSet.of( + SortOrder.UPDATED, + SortOrder.POPULARITY, + SortOrder.NEWEST + ) + + override suspend fun getFilterOptions(): MangaListFilterOptions = MangaListFilterOptions() + + override val searchQueryCapabilities = MangaSearchQueryCapabilities( + SearchCapability( + field = SearchableField.TITLE_NAME, + criteriaTypes = setOf(Match::class), + isMultiple = false + ) + ) + + override suspend fun getListPage(query: MangaSearchQuery, page: Int): List { + val url = buildString { + append("https://") + append(domain) + append("/search") + append("?") + when (query.order) { + SortOrder.POPULARITY -> append("sort_by=views") + SortOrder.UPDATED -> append("sort_by=recent") + else -> append("sort_by=recent") + } + if (page > 1) { + append("&page=") + append(page) + } + query.criteria.find { it.field == SearchableField.TITLE_NAME }?.let { criteria -> + when (criteria) { + is Match -> { + append("&q=") + append(criteria.value.toString()) + } + is Include, + is Exclude, + is Range -> Unit // Not supported for this field + } + } + } + + val doc = webClient.httpGet(url).parseHtml() + return doc.select("div.grid.grid-cols-1.sm\\:grid-cols-2.lg\\:grid-cols-3.xl\\:grid-cols-4.gap-8.p-6 > div").map { div -> + val href = div.selectFirst("a")?.attr("href") ?: throw ParseException("Link not found", url) + Manga( + id = generateUid(href), + url = href, + publicUrl = href.toAbsoluteUrl(domain), + coverUrl = div.selectFirst("a > div > img")?.src().orEmpty(), + title = div.selectFirst("div > a > h3")?.text().orEmpty(), + altTitles = emptySet(), + rating = RATING_UNKNOWN, + tags = emptySet(), + authors = emptySet(), + state = null, + source = source, + contentRating = null, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val altTitles = doc.select("p:contains(Alternative Title)").firstOrNull()?.text() + ?.substringAfter("Alternative Title:") + ?.removeSurrounding("[", "]") + ?.split(",") + ?.map { it.trim().removeSurrounding("'", "'") } + ?.toSet() + ?: emptySet() + + val description = doc.select("p:contains(Synopsis)").firstOrNull()?.text() + ?.substringAfter("Synopsis:").orEmpty() + + val authors = doc.select("p:contains(Author:)").firstOrNull()?.text() + ?.substringAfter("Author:") + + val state = when (doc.select("p:contains(Status:)").firstOrNull()?.text()?.contains("Ongoing") == true) { + true -> MangaState.ONGOING + false -> MangaState.FINISHED + } + + val chaptersRoot = doc.selectFirst("section.bg-gray-800.rounded-lg.shadow-md.mt-8.p-6") + ?: throw ParseException("Chapters not found", manga.url) + + val chapters = chaptersRoot.select("ul > li").mapNotNull { li -> + val link = li.selectFirst("a") ?: return@mapNotNull null + val href = link.attrAsRelativeUrl("href") + val title = link.text() + val number = title.substringAfter("Chapter ").substringBefore(" ").toFloatOrNull() ?: 0f + + val dateString = li.select("span.text-gray-400").firstOrNull()?.text()?.trim() ?: "" + val uploadDate = if (dateString.isNotEmpty()) parseChapterDate(dateString) else 0L + + MangaChapter( + id = generateUid(href), + title = title, + number = number, + volume = 0, + url = href, + scanlator = null, + uploadDate = uploadDate, + branch = null, + source = source + ) + }.toList() + + return manga.copy( + description = description, + authors = setOfNotNull(authors), + state = state, + altTitles = altTitles, + chapters = chapters.reversed() + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = webClient.httpGet(chapter.url.toAbsoluteUrl(domain)).parseHtml() + val jsonText = doc.selectFirst("div#json-data")?.text() + ?: throw ParseException("JSON data not found", chapter.url) + + val imgUrls = Regex(""""img_url":\s*"([^"]+)"""").findAll(jsonText) + .map { it.groupValues[1] } + .toList() + + return imgUrls.map { imgUrl -> + val fullUrl = "https://$imgUrl" + MangaPage( + id = generateUid(fullUrl), + url = fullUrl, + preview = null, + source = source, + ) + } + } + + private fun parseChapterDate(dateString: String): Long { + val calendar = Calendar.getInstance() + return when { + "minute" in dateString -> { + val minutes = dateString.substringBefore(" minute").toInt() + calendar.add(Calendar.MINUTE, -minutes) + calendar.timeInMillis + } + "hour" in dateString -> { + val hours = dateString.substringBefore(" hour").toInt() + calendar.add(Calendar.HOUR_OF_DAY, -hours) + calendar.timeInMillis + } + "day" in dateString -> { + val days = dateString.substringBefore(" day").toInt() + calendar.add(Calendar.DAY_OF_YEAR, -days) + calendar.timeInMillis + } + else -> { + try { + val sdf = SimpleDateFormat("dd MMMM yyyy", Locale.ENGLISH) + sdf.timeZone = TimeZone.getTimeZone("UTC") + sdf.parse(dateString)?.time ?: 0L + } catch (e: Exception) { + 0L + } + } + } + } +} \ No newline at end of file