Add nhentai source.

This commit is contained in:
NerdNumber9 2017-03-09 16:01:34 -05:00
parent 0a7812bb2c
commit c8a8eb0a4d
12 changed files with 366 additions and 22 deletions

View File

@ -43,6 +43,7 @@ TachiyomiEH is a fork of the [original Tachiyomi app](https://github.com/inorich
* E-Hentai * E-Hentai
* ExHentai * ExHentai
* PervEden * PervEden
* nhentai
TachiyomiEH is fully compatible with Tachiyomi source extensions. TachiyomiEH is fully compatible with Tachiyomi source extensions.
Backups from Tachiyomi are also compatible with TachiyomiEH (and vice versa). Backups from Tachiyomi are also compatible with TachiyomiEH (and vice versa).

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.YamlHttpSource import eu.kanade.tachiyomi.source.online.YamlHttpSource
import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.source.online.all.PervEden import eu.kanade.tachiyomi.source.online.all.PervEden
import eu.kanade.tachiyomi.source.online.english.* import eu.kanade.tachiyomi.source.online.english.*
import eu.kanade.tachiyomi.source.online.german.WieManga import eu.kanade.tachiyomi.source.online.german.WieManga
@ -99,6 +100,7 @@ open class SourceManager(private val context: Context) {
} }
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, "en") exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, "en")
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, "it") exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, "it")
exSrcs += NHentai(context)
return exSrcs return exSrcs
} }

View File

@ -164,10 +164,7 @@ class EHentai(override val id: Long,
exh = this@EHentai.exh exh = this@EHentai.exh
title = select("#gn").text().nullIfBlank()?.trim() title = select("#gn").text().nullIfBlank()?.trim()
altTitles.clear() altTitle = select("#gj").text().nullIfBlank()?.trim()
select("#gj").text().nullIfBlank()?.trim()?.let { newAltTitle ->
altTitles.add(newAltTitle)
}
thumbnailUrl = select("#gd1 img").attr("src").nullIfBlank()?.trim() thumbnailUrl = select("#gd1 img").attr("src").nullIfBlank()?.trim()

View File

@ -0,0 +1,227 @@
package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.long
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
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 exh.NHENTAI_SOURCE_ID
import exh.metadata.MetadataHelper
import exh.metadata.copyTo
import exh.metadata.models.NHentaiMetadata
import exh.metadata.models.Tag
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import timber.log.Timber
/**
* NHentai source
*/
class NHentai(context: Context) : HttpSource() {
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
//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
return fetchLatestUpdates(page)
}
override fun popularMangaRequest(page: Int): Request {
TODO("Currently unavailable!")
}
override fun popularMangaParse(response: Response): MangasPage {
TODO("Currently unavailable!")
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
//Currently we have no filters
//TODO Filter builder
val uri = Uri.parse("$baseUrl/api/galleries/search").buildUpon()
uri.appendQueryParameter("query", query)
uri.appendQueryParameter("page", page.toString())
return nhGet(uri.toString(), page)
}
override fun searchMangaParse(response: Response)
= parseResultPage(response)
override fun latestUpdatesRequest(page: Int): Request {
val uri = Uri.parse("$baseUrl/api/galleries/all").buildUpon()
uri.appendQueryParameter("page", page.toString())
return nhGet(uri.toString(), page)
}
override fun latestUpdatesParse(response: Response)
= parseResultPage(response)
override fun mangaDetailsParse(response: Response)
= parseGallery(jsonParser.parse(response.body().string()).asJsonObject)
override fun mangaDetailsRequest(manga: SManga)
= urlToDetailsRequest(manga.url)
fun urlToDetailsRequest(url: String)
= nhGet(baseUrl + "/api/gallery/" + url.split("/").last())
fun parseResultPage(response: Response): MangasPage {
val res = jsonParser.parse(response.body().string()).asJsonObject
val error = res.get("error")
if(error == null) {
val results = res.getAsJsonArray("result")?.map {
parseGallery(it.asJsonObject)
}
val numPages = res.get("num_pages")?.int
if(results != null && numPages != null)
return MangasPage(results, numPages < response.request().tag() as Int)
} else {
Timber.w("An error occurred while performing the search: $error")
}
return MangasPage(emptyList(), false)
}
fun rawParseGallery(obj: JsonObject) = NHentaiMetadata().apply {
uploadDate = obj.get("upload_date")?.notNull()?.long
favoritesCount = obj.get("num_favorites")?.notNull()?.long
mediaId = obj.get("media_id")?.notNull()?.string
obj.get("title")?.asJsonObject?.let {
japaneseTitle = it.get("japanese")?.notNull()?.string
shortTitle = it.get("pretty")?.notNull()?.string
englishTitle = it.get("english")?.notNull()?.string
}
obj.get("images")?.asJsonObject?.let {
coverImageType = it.get("cover")?.get("t")?.notNull()?.asString
it.get("pages")?.asJsonArray?.map {
it?.asJsonObject?.get("t")?.notNull()?.asString
}?.filterNotNull()?.let {
pageImageTypes.clear()
pageImageTypes.addAll(it)
}
thumbnailImageType = it.get("thumbnail")?.get("t")?.notNull()?.asString
}
scanlator = obj.get("scanlator")?.notNull()?.asString
id = obj.get("id")?.asLong
obj.get("tags")?.asJsonArray?.map {
val asObj = it.asJsonObject
Pair(asObj.get("type")?.string, asObj.get("name")?.string)
}?.apply {
tags.clear()
}?.forEach {
if(it.first != null && it.second != null)
tags.getOrPut(it.first!!, { ArrayList() }).add(Tag(it.second!!, false))
}
}
fun parseGallery(obj: JsonObject) = rawParseGallery(obj).let {
metadataHelper.writeGallery(it, id)
SManga.create().apply {
it.copyTo(this)
}
}
fun lazyLoadMetadata(url: String) =
Observable.fromCallable {
metadataHelper.fetchNhentaiMetadata(url)
?: client.newCall(urlToDetailsRequest(url))
.asObservableSuccess()
.map {
rawParseGallery(jsonParser.parse(it.body().string()).asJsonObject)
}.toBlocking().first()
}!!
override fun fetchChapterList(manga: SManga)
= lazyLoadMetadata(manga.url).map {
listOf(SChapter.create().apply {
url = manga.url
name = "Chapter"
date_upload = it.uploadDate ?: 0
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): List<SChapter> {
throw NotImplementedError("Unused method called!")
}
override fun pageListParse(response: Response): List<Page> {
throw NotImplementedError("Unused method called!")
}
override fun imageUrlParse(response: Response): String {
throw NotImplementedError("Unused method called!")
}
val appName by lazy {
context.getString(R.string.app_name)!!
}
fun nhGet(url: String, tag: Any? = null) = GET(url)
.newBuilder()
.header("User-Agent",
"Mozilla/5.0 (X11; Linux x86_64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/56.0.2924.87 " +
"Safari/537.36 " +
"$appName/${BuildConfig.VERSION_CODE}")
.tag(tag).build()!!
override val id = NHENTAI_SOURCE_ID
override val lang = "all"
override val name = "nhentai"
override val baseUrl = NHentaiMetadata.BASE_URL
override val supportsLatest = true
companion object {
val jsonParser by lazy {
JsonParser()
}
val metadataHelper by lazy {
MetadataHelper()
}
}
fun JsonElement.notNull() =
if(this is JsonNull)
null
else this
}

View File

@ -13,6 +13,8 @@ val EXH_METADATA_SOURCE_ID = LEWD_SOURCE_SERIES + 4
val PERV_EDEN_EN_SOURCE_ID = LEWD_SOURCE_SERIES + 5 val PERV_EDEN_EN_SOURCE_ID = LEWD_SOURCE_SERIES + 5
val PERV_EDEN_IT_SOURCE_ID = LEWD_SOURCE_SERIES + 6 val PERV_EDEN_IT_SOURCE_ID = LEWD_SOURCE_SERIES + 6
val NHENTAI_SOURCE_ID = LEWD_SOURCE_SERIES + 7
fun isLewdSource(source: Long) = source in 6900..6999 fun isLewdSource(source: Long) = source in 6900..6999
fun isEhSource(source: Long) = source == EH_SOURCE_ID fun isEhSource(source: Long) = source == EH_SOURCE_ID
@ -23,3 +25,5 @@ fun isExSource(source: Long) = source == EXH_SOURCE_ID
fun isPervEdenSource(source: Long) = source == PERV_EDEN_IT_SOURCE_ID fun isPervEdenSource(source: Long) = source == PERV_EDEN_IT_SOURCE_ID
|| source == PERV_EDEN_EN_SOURCE_ID || source == PERV_EDEN_EN_SOURCE_ID
fun isNhentaiSource(source: Long) = source == NHENTAI_SOURCE_ID

View File

@ -2,6 +2,7 @@ package exh.metadata
import exh.* import exh.*
import exh.metadata.models.ExGalleryMetadata import exh.metadata.models.ExGalleryMetadata
import exh.metadata.models.NHentaiMetadata
import exh.metadata.models.PervEdenGalleryMetadata import exh.metadata.models.PervEdenGalleryMetadata
import exh.metadata.models.SearchableGalleryMetadata import exh.metadata.models.SearchableGalleryMetadata
import io.paperdb.Paper import io.paperdb.Paper
@ -11,6 +12,7 @@ class MetadataHelper {
fun writeGallery(galleryMetadata: SearchableGalleryMetadata, source: Long) fun writeGallery(galleryMetadata: SearchableGalleryMetadata, source: Long)
= (if(isExSource(source) || isEhSource(source)) exGalleryBook() = (if(isExSource(source) || isEhSource(source)) exGalleryBook()
else if(isPervEdenSource(source)) pervEdenGalleryBook() else if(isPervEdenSource(source)) pervEdenGalleryBook()
else if(isNhentaiSource(source)) nhentaiGalleryBook()
else null)?.write(galleryMetadata.galleryUniqueIdentifier(), galleryMetadata)!! else null)?.write(galleryMetadata.galleryUniqueIdentifier(), galleryMetadata)!!
fun fetchEhMetadata(url: String, exh: Boolean): ExGalleryMetadata? fun fetchEhMetadata(url: String, exh: Boolean): ExGalleryMetadata?
@ -31,11 +33,18 @@ class MetadataHelper {
return pervEdenGalleryBook().read<PervEdenGalleryMetadata>(it.galleryUniqueIdentifier()) return pervEdenGalleryBook().read<PervEdenGalleryMetadata>(it.galleryUniqueIdentifier())
} }
fun fetchNhentaiMetadata(url: String) = NHentaiMetadata().let {
it.url = url
nhentaiGalleryBook().read<NHentaiMetadata>(it.galleryUniqueIdentifier())
}
fun fetchMetadata(url: String, source: Long): SearchableGalleryMetadata? { fun fetchMetadata(url: String, source: Long): SearchableGalleryMetadata? {
if(isExSource(source) || isEhSource(source)) { if(isExSource(source) || isEhSource(source)) {
return fetchEhMetadata(url, isExSource(source)) return fetchEhMetadata(url, isExSource(source))
} else if(isPervEdenSource(source)) { } else if(isPervEdenSource(source)) {
return fetchPervEdenMetadata(url, source) return fetchPervEdenMetadata(url, source)
} else if(isNhentaiSource(source)) {
return fetchNhentaiMetadata(url)
} else { } else {
return null return null
} }
@ -57,4 +66,6 @@ class MetadataHelper {
fun exGalleryBook() = Paper.book("gallery-ex")!! fun exGalleryBook() = Paper.book("gallery-ex")!!
fun pervEdenGalleryBook() = Paper.book("gallery-perveden")!! fun pervEdenGalleryBook() = Paper.book("gallery-perveden")!!
fun nhentaiGalleryBook() = Paper.book("gallery-nhentai")!!
} }

View File

@ -4,10 +4,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.PervEden import eu.kanade.tachiyomi.source.online.all.PervEden
import exh.metadata.models.ExGalleryMetadata import exh.metadata.models.*
import exh.metadata.models.PervEdenGalleryMetadata
import exh.metadata.models.SearchableGalleryMetadata
import exh.metadata.models.Tag
import exh.plusAssign import exh.plusAssign
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -17,8 +14,11 @@ import java.util.*
* Copies gallery metadata to a manga object * Copies gallery metadata to a manga object
*/ */
private const val ARTIST_NAMESPACE = "artist" private const val EH_ARTIST_NAMESPACE = "artist"
private const val AUTHOR_NAMESPACE = "author" private const val EH_AUTHOR_NAMESPACE = "author"
private const val NHENTAI_ARTIST_NAMESPACE = "artist"
private const val NHENTAI_CATEGORIES_NAMESPACE = "category"
private val ONGOING_SUFFIX = arrayOf( private val ONGOING_SUFFIX = arrayOf(
"[ongoing]", "[ongoing]",
@ -43,17 +43,17 @@ fun ExGalleryMetadata.copyTo(manga: SManga) {
//No title bug? //No title bug?
val titleObj = if(prefs.useJapaneseTitle().getOrDefault()) val titleObj = if(prefs.useJapaneseTitle().getOrDefault())
altTitles.firstOrNull() ?: title altTitle ?: title
else else
title title
titleObj?.let { manga.title = it } titleObj?.let { manga.title = it }
//Set artist (if we can find one) //Set artist (if we can find one)
tags[ARTIST_NAMESPACE]?.let { tags[EH_ARTIST_NAMESPACE]?.let {
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name) if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
} }
//Set author (if we can find one) //Set author (if we can find one)
tags[AUTHOR_NAMESPACE]?.let { tags[EH_AUTHOR_NAMESPACE]?.let {
if(it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name) if(it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name)
} }
//Set genre //Set genre
@ -73,7 +73,7 @@ fun ExGalleryMetadata.copyTo(manga: SManga) {
//Build a nice looking description out of what we know //Build a nice looking description out of what we know
val titleDesc = StringBuilder() val titleDesc = StringBuilder()
title?.let { titleDesc += "Title: $it\n" } title?.let { titleDesc += "Title: $it\n" }
altTitles.firstOrNull()?.let { titleDesc += "Alternate Title: $it\n" } altTitle?.let { titleDesc += "Alternate Title: $it\n" }
val detailsDesc = StringBuilder() val detailsDesc = StringBuilder()
uploader?.let { detailsDesc += "Uploader: $it\n" } uploader?.let { detailsDesc += "Uploader: $it\n" }
@ -93,7 +93,6 @@ fun ExGalleryMetadata.copyTo(manga: SManga) {
detailsDesc += "\n" detailsDesc += "\n"
} }
val tagsDesc = buildTagsDescription(this) val tagsDesc = buildTagsDescription(this)
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString()) manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
@ -146,6 +145,55 @@ fun PervEdenGalleryMetadata.copyTo(manga: SManga) {
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
fun NHentaiMetadata.copyTo(manga: SManga) {
url?.let { manga.url = it }
//TODO next update allow this to be changed to use HD covers
if(mediaId != null)
NHentaiMetadata.typeToExtension(thumbnailImageType)?.let {
manga.thumbnail_url = "https://t.nhentai.net/galleries/$mediaId/thumb.$it"
}
manga.title = englishTitle ?: japaneseTitle ?: shortTitle!!
//Set artist (if we can find one)
tags[NHENTAI_ARTIST_NAMESPACE]?.let {
if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
}
tags[NHENTAI_CATEGORIES_NAMESPACE]?.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 ->
ONGOING_SUFFIX.find {
t.endsWith(it, ignoreCase = true)
}?.let {
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: SearchableGalleryMetadata) private fun buildTagsDescription(metadata: SearchableGalleryMetadata)
= StringBuilder("Tags:\n").apply { = StringBuilder("Tags:\n").apply {
//BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags' //BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'

View File

@ -14,6 +14,9 @@ class ExGalleryMetadata : SearchableGalleryMetadata() {
var thumbnailUrl: String? = null var thumbnailUrl: String? = null
var title: String? = null
var altTitle: String? = null
var genre: String? = null var genre: String? = null
var datePosted: Long? = null var datePosted: Long? = null
@ -27,6 +30,7 @@ class ExGalleryMetadata : SearchableGalleryMetadata() {
var ratingCount: Int? = null var ratingCount: Int? = null
var averageRating: Double? = null var averageRating: Double? = null
override fun getTitles() = listOf(title, altTitle).filterNotNull()
private fun splitGalleryUrl() private fun splitGalleryUrl()
= url?.let { = url?.let {

View File

@ -0,0 +1,48 @@
package exh.metadata.models
/**
* NHentai metadata
*/
class NHentaiMetadata : SearchableGalleryMetadata() {
var id: Long? = null
var url get() = id?.let { "$BASE_URL/g/$it" }
set(a) {
a?.let {
id = a.split("/").last().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
override fun galleryUniqueIdentifier(): String? = "NHENTAI-$id"
override fun getTitles() = listOf(japaneseTitle, englishTitle, shortTitle).filterNotNull()
companion object {
val BASE_URL = "https://nhentai.net"
fun typeToExtension(t: String?) =
when(t) {
"p" -> "png"
"j" -> "jpg"
else -> null
}
}
}

View File

@ -6,6 +6,9 @@ class PervEdenGalleryMetadata : SearchableGalleryMetadata() {
var url: String? = null var url: String? = null
var thumbnailUrl: String? = null var thumbnailUrl: String? = null
var title: String? = null
var altTitles: MutableList<String> = mutableListOf()
var artist: String? = null var artist: String? = null
var type: String? = null var type: String? = null
@ -16,6 +19,8 @@ class PervEdenGalleryMetadata : SearchableGalleryMetadata() {
var lang: String? = null var lang: String? = null
override fun getTitles() = listOf(title).plus(altTitles).filterNotNull()
private fun splitGalleryUrl() private fun splitGalleryUrl()
= url?.let { = url?.let {
Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank) Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)

View File

@ -9,11 +9,10 @@ import java.util.HashMap
abstract class SearchableGalleryMetadata { abstract class SearchableGalleryMetadata {
var uploader: String? = null var uploader: String? = null
var title: String? = null
val altTitles: MutableList<String> = mutableListOf()
//Being specific about which classes are used in generics to make deserialization easier //Being specific about which classes are used in generics to make deserialization easier
val tags: HashMap<String, ArrayList<Tag>> = HashMap() val tags: HashMap<String, ArrayList<Tag>> = HashMap()
abstract fun galleryUniqueIdentifier(): String? abstract fun galleryUniqueIdentifier(): String?
abstract fun getTitles(): List<String>
} }

View File

@ -28,14 +28,12 @@ class SearchEngine {
return true return true
} }
val cachedLowercaseTitle = metadata.title?.toLowerCase() val cachedTitles = metadata.getTitles().map(String::toLowerCase)
val cachedLowercaseAltTitles = metadata.altTitles.map(String::toLowerCase)
for(component in query) { for(component in query) {
if(component is Text) { if(component is Text) {
//Match title //Match title
if (component.asRegex().test(cachedLowercaseTitle) if (cachedTitles.find { component.asRegex().test(it) } != null) {
|| cachedLowercaseAltTitles.find { component.asRegex().test(it) } != null) {
continue continue
} }
//Match tags //Match tags