diff --git a/src/all/nhentai/build.gradle b/src/all/nhentai/build.gradle index 6652a67bf..a53362fe8 100644 --- a/src/all/nhentai/build.gradle +++ b/src/all/nhentai/build.gradle @@ -2,16 +2,11 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' ext { - appName = 'Tachiyomi: nhentai' + appName = 'Tachiyomi: NHentai' pkgNameSuffix = 'all.nhentai' - extClass = '.NHJapanese; .NHEnglish; .NHChinese; .NHSpeechless; .NHCzech; .NHEsperanto; .NHMongolian; .NHSlovak; .NHArabic; .NHUkrainian' - extVersionCode = 3 + extClass = '.NHEnglish; .NHJapanese; .NHChinese' + extVersionCode = 4 libVersion = '1.2' } -dependencies { - compileOnly 'com.google.code.gson:gson:2.8.2' - compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0' -} - apply from: "$rootDir/common.gradle" diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/MetadataCopier.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/MetadataCopier.kt deleted file mode 100644 index de5c592ca..000000000 --- a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/MetadataCopier.kt +++ /dev/null @@ -1,72 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.nhentai - -import eu.kanade.tachiyomi.source.model.SManga -import java.text.SimpleDateFormat -import java.util.* - -private val ONGOING_SUFFIX = arrayOf( - "[ongoing]", - "(ongoing)", - "{ongoing}" -) - -private val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US) - -fun NHentaiMetadata.copyTo(manga: SManga) { - url?.let { manga.url = it } - - mediaId?.let { mid -> - NHentaiMetadata.typeToExtension(thumbnailImageType)?.let { - manga.thumbnail_url = "https://t.nhentai.net/galleries/$mid/thumb.$it" - } - } - - manga.title = englishTitle ?: japaneseTitle ?: shortTitle!! - - //Set artist (if we can find one) - tags["artist"]?.let { - if (it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name) - } - - tags["category"]?.let { - if (it.isNotEmpty()) manga.genre = it.joinToString(transform = Tag::name) - } - - //Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes - //We default to completed - manga.status = SManga.COMPLETED - englishTitle?.let { t -> - if (ONGOING_SUFFIX.any { - t.endsWith(it, ignoreCase = true) - }) manga.status = SManga.ONGOING - } - - val titleDesc = StringBuilder() - englishTitle?.let { titleDesc += "English Title: $it\n" } - japaneseTitle?.let { titleDesc += "Japanese Title: $it\n" } - shortTitle?.let { titleDesc += "Short Title: $it\n" } - - val detailsDesc = StringBuilder() - uploadDate?.let { detailsDesc += "Upload Date: ${EX_DATE_FORMAT.format(Date(it))}\n" } - pageImageTypes.size.let { detailsDesc += "Length: $it pages\n" } - favoritesCount?.let { detailsDesc += "Favorited: $it times\n" } - scanlator?.nullIfBlank()?.let { detailsDesc += "Scanlator: $it\n" } - - val tagsDesc = buildTagsDescription(this) - - manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) - .filter(String::isNotBlank) - .joinToString(separator = "\n") -} - -private fun buildTagsDescription(metadata: NHentaiMetadata) - = StringBuilder("Tags:\n").apply { - //BiConsumer only available in Java 8, we have to use destructuring here - metadata.tags.forEach { (namespace, tags) -> - if (tags.isNotEmpty()) { - val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" }) - this += "▪ $namespace: $joinedTags\n" - } - } -} - diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHFilters.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHFilters.kt new file mode 100644 index 000000000..5179825cd --- /dev/null +++ b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHFilters.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.extension.all.nhentai + +import eu.kanade.tachiyomi.source.model.Filter + +class SortFilter : Filter.Select<String>("Sort", arrayOf("Date", "Popular")) \ No newline at end of file diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHLangs.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHLangs.kt index 4e90a4ddc..76ac099c3 100644 --- a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHLangs.kt +++ b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHLangs.kt @@ -1,29 +1,5 @@ package eu.kanade.tachiyomi.extension.all.nhentai -/** - * NHentai languages - */ - -class NHJapanese : NHentai("ja", "japanese") class NHEnglish : NHentai("en", "english") +class NHJapanese : NHentai("ja", "japanese") class NHChinese : NHentai("zh", "chinese") -class NHSpeechless : NHentai("none", "speechless") -class NHCzech : NHentai("cs", "czech") -class NHEsperanto : NHentai("eo", "esperanto") -class NHMongolian : NHentai("mn", "mongolian") -class NHSlovak : NHentai("sk", "slovak") -class NHArabic : NHentai("ar", "arabic") -class NHUkrainian : NHentai("uk", "ukrainian") - -fun getAllNHentaiLanguages() = listOf( - NHJapanese(), - NHEnglish(), - NHChinese(), - NHSpeechless(), - NHCzech(), - NHEsperanto(), - NHMongolian(), - NHSlovak(), - NHArabic(), - NHUkrainian() -) diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtil.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtil.kt deleted file mode 100644 index 23e3a3546..000000000 --- a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtil.kt +++ /dev/null @@ -1,17 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.nhentai - -/** - * Append Strings to StringBuilder with '+' operator - */ -operator fun StringBuilder.plusAssign(other: String) { - append(other) -} - -/** - * Return null if String is blank, otherwise returns the original String - * @returns null if the String is blank, otherwise returns the original String - */ -fun String?.nullIfBlank(): String? = if (isNullOrBlank()) - null -else - this 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 new file mode 100644 index 000000000..68b6ac03a --- /dev/null +++ b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtils.kt @@ -0,0 +1,91 @@ +package eu.kanade.tachiyomi.extension.all.nhentai + +import org.jsoup.nodes.Document +import java.lang.StringBuilder +import java.text.SimpleDateFormat + +class NHUtils { + companion object { + fun getArtists(document: Document): String { + val stringBuilder = StringBuilder() + val artists = document.select("#tags > div:nth-child(4) > span > a") + + artists.forEach { + stringBuilder.append(cleanTag(it.text())) + + if (it != artists.last()) + stringBuilder.append(", ") + } + + return stringBuilder.toString() + } + + fun getGroups(document: Document): String? { + val stringBuilder = StringBuilder() + val groups = document.select("#tags > div:nth-child(5) > span > a") + + groups.forEach { + stringBuilder.append(cleanTag(it.text())) + + if (it != groups.last()) + stringBuilder.append(", ") + } + + return if (stringBuilder.toString().isEmpty()) null else stringBuilder.toString() + } + + fun getTags(document: Document): String { + val stringBuilder = StringBuilder() + val parodies = document.select("#tags > div:nth-child(1) > span > a") + val characters = document.select("#tags > div:nth-child(2) > span > a") + val tags = document.select("#tags > div:nth-child(3) > span > a") + + if (parodies.size > 0) { + stringBuilder.append("Parodies: ") + + parodies.forEach { + stringBuilder.append(cleanTag(it.text())) + + if (it != parodies.last()) + stringBuilder.append(", ") + } + + stringBuilder.append("\n\n") + } + + if (characters.size > 0) { + stringBuilder.append("Characters: ") + + characters.forEach { + stringBuilder.append(cleanTag(it.text())) + + if (it != characters.last()) + stringBuilder.append(", ") + } + + stringBuilder.append("\n\n") + } + + if (tags.size > 0) { + stringBuilder.append("Tags: ") + + tags.forEach { + stringBuilder.append(cleanTag(it.text())) + + if (it != tags.last()) + stringBuilder.append(", ") + } + } + + return stringBuilder.toString() + } + + 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 + } + + private fun cleanTag(tag: String): String = tag.replace(Regex("\\(.*\\)"), "").trim() + } +} \ No newline at end of file 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 1234dc6eb..c53cd6784 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 @@ -1,216 +1,122 @@ package eu.kanade.tachiyomi.extension.all.nhentai -import android.net.Uri -import com.github.salomonbrys.kotson.* -import com.google.gson.JsonObject -import com.google.gson.JsonParser +import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.Companion.getArtists +import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.Companion.getGroups +import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.Companion.getTags +import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.Companion.getTime import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.source.model.* -import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup import okhttp3.Request import okhttp3.Response -import rx.Observable - -/** - * NHentai source - */ - -open class NHentai(override val lang: String, val nhLang: String) : HttpSource() { - override val name = "nhentai" - - override val baseUrl = "https://nhentai.net" +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.net.URLEncoder +open class NHentai(override val lang: String, private val nhLang: String) : ParsedHttpSource() { + final override val baseUrl = "https://nhentai.net" + override val name = "NHentai" override val supportsLatest = true + override val client = network.cloudflareClient - //TODO There is currently no way to get the most popular mangas - //TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen - override fun fetchPopularManga(page: Int) = fetchLatestUpdates(page) + private val searchUrl = "$baseUrl/search" - override fun popularMangaRequest(page: Int) - = throw UnsupportedOperationException("This method should not be called!") + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Not used") - override fun popularMangaParse(response: Response) - = throw UnsupportedOperationException("This method should not be called!") + override fun chapterListParse(response: Response): List<SChapter> { + val document = response.asJsoup() + val chapterList = mutableListOf<SChapter>() + val chapter = SChapter.create().apply { + name = "Chapter" + scanlator = getGroups(document) + date_upload = getTime(document) + setUrlWithoutDomain(response.request().url().encodedPath()) + } + + chapterList.add(chapter) + + return chapterList + } + + override fun chapterListRequest(manga: SManga): Request = GET("$baseUrl${manga.url}") + + override fun chapterListSelector() = throw UnsupportedOperationException("Not used") + + override fun getFilterList(): FilterList = FilterList(SortFilter()) + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.select("a").attr("href")) + title = element.select("a > div").text().replace("\"", "").trim() + } + + override fun latestUpdatesNextPageSelector() = "#content > section.pagination > a.next" + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/language/$nhLang/?page=$page") + + override fun latestUpdatesSelector() = "#content > div > div" + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + title = document.select("#info > h1").text().replace("\"", "").trim() + thumbnail_url = document.select("#cover > a > img").attr("data-src") + status = SManga.COMPLETED + artist = getArtists(document) + author = artist + description = getTags(document) + } + + override fun pageListParse(document: Document): List<Page> { + val pageElements = document.select("#thumbnail-container > div") + val pageList = mutableListOf<Page>() + + pageElements.forEach { + Page(pageList.size).run { + this.imageUrl = it.select("a > img").attr("data-src").replace("t.nh", "i.nh").replace("t.", ".") + + pageList.add(pageList.size, this) + } + } + + return pageList + } + + override fun pageListRequest(chapter: SChapter) = GET("$baseUrl${chapter.url}") + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.select("a").attr("href")) + title = element.select("a > div").text().replace("\"", "").trim() + } + + override fun popularMangaNextPageSelector() = "#content > section.pagination > a.next" + + override fun popularMangaRequest(page: Int) = GET("$searchUrl/?q=+english&sort=popular") + + override fun popularMangaSelector() = "#content > div > div" + + override fun searchMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.select("a").attr("href")) + title = element.select("a > div").text().replace("\"", "").trim() + } + + override fun searchMangaNextPageSelector() = "#content > section.pagination > a.next" override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val uri = Uri.parse("$baseUrl/api/galleries/search").buildUpon() - uri.appendQueryParameter("query", "language:$nhLang $query") - uri.appendQueryParameter("page", page.toString()) + val stringBuilder = StringBuilder() + stringBuilder.append(searchUrl) + stringBuilder.append("/?q=${URLEncoder.encode("$query +$nhLang", "UTF-8")}&") + filters.forEach { - if (it is UriFilter) - it.addToUri(uri) - } - return nhGet(uri.toString(), page) - } - - override fun searchMangaParse(response: Response) = parseResultPage(response) - - override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", getFilterList()) - - override fun latestUpdatesRequest(page: Int) - = throw UnsupportedOperationException("This method should not be called!") - - override fun latestUpdatesParse(response: Response) - = throw UnsupportedOperationException("This method should not be called!") - - override fun mangaDetailsParse(response: Response) - = parseGallery(jsonParser.parse(response.body()!!.string()).obj) - - //Hack so we can use a different URL for fetching manga details and opening the details in the browser - override fun fetchMangaDetails(manga: SManga) - = client.newCall(urlToDetailsRequest(manga.url)) - .asObservableSuccess() - .map { response -> - mangaDetailsParse(response).apply { initialized = true } + when (it) { + is SortFilter -> stringBuilder.append("sort=${it.values[it.state].toLowerCase()}&") } - - override fun mangaDetailsRequest(manga: SManga) = nhGet( baseUrl + manga.url ) - - fun urlToDetailsRequest(url: String) = nhGet("$baseUrl/api/gallery/${url.substringAfterLast('/')}") - - fun parseResultPage(response: Response): MangasPage { - val res = jsonParser.parse(response.body()!!.string()).obj - - res["error"]?.let { - throw RuntimeException("An error occurred while performing the search: $it") } - val results = res.getAsJsonArray("result")?.map { - parseGallery(it.obj) - } - val numPages = res["num_pages"].nullInt - if (results != null && numPages != null) - return MangasPage(results, numPages > response.request().tag() as Int) - return MangasPage(emptyList(), false) + stringBuilder.append("page=$page") + + return GET(stringBuilder.toString()) } - fun rawParseGallery(obj: JsonObject) = NHentaiMetadata().apply { - uploadDate = obj["upload_date"].nullLong - - favoritesCount = obj["num_favorites"].nullLong - - mediaId = obj["media_id"].nullString - - obj["title"].nullObj?.let { - japaneseTitle = it["japanese"].nullString - shortTitle = it["pretty"].nullString - englishTitle = it["english"].nullString - } - - obj["images"].nullObj?.let { - coverImageType = it["cover"]?.get("t").nullString - it["pages"].nullArray?.map { - it.nullObj?.get("t").nullString - }?.filterNotNull()?.let { - pageImageTypes.clear() - pageImageTypes.addAll(it) - } - thumbnailImageType = it["thumbnail"]?.get("t").nullString - } - - scanlator = obj["scanlator"].nullString - - id = obj["id"]?.asLong - - obj["tags"].nullArray?.map { - val asObj = it.obj - Pair(asObj["type"].nullString, asObj["name"].nullString) - }?.apply { - tags.clear() - }?.forEach { - if (it.first != null && it.second != null) - tags.getOrPut(it.first!!) { mutableListOf() }.add(Tag(it.second!!, false)) - }!! - } - - fun parseGallery(obj: JsonObject) = SManga.create().apply { - rawParseGallery(obj).copyTo(this) - } - - fun lazyLoadMetadata(url: String) = - client.newCall(urlToDetailsRequest(url)) - .asObservableSuccess() - .map { - rawParseGallery(jsonParser.parse(it.body()!!.string()).obj) - }!! - - override fun fetchChapterList(manga: SManga) - = Observable.just(listOf(SChapter.create().apply { - url = manga.url - name = "Chapter" - chapter_number = 1f - }))!! - - override fun fetchPageList(chapter: SChapter) - = lazyLoadMetadata(chapter.url).map { metadata -> - if (metadata.mediaId == null) emptyList() - else - metadata.pageImageTypes.mapIndexed { index, s -> - val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s) - Page(index, imageUrl!!, imageUrl) - } - }!! - - override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!! - - fun imageUrlFromType(mediaId: String, page: Int, t: String) = NHentaiMetadata.typeToExtension(t)?.let { - "https://i.nhentai.net/galleries/$mediaId/$page.$it" - } - - override fun chapterListParse(response: Response) - = throw UnsupportedOperationException("This method should not be called!") - - override fun pageListParse(response: Response) - = throw UnsupportedOperationException("This method should not be called!") - - override fun imageUrlParse(response: Response) - = throw UnsupportedOperationException("This method should not be called!") - - override fun getFilterList() = FilterList(SortFilter()) - - private class SortFilter : UriSelectFilter("Sort", "sort", arrayOf( - Pair("date", "Date"), - Pair("popular", "Popularity") - ), firstIsUnspecified = false) - - private fun nhGet(url: String, tag: Any? = null) = GET(url) - .newBuilder() - //Requested by nhentai admins to use a custom user agent - .header("User-Agent", - "Mozilla/5.0 (X11; Linux x86_64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) " + - "Chrome/56.0.2924.87 " + - "Safari/537.36 " + - "Tachiyomi/1.0") - .tag(tag).build()!! - - /** - * Class that creates a select filter. Each entry in the dropdown has a name and a display name. - * If an entry is selected it is appended as a query parameter onto the end of the URI. - * If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI. - */ - //vals: <name, display> - private open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>, - val firstIsUnspecified: Boolean = true, - defaultValue: Int = 0) : - Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter { - override fun addToUri(uri: Uri.Builder) { - if (state != 0 || !firstIsUnspecified) - uri.appendQueryParameter(uriParam, vals[state].first) - } - } - - /** - * Represents a filter that is able to modify a URI. - */ - private interface UriFilter { - fun addToUri(uri: Uri.Builder) - } - - companion object { - val jsonParser by lazy { - JsonParser() - } - } -} + override fun searchMangaSelector() = "#content > div > div" +} \ No newline at end of file diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentaiMetadata.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentaiMetadata.kt deleted file mode 100644 index d29f67e2a..000000000 --- a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentaiMetadata.kt +++ /dev/null @@ -1,44 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.nhentai - -/** - * NHentai metadata - */ - -class NHentaiMetadata { - - var id: Long? = null - - var url: String? - get() = id?.let { "/g/$it" } - set(a) { - id = a?.substringAfterLast('/')?.toLong() - } - - var uploadDate: Long? = null - - var favoritesCount: Long? = null - - var mediaId: String? = null - - var japaneseTitle: String? = null - var englishTitle: String? = null - var shortTitle: String? = null - - var coverImageType: String? = null - var pageImageTypes: MutableList<String> = mutableListOf() - var thumbnailImageType: String? = null - - var scanlator: String? = null - - val tags: MutableMap<String, MutableList<Tag>> = mutableMapOf() - - companion object { - fun typeToExtension(t: String?) = - when (t) { - "p" -> "png" - "j" -> "jpg" - else -> null - } - } -} - diff --git a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/Tag.kt b/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/Tag.kt deleted file mode 100644 index 005301bd9..000000000 --- a/src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/Tag.kt +++ /dev/null @@ -1,7 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.nhentai - -/** - * Simple tag model - */ - -data class Tag(val name: String, val light: Boolean)