Use SimplyHentai API (#9306)

This commit is contained in:
ObserverOfTime 2021-10-03 00:39:29 +03:00 committed by GitHub
parent 83f8e828bf
commit 421fde6d82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 294 additions and 205 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Simply Hentai' extName = 'Simply Hentai'
pkgNameSuffix = 'all.simplyhentai' pkgNameSuffix = 'all.simplyhentai'
extClass = '.SimplyHentaiFactory' extClass = '.SimplyHentaiFactory'
extVersionCode = 4 extVersionCode = 5
containsNsfw = true containsNsfw = true
} }

View File

@ -1,221 +1,229 @@
package eu.kanade.tachiyomi.extension.all.simplyhentai package eu.kanade.tachiyomi.extension.all.simplyhentai
import android.annotation.SuppressLint import android.app.Application
import android.net.Uri
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import uy.kohesive.injekt.Injekt
import org.jsoup.nodes.Element import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale
abstract class SimplyHentai( @ExperimentalSerializationApi
override val lang: String, open class SimplyHentai(override val lang: String) : ConfigurableSource, HttpSource() {
private val urlLang: String,
private val searchLang: String
) : ParsedHttpSource() {
override val name = "Simply Hentai" override val name = "Simply Hentai"
override val baseUrl = "https://old.simply-hentai.com" override val baseUrl = "https://www.simply-hentai.com"
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client = network.cloudflareClient
private val json: Json by injectLazy() override val versionId = 2
// Popular private val apiUrl = "https://api.simply-hentai.com/v3"
override fun popularMangaRequest(page: Int): Request { private val langName by lazy {
return GET("$baseUrl/album/language/$urlLang/$page/popularity/desc", headers) Locale.forLanguageTag(lang).displayName
} }
override fun popularMangaSelector() = "div.col-sm-3" private val json by lazy { Injekt.get<Json>() }
override fun popularMangaFromElement(element: Element): SManga { private val preferences by lazy {
val manga = SManga.create() Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
element.select("h3.object-title a").let {
manga.url = it.attr("href").substringAfter(baseUrl)
manga.title = it.text()
}
manga.thumbnail_url = element.select("img.img-responsive").attr("abs:data-src")
return manga
} }
override fun popularMangaNextPageSelector() = "a[rel=next]" override fun popularMangaRequest(page: Int) =
Uri.parse("$apiUrl/albums").buildUpon().run {
// Latest appendQueryParameter("si", "0")
appendQueryParameter("locale", lang)
override fun latestUpdatesRequest(page: Int): Request { appendQueryParameter("language", langName)
return GET("$baseUrl/album/language/$urlLang/$page", headers) appendQueryParameter("sort", "spotlight")
appendQueryParameter("page", page.toString())
GET(build().toString(), headers)
} }
override fun latestUpdatesSelector() = popularMangaSelector() override fun popularMangaParse(response: Response) =
response.decode<SHList<SHObject>>().run {
MangasPage(
data.map {
SManga.create().apply {
url = it.path
title = it.title
thumbnail_url = it.preview.sizes.thumb
}
},
pagination.next != null
)
}
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) override fun latestUpdatesRequest(page: Int) =
Uri.parse("$apiUrl/albums").buildUpon().run {
appendQueryParameter("si", "0")
appendQueryParameter("locale", lang)
appendQueryParameter("language", langName)
appendQueryParameter("sort", "newest")
appendQueryParameter("page", page.toString())
GET(build().toString(), headers)
}
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() override fun latestUpdatesParse(response: Response) =
popularMangaParse(response)
// Search override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
Uri.parse("$apiUrl/search/complex").buildUpon().run {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { appendQueryParameter("si", "0")
val url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder() appendQueryParameter("locale", lang)
.addQueryParameter("query", query) appendQueryParameter("query", query)
.addQueryParameter("language_ids[$searchLang]", searchLang) appendQueryParameter("page", page.toString())
.addQueryParameter("page", page.toString()) appendQueryParameter("blacklist", blacklist)
appendQueryParameter("filter[languages][0]", langName)
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
is GenreList -> { is SortFilter -> {
filter.state.forEach { appendQueryParameter("sort", filter.orders[filter.state])
if (it.state) url.addQueryParameter("tag_ids[${it.id}]", it.id) }
} is SeriesFilter -> filter.value?.let {
} appendQueryParameter("filter[series_title][0]", it)
is SeriesList -> { }
filter.state.forEach { is TagsFilter -> filter.value?.forEachIndexed { idx, tag ->
if (it.state) url.addQueryParameter("series_id[${it.id}]", it.id) appendQueryParameter("filter[tags][$idx]", tag.trim())
} }
} is ArtistsFilter -> filter.value?.forEachIndexed { idx, tag ->
is SortOrder -> { appendQueryParameter("filter[artists][$idx]", tag.trim())
url.addQueryParameter("sort", getSortOrder()[filter.state].second) }
is TranslatorsFilter -> filter.value?.forEachIndexed { idx, tag ->
appendQueryParameter("filter[translators][$idx]", tag.trim())
}
is CharactersFilter -> filter.value?.forEachIndexed { idx, tag ->
appendQueryParameter("filter[characters][$idx]", tag.trim())
}
else -> Unit
} }
} }
GET(build().toString(), headers)
} }
return GET(url.toString(), headers) override fun searchMangaParse(response: Response) =
response.decode<SHList<SHWrapper>>().run {
MangasPage(
data.map {
SManga.create().apply {
url = it.`object`.path
title = it.`object`.title
thumbnail_url = it.`object`.preview.sizes.thumb
}
},
pagination.next != null
)
} }
override fun searchMangaSelector() = popularMangaSelector() override fun mangaDetailsRequest(manga: SManga) =
GET(baseUrl + manga.url, headers)
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) override fun fetchMangaDetails(manga: SManga) =
client.newCall(chapterListRequest(manga))
.asObservableSuccess().map(::mangaDetailsParse)!!
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() override fun mangaDetailsParse(response: Response) =
SManga.create().apply {
// Details val album = response.decode<SHAlbum>().data
url = album.path
override fun mangaDetailsParse(document: Document): SManga { title = album.title
val manga = SManga.create() description = buildString {
if (!album.description.isNullOrEmpty()) {
document.select("div.padding-md-right-8").let { info -> append("${album.description}\n\n")
manga.artist = info.select("div.box-title:contains(Artists) + a").text()
manga.author = manga.artist
manga.genre = info.select("a[rel=tag]").joinToString { it.text() }
manga.description = info.select("div.link-box > div.box-title:contains(Series) ~ a").let { e ->
if (e.text().isNotEmpty()) "Series: ${e.joinToString { it.text() }}\n\n" else ""
} }
manga.description += info.select("div.link-box > div.box-title:contains(Characters) ~ a").let { e -> append("Series: ${album.series.title}\n")
if (e.text().isNotEmpty()) ("Characters: ${e.joinToString { it.text() }}\n\n") else "" album.characters.joinTo(this, prefix = "Characters: ") { it.title }
} }
manga.status = SManga.COMPLETED thumbnail_url = album.preview.sizes.thumb
} genre = album.tags.joinToString { it.title }
manga.thumbnail_url = document.select("div.col-xs-12 img.img-responsive").attr("abs:data-src") artist = album.artists.joinToString { it.title }
author = artist
return manga initialized = true
} }
// Chapters override fun chapterListRequest(manga: SManga) =
Uri.parse("$apiUrl/album").buildUpon().run {
appendEncodedPath(manga.url.split('/')[2])
appendQueryParameter("si", "0")
appendQueryParameter("locale", lang)
GET(build().toString(), headers)
}
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response) =
return listOf(
SChapter.create().apply { SChapter.create().apply {
val album = response.decode<SHAlbum>().data
name = "Chapter" name = "Chapter"
url = response.request.url.toString().removeSuffix("/").substringAfterLast("/") chapter_number = -1f
chapter_number = 1f url = "${album.path}/all-pages"
scanlator = album.translators.joinToString { it.title }
date_upload = dateFormat.parse(album.created_at)?.time ?: 0L
}.let(::listOf)
date_upload = response.asJsoup().select(".stat-container div:contains(Uploaded) div.bold")?.text().let { override fun pageListRequest(chapter: SChapter) =
DATE_FORMAT.parse(it!!)?.time Uri.parse("$apiUrl/album").buildUpon().run {
} ?: 0L appendEncodedPath(chapter.url.split('/')[2])
} appendQueryParameter("si", "0")
) appendQueryParameter("locale", lang)
GET(build().toString(), headers)
} }
override fun chapterListSelector() = "not used" override fun pageListParse(response: Response) =
response.decode<SHAlbum>().data.images.map {
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException("Not used") Page(it.page_num, "", it.sizes.full)
// Pages
override fun pageListRequest(chapter: SChapter): Request {
return GET("https://api.simply-hentai.com/v1/images/album/${chapter.url}", headersBuilder().add("X-Requested-With", "XMLHttpRequest").build())
} }
override fun pageListParse(response: Response): List<Page> {
val pages = mutableListOf<Page>()
json.parseToJsonElement(response.body!!.string()).jsonObject.forEach {
pages.add(
Page(
pages.size, "",
it.value.jsonObject["sizes"]!!.jsonObject["full"]!!.jsonPrimitive.content
)
)
}
return pages
}
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException("Not used")
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
// Filters
private class SortOrder(sortPairs: List<Pair<String, String>>) : Filter.Select<String>("Sort By", sortPairs.map { it.first }.toTypedArray())
private class SearchPair(name: String, val id: String = name) : Filter.CheckBox(name)
private class GenreList(searchVal: List<SearchPair>) : Filter.Group<SearchPair>("Genres", searchVal)
private class SeriesList(searchVal: List<SearchPair>) : Filter.Group<SearchPair>("Series", searchVal)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
SortOrder(getSortOrder()), SortFilter(),
GenreList(getGenreList()), SeriesFilter(),
SeriesList(getSeriesList()) Note("tags"),
TagsFilter(),
Note("artists"),
ArtistsFilter(),
Note("translators"),
TranslatorsFilter(),
Note("characters"),
CharactersFilter(),
) )
// "Relevance" should be empty, don't add a "Views" sort order override fun setupPreferenceScreen(screen: PreferenceScreen) {
private fun getSortOrder() = listOf( EditTextPreference(screen.context).apply {
Pair("Relevance", ""), key = "blacklist"
Pair("Popularity", "sort_value"), title = "Blacklist"
Pair("Upload Date", "created_at") summary = "Separate multiple tags with commas (,)"
)
// TODO: add more to getGenreList and getSeriesList setOnPreferenceChangeListener { _, newValue ->
private fun getGenreList() = listOf( preferences.edit().putString("blacklist", newValue as String).commit()
SearchPair("Solo Female", "4807"), }
SearchPair("Solo Male", "4805"), }.let(screen::addPreference)
SearchPair("Big Breasts", "2528"), }
SearchPair("Nakadashi", "2418"),
SearchPair("Blowjob", "64"),
SearchPair("Schoolgirl Uniform", "2522"),
SearchPair("Stockings", "33")
)
private fun getSeriesList() = listOf( private inline val blacklist: String
SearchPair("Original Work", "1093"), get() = preferences.getString("blacklist", "")!!
SearchPair("Kantai Collection", "1316"),
SearchPair("Touhou", "747"), private inline fun <reified T> Response.decode() =
SearchPair("Fate Grand Order", "2171"), json.decodeFromString<T>(body!!.string())
SearchPair("Idolmaster", "306"),
SearchPair("Granblue Fantasy", "2041"), override fun imageUrlParse(response: Response) =
SearchPair("Girls Und Panzer", "1324") throw UnsupportedOperationException("Not used")
)
companion object { companion object {
private val dateFormat =
@SuppressLint("SimpleDateFormat") SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.ROOT)
private val DATE_FORMAT = SimpleDateFormat("dd.MM.yyyy")
} }
} }

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.extension.all.simplyhentai
import kotlinx.serialization.Serializable
@Serializable
data class SHList<T>(val pagination: SHPagination, val data: List<T>)
@Serializable
data class SHPagination(val next: Int?)
@Serializable
data class SHWrapper(val `object`: SHObject)
@Serializable
data class SHObject(
val preview: SHImage,
val series: SHTag,
val slug: String,
val title: String
) {
val path by lazy { "/${series.slug}/$slug" }
}
@Serializable
data class SHImage(val page_num: Int, val sizes: SHSizes)
@Serializable
data class SHSizes(val full: String, val thumb: String)
@Serializable
data class SHTag(val slug: String, val title: String)
@Serializable
data class SHAlbum(val data: SHData)
@Serializable
data class SHData(
val artists: List<SHTag>,
val characters: List<SHTag>,
val created_at: String,
val description: String?,
val images: List<SHImage>,
val preview: SHImage,
val series: SHTag,
val slug: String,
val tags: List<SHTag>,
val title: String,
val translators: List<SHTag>
) {
val path by lazy { "/${series.slug}/$slug" }
}

View File

@ -1,34 +1,26 @@
package eu.kanade.tachiyomi.extension.all.simplyhentai package eu.kanade.tachiyomi.extension.all.simplyhentai
import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import kotlinx.serialization.ExperimentalSerializationApi
@Nsfw @ExperimentalSerializationApi @Nsfw
class SimplyHentaiFactory : SourceFactory { class SimplyHentaiFactory : SourceFactory {
override fun createSources(): List<Source> = listOf( override fun createSources() = listOf(
SimplyHentaiEN(), SimplyHentai("en"),
SimplyHentaiJA(), SimplyHentai("ja"),
SimplyHentaiZH(), SimplyHentai("zh"),
SimplyHentaiKO(), SimplyHentai("ko"),
SimplyHentaiES(), SimplyHentai("es"),
SimplyHentaiRU(), SimplyHentai("ru"),
SimplyHentaiFR(), SimplyHentai("fr"),
SimplyHentaiDE(), SimplyHentai("de"),
SimplyHentaiPT() object : SimplyHentai("pt-BR") {
)
}
class SimplyHentaiEN : SimplyHentai("en", "english", "1")
class SimplyHentaiJA : SimplyHentai("ja", "japanese", "2")
class SimplyHentaiZH : SimplyHentai("zh", "chinese", "11")
class SimplyHentaiKO : SimplyHentai("ko", "korean", "9")
class SimplyHentaiES : SimplyHentai("es", "spanish", "8")
class SimplyHentaiRU : SimplyHentai("ru", "russian", "7")
class SimplyHentaiFR : SimplyHentai("fr", "french", "3")
class SimplyHentaiDE : SimplyHentai("de", "german", "4")
class SimplyHentaiPT : SimplyHentai("pt-BR", "portuguese", "6") {
// The site uses a Portugal flag for the language, // The site uses a Portugal flag for the language,
// but the contents are in Brazilian Portuguese. // but the contents are in Brazilian Portuguese.
override val id: Long = 7265793330155215502 override val id = 23032005200449651
},
SimplyHentai("it"),
SimplyHentai("pl"),
)
} }

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.extension.all.simplyhentai
import eu.kanade.tachiyomi.source.model.Filter
class SortFilter(values: Array<String> = labels) : Filter.Select<String>("Sort by", values) {
val orders = arrayOf("", "upload-date", "popularity")
companion object {
val labels = arrayOf("Relevance", "Upload Date", "Popularity")
}
}
class SeriesFilter : Filter.Text("Series") {
inline val value: String?
get() = state.ifBlank { null }
}
class TagsFilter : Filter.Text("Tags") {
inline val value: List<String>?
get() = if (state.isBlank()) null else state.split(',')
}
class ArtistsFilter : Filter.Text("Artists") {
inline val value: List<String>?
get() = if (state.isBlank()) null else state.split(',')
}
class TranslatorsFilter : Filter.Text("Translators") {
inline val value: List<String>?
get() = if (state.isBlank()) null else state.split(',')
}
class CharactersFilter : Filter.Text("Characters") {
inline val value: List<String>?
get() = if (state.isBlank()) null else state.split(',')
}
class Note(type: String) : Filter.Header("Separate multiple $type with commas (,)")