Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New source pull request + Update domain #1030

Merged
merged 5 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package org.koitharu.kotatsu.parsers.site.vi

import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.PagedMangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.*
import org.koitharu.kotatsu.parsers.util.json.*
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.network.UserAgents
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.Inflater

@MangaSourceParser("CUUTRUYEN", "CuuTruyen", "vi")
internal class CuuTruyenParser(context: MangaLoaderContext) : PagedMangaParser(context, MangaParserSource.CUUTRUYEN, 20), Interceptor {

override val configKeyDomain = ConfigKey.Domain("cuutruyen.net", "nettrom.com", "hetcuutruyen.net", "cuutruyent9sv7.xyz")

override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.NEWEST,
)

override fun getRequestHeaders(): Headers = Headers.Builder()
.add("User-Agent", UserAgents.KOTATSU)
.build()

private val decryptionKey = "3141592653589793"
dragonx943 marked this conversation as resolved.
Show resolved Hide resolved
private val itemsPerPage: Int = 20

override suspend fun getAvailableTags(): Set<MangaTag> = emptySet()

private suspend fun getListPage(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val page = offset / itemsPerPage + 1
val url = buildString {
if (!query.isNullOrEmpty()) {
append("$domain/api/v2/mangas/search")
append("?q=")
append(query.urlEncoded())
append("&page=")
append(page.toString())
append("&per_page=")
append(itemsPerPage.toString())
} else {
append("$domain/api/v2/mangas")
when (sortOrder) {
SortOrder.UPDATED -> append("/recently_updated")
SortOrder.POPULARITY -> append("/top")
SortOrder.NEWEST -> append("/recently_updated")
else -> append("/recently_updated")
}
append("?page=")
append(page.toString())
}
}

val json = webClient.httpGet(url).parseJson()
val data = json.optJSONObject("data") ?: throw ParseException("Invalid response", url)

return data.getJSONArray("data").mapJSON { jo ->
Manga(
id = generateUid(jo.getLong("id")),
url = "/api/v2/mangas/${jo.getLong("id")}",
publicUrl = "$domain/manga/${jo.getLong("id")}",
title = jo.getString("name"),
altTitle = null,
coverUrl = jo.getString("cover_url"),
largeCoverUrl = jo.getString("cover_mobile_url"),
author = jo.getStringOrNull("author_name"),
tags = emptySet(),
state = null,
description = null,
isNsfw = false,
source = source,
rating = RATING_UNKNOWN,
)
}
}

override suspend fun getDetails(manga: Manga): Manga {
val url = domain + manga.url
val json = webClient.httpGet(url).parseJson().getJSONObject("data")
?: throw ParseException("Invalid response", url)

return manga.copy(
description = json.getString("description"),
chapters = json.getJSONArray("chapters").mapJSON { jo ->
MangaChapter(
id = generateUid(jo.getLong("id")),
name = jo.getString("name"),
number = jo.getInt("number"),
url = "/api/v2/chapters/${jo.getLong("id")}",
scanlator = jo.optString("group_name"),
uploadDate = parseChapterDate(jo.getString("created_at")),
branch = null,
source = source,
)
}.reversed(),
)
}

override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val url = "$domain${chapter.url}"
val json = webClient.httpGet(url).parseJson().getJSONObject("data")
?: throw ParseException("Invalid response", url)

return json.getJSONArray("pages").mapJSON { jo ->
val imageUrl = jo.getString("image_url")
MangaPage(
id = generateUid(jo.getLong("id")),
url = imageUrl,
preview = null,
source = source,
)
}
}

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)

if (!request.url.host.contains("cuutruyen.net")) {
return response
}

val body = response.body ?: return response
val contentType = body.contentType()
val bytes = body.bytes()

val decrypted = try {
decrypt(bytes)
} catch (e: Exception) {
bytes
}

val decompressed = try {
decompress(decrypted)
} catch (e: Exception) {
decrypted
}

val newBody = decompressed.toResponseBody(contentType)
return response.newBuilder().body(newBody).build()
}

private fun decrypt(input: ByteArray): ByteArray {
val key = decryptionKey.toByteArray()
return input.mapIndexed { index, byte ->
(byte.toInt() xor key[index % key.size].toInt()).toByte()
}.toByteArray()
}

private fun decompress(input: ByteArray): ByteArray {
val inflater = Inflater()
inflater.setInput(input, 0, input.size)
val outputStream = ByteArrayOutputStream(input.size)
val buffer = ByteArray(1024)
while (!inflater.finished()) {
val count = inflater.inflate(buffer)
outputStream.write(buffer, 0, count)
}
return outputStream.toByteArray()
}

private fun parseChapterDate(dateString: String): Long {
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).parse(dateString)?.time ?: 0L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package org.koitharu.kotatsu.parsers.site.wpcomics.vi

import androidx.collection.ArrayMap
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
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.SortOrder
import org.koitharu.kotatsu.parsers.site.wpcomics.WpComicsParser
import org.koitharu.kotatsu.parsers.util.*
import java.util.EnumSet

@MangaSourceParser("NETTRUYENHE", "NetTruyenHE", "vi")
internal class NetTruyenHE(context: MangaLoaderContext) :
WpComicsParser(context, MangaParserSource.NETTRUYENHE, "nettruyenhe.com", 20) {

override val isMultipleTagsSupported = true
override val isTagsExclusionSupported = true
override val listUrl = "/tim-kiem-nang-cao"
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
SortOrder.UPDATED,
SortOrder.POPULARITY,
SortOrder.RATING,
SortOrder.NEWEST,
SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC,
)

override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
val response =
when (filter) {
is MangaListFilter.Search -> {
val url = buildString {
append("https://")
append(domain)
append("/search")
append('/')
append(page.toString())
append('/')
append("?keyword=")
append(filter.query.urlEncoded())
}

val result = runCatchingCancellable { webClient.httpGet(url) }
val exception = result.exceptionOrNull()
if (exception is NotFoundException) {
return emptyList()
}
result.getOrThrow()
}

is MangaListFilter.Advanced -> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)

append('/')
append(page.toString())
append('/')

val tagQuery = filter.tags.joinToString(",") { it.key }
append("?genres=")
append(tagQuery)

val tagQueryExclude = filter.tagsExclude.joinToString(",") { it.key }
append("&notGenres=")
append(tagQueryExclude)

append("&sex=All")

filter.states.oneOrThrowIfMany()?.let {
append("&status=")
append(
when (it) {
MangaState.ONGOING -> "on-going"
MangaState.FINISHED -> "completed"
MangaState.PAUSED -> "on-hold"
MangaState.ABANDONED -> "canceled"
else -> "-1"
},
)
}

append("&chapter_count=0")

append("&sort=")
append(
when (filter.sortOrder) {
SortOrder.UPDATED -> "latest-updated"
SortOrder.POPULARITY -> "views"
SortOrder.NEWEST -> "new"
SortOrder.RATING -> "score"
SortOrder.ALPHABETICAL -> "az"
SortOrder.ALPHABETICAL_DESC -> "za"
else -> null
},
)
}

webClient.httpGet(url)
}

null -> {
val url = buildString {
append("https://")
append(domain)
append(listUrl)
append('/')
append(page.toString())
append('/')
append("?genres=&notGenres=&sex=All&status=&chapter_count=0&sort=latest-updated")
}
webClient.httpGet(url)
}
}

val tagMap = getOrCreateTagMap()
return parseMangaList(response.parseHtml(), tagMap)
}

override suspend fun getOrCreateTagMap(): ArrayMap<String, MangaTag> = mutex.withLock {
tagCache?.let { return@withLock it }
val doc = webClient.httpGet(listUrl.toAbsoluteUrl(domain)).parseHtml()
val tagItems = doc.select("div.genre-item")
val result = ArrayMap<String, MangaTag>(tagItems.size)
for (item in tagItems) {
val title = item.text()
val key = item.selectFirstOrThrow("span").attr("data-id")
if (key.isNotEmpty() && title.isNotEmpty()) {
result[title] = MangaTag(title = title, key = key, source = source)
}
}
tagCache = result
result
}
}
Loading