diff --git a/src/all/nhentai/build.gradle b/src/all/nhentai/build.gradle index 95e27664e..64d3e9dfa 100644 --- a/src/all/nhentai/build.gradle +++ b/src/all/nhentai/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'NHentai' extClass = '.NHFactory' - extVersionCode = 46 + extVersionCode = 47 isNsfw = true } diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHDto.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHDto.kt new file mode 100644 index 000000000..c73d92ba3 --- /dev/null +++ b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHDto.kt @@ -0,0 +1,37 @@ +package eu.kanade.tachiyomi.extension.all.nhentai + +import kotlinx.serialization.Serializable + +@Serializable +class Hentai( + var id: Int, + val images: Images, + val media_id: String, + val tags: List, + val title: Title, + val upload_date: Long, + val num_favorites: Long, +) + +@Serializable +class Title( + var english: String? = null, + val japanese: String? = null, + val pretty: String? = null, +) + +@Serializable +class Images( + val pages: List, +) + +@Serializable +class Image( + val t: String, +) + +@Serializable +class Tag( + val name: String, + val type: String, +) diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtils.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtils.kt index ccac6774a..fdd6cd565 100644 --- a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtils.kt +++ b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtils.kt @@ -1,63 +1,36 @@ package eu.kanade.tachiyomi.extension.all.nhentai -import org.jsoup.nodes.Document import org.jsoup.nodes.Element -import java.text.SimpleDateFormat object NHUtils { - fun getArtists(document: Document): String { - val artists = document.select("#tags > div:nth-child(4) > span > a .name") - return artists.joinToString(", ") { it.cleanTag() } + fun getArtists(data: Hentai): String { + val artists = data.tags.filter { it.type == "artist" } + return artists.joinToString(", ") { it.name } } - fun getGroups(document: Document): String? { - val groups = document.select("#tags > div:nth-child(5) > span > a .name") - return if (groups.isNotEmpty()) { - groups.joinToString(", ") { it.cleanTag() } - } else { - null + fun getGroups(data: Hentai): String? { + val groups = data.tags.filter { it.type == "group" } + return groups.joinToString(", ") { it.name }.takeIf { it.isBlank() } + } + + fun getTagDescription(data: Hentai): String { + val tags = data.tags.groupBy { it.type } + return buildString { + tags["category"]?.joinToString { it.name }?.let { + append("Categories: ", it, "\n") + } + tags["parody"]?.joinToString { it.name }?.let { + append("Parodies: ", it, "\n") + } + tags["character"]?.joinToString { it.name }?.let { + append("Characters: ", it, "\n\n") + } } } - fun getTagDescription(document: Document): String { - val stringBuilder = StringBuilder() - - val categories = document.select("#tags > div:nth-child(7) > span > a .name") - if (categories.isNotEmpty()) { - stringBuilder.append("Categories: ") - stringBuilder.append(categories.joinToString(", ") { it.cleanTag() }) - stringBuilder.append("\n\n") - } - - val parodies = document.select("#tags > div:nth-child(1) > span > a .name") - if (parodies.isNotEmpty()) { - stringBuilder.append("Parodies: ") - stringBuilder.append(parodies.joinToString(", ") { it.cleanTag() }) - stringBuilder.append("\n\n") - } - - val characters = document.select("#tags > div:nth-child(2) > span > a .name") - if (characters.isNotEmpty()) { - stringBuilder.append("Characters: ") - stringBuilder.append(characters.joinToString(", ") { it.cleanTag() }) - } - - return stringBuilder.toString() - } - - fun getTags(document: Document): String { - val tags = document.select("#tags > div:nth-child(3) > span > a .name") - return tags.map { it.cleanTag() }.sorted().joinToString(", ") - } - - fun getNumPages(document: Document): String { - return document.select("#tags > div:nth-child(8) > span > a .name").first()!!.cleanTag() - } - - fun getTime(document: Document): Long { - val timeString = document.toString().substringAfter("datetime=\"").substringBefore("\">").replace("T", " ") - - return SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSSZ").parse(timeString)?.time ?: 0L + fun getTags(data: Hentai): String { + val artists = data.tags.filter { it.type == "tag" } + return artists.joinToString(", ") { it.name } } private fun Element.cleanTag(): String = text().replace(Regex("\\(.*\\)"), "").trim() diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt index b1ad0849f..faab6dcc2 100644 --- a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt +++ b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt @@ -6,10 +6,8 @@ import androidx.preference.ListPreference import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getArtists import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getGroups -import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getNumPages import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getTagDescription import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getTags -import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getTime import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA import eu.kanade.tachiyomi.lib.randomua.getPrefUAType @@ -27,6 +25,8 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request @@ -36,6 +36,7 @@ import org.jsoup.nodes.Element import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy open class NHentai( override val lang: String, @@ -50,6 +51,8 @@ open class NHentai( override val supportsLatest = true + private val json: Json by injectLazy() + private val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } @@ -71,6 +74,7 @@ open class NHentai( } private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""") + private val dataRegex = Regex("""JSON.parse\("([^*]*)"\)""") private fun String.shortenTitle() = this.replace(shortenTitleRegex, "").trim() override fun setupPreferenceScreen(screen: PreferenceScreen) { @@ -103,7 +107,7 @@ open class NHentai( title = element.select("a > div").text().replace("\"", "").let { if (displayFullTitle) it.trim() else it.shortenTitle() } - thumbnail_url = element.select(".cover img").first()!!.let { img -> + thumbnail_url = element.selectFirst(".cover img")!!.let { img -> if (img.hasAttr("data-src")) img.attr("abs:data-src") else img.attr("abs:src") } } @@ -207,22 +211,25 @@ open class NHentai( override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() override fun mangaDetailsParse(document: Document): SManga { - val fullTitle = document.select("#info > h1").text().replace("\"", "").trim() + val script = document.selectFirst("script:containsData(JSON.parse)")!!.data() + val json = dataRegex.find(script)?.groupValues!![1] + + val data = json.parseAs() return SManga.create().apply { - title = if (displayFullTitle) fullTitle else fullTitle.shortenTitle() + title = if (displayFullTitle) data.title.english ?: data.title.japanese ?: data.title.pretty!! else data.title.pretty ?: (data.title.english ?: data.title.japanese)!!.shortenTitle() thumbnail_url = document.select("#cover > a > img").attr("data-src") status = SManga.COMPLETED - artist = getArtists(document) - author = getGroups(document) + artist = getArtists(data) + author = getGroups(data) // Some people want these additional details in description description = "Full English and Japanese titles:\n" - .plus("$fullTitle\n") - .plus("${document.select("div#info h2").text()}\n\n") - .plus("Pages: ${getNumPages(document)}\n") - .plus("Favorited by: ${document.select("div#info i.fa-heart ~ span span").text().removeSurrounding("(", ")")}\n") - .plus(getTagDescription(document)) - genre = getTags(document) + .plus("${data.title.english}\n") + .plus("${data.title.japanese}\n\n") + .plus("Pages: ${data.images.pages.size}\n") + .plus("Favorited by: ${data.num_favorites}\n") + .plus(getTagDescription(data)) + genre = getTags(data) update_strategy = UpdateStrategy.ONLY_FETCH_ONCE } } @@ -231,11 +238,16 @@ open class NHentai( override fun chapterListParse(response: Response): List { val document = response.asJsoup() + val script = document.selectFirst("script:containsData(JSON.parse)")!!.data() + + val json = dataRegex.find(script)?.groupValues!![1] + + val data = json.parseAs() return listOf( SChapter.create().apply { name = "Chapter" - scanlator = getGroups(document) - date_upload = getTime(document) + scanlator = getGroups(data) + date_upload = data.upload_date * 1000 setUrlWithoutDomain(response.request.url.encodedPath) }, ) @@ -246,11 +258,23 @@ open class NHentai( override fun chapterListSelector() = throw UnsupportedOperationException() override fun pageListParse(document: Document): List { - val script = document.select("script:containsData(media_server)").first()!!.data() - val mediaServer = Regex("""media_server\s*:\s*(\d+)""").find(script)?.groupValues!![1] + val script = document.selectFirst("script:containsData(media_server)")!!.data() + val script2 = document.selectFirst("script:containsData(JSON.parse)")!!.data() - return document.select("div.thumbs a > img").mapIndexed { i, img -> - Page(i, "", img.attr("abs:data-src").replace("t.nh", "i.nh").replace("t\\d+.nh".toRegex(), "i$mediaServer.nh").replace("t.", ".")) + val mediaServer = Regex("""media_server\s*:\s*(\d+)""").find(script)?.groupValues!![1] + val json = dataRegex.find(script2)?.groupValues!![1] + + val data = json.parseAs() + return data.images.pages.mapIndexed { i, image -> + Page( + i, + imageUrl = "${baseUrl.replace("https://", "https://i$mediaServer.")}/galleries/${data.media_id}/${i + 1}" + + when (image.t) { + "w" -> ".webp" + "p" -> ".png" + else -> ".jpg" + }, + ) } } @@ -303,6 +327,13 @@ open class NHentai( ), ) + private inline fun String.parseAs(): T { + return json.decodeFromString( + Regex("""\\u([0-9A-Fa-f]{4})""").replace(this) { + it.groupValues[1].toInt(16).toChar().toString() + }, + ) + } private open class UriPartFilter(displayName: String, val vals: Array>) : Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { fun toUriPart() = vals[state].second