Add url intent and id search for IMHentai (#8700)

* Add url intent and id search

* Slight refactoring and fix image loading
This commit is contained in:
Arraiment 2021-08-22 22:37:42 +08:00 committed by GitHub
parent 43177c7c10
commit 1e6d92e0a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 140 additions and 23 deletions

View File

@ -1,2 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" /> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".all.imhentai.IMHentaiUrlActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="imhentai.xxx"
android:pathPattern="/gallery/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -5,7 +5,7 @@ ext {
extName = 'IMHentai' extName = 'IMHentai'
pkgNameSuffix = 'all.imhentai' pkgNameSuffix = 'all.imhentai'
extClass = '.IMHentaiFactory' extClass = '.IMHentaiFactory'
extVersionCode = 2 extVersionCode = 3
libVersion = '1.2' libVersion = '1.2'
containsNsfw = true containsNsfw = true
} }

View File

@ -1,26 +1,29 @@
package eu.kanade.tachiyomi.extension.all.imhentai package eu.kanade.tachiyomi.extension.all.imhentai
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Elements import org.jsoup.select.Elements
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
class IMHentai(override val lang: String, private val imhLang: String) : ParsedHttpSource() { class IMHentai(override val lang: String, private val imhLang: String) : ParsedHttpSource() {
@ -65,6 +68,20 @@ class IMHentai(override val lang: String, private val imhLang: String) : ParsedH
// Search // Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith("id:")) {
val id = query.substringAfter("id:")
return client.newCall(GET("$baseUrl/gallery/$id/"))
.asObservableSuccess()
.map { response ->
val manga = mangaDetailsParse(response)
manga.url = "/gallery/$id/"
MangasPage(listOf(manga), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector() override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
@ -94,7 +111,8 @@ class IMHentai(override val lang: String, private val imhLang: String) : ParsedH
url.addQueryParameter(pair.second, toBinary(filter.state == index)) url.addQueryParameter(pair.second, toBinary(filter.state == index))
} }
} }
else -> { } else -> {
}
} }
} }
@ -119,6 +137,11 @@ class IMHentai(override val lang: String, private val imhLang: String) : ParsedH
} }
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.selectFirst("div.right_details > h1").text()
thumbnail_url = document.selectFirst("div.left_cover img").attr("abs:data-src")
val mangaInfoElement = document.select(".galleries_info") val mangaInfoElement = document.select(".galleries_info")
val infoMap = mangaInfoElement.select("li:not(.pages)").map { val infoMap = mangaInfoElement.select("li:not(.pages)").map {
it.select("span.tags_text").text().removeSuffix(":") to it.select(".tag") it.select("span.tags_text").text().removeSuffix(":") to it.select(".tag")
@ -149,20 +172,30 @@ class IMHentai(override val lang: String, private val imhLang: String) : ParsedH
// Chapters // Chapters
private fun pageLoadMetaParse(document: Document): String { private fun buildPageListRequest(document: Document): Request {
return document.select(".gallery_divider ~ input[type=\"hidden\"]").map { m -> val formBuilder = FormBody.Builder()
m.attr("id") to m.attr("value") .add("type", "2")
}.toMap().let { .add("visible_pages", "0")
listOf( // Extracts form data from webpage
document.select("div.gallery_divider ~ input[type=hidden]").forEach { element ->
val keys = listOf(
Pair("server", "load_server"), Pair("server", "load_server"),
Pair("u_id", "gallery_id"), Pair("u_id", "gallery_id"),
Pair("g_id", "load_id"), Pair("g_id", "load_id"),
Pair("img_dir", "load_dir"), Pair("img_dir", "load_dir"),
Pair("total_pages", "load_pages") Pair("total_pages", "load_pages")
).map { meta -> "${meta.first}=${it[meta.second]}" } )
.let { payload -> payload + listOf("type=2", "visible_pages=0") } for (key in keys) {
.joinToString("&") if (key.second == element.attr("id")) {
formBuilder.add(key.first, element.attr("value"))
}
}
} }
return Request.Builder()
.url("https://imhentai.xxx/inc/thumbs_loader.php")
.headers(pageLoadHeaders)
.post(formBuilder.build())
.build()
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
@ -181,27 +214,54 @@ class IMHentai(override val lang: String, private val imhLang: String) : ParsedH
// Pages // Pages
private val json: Json by injectLazy()
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val gifPages = mutableListOf<Int>()
return client.newCall(GET("$baseUrl${chapter.url}")) return client.newCall(GET("$baseUrl${chapter.url}"))
.asObservableSuccess() .asObservableSuccess()
.map { pageLoadMetaParse(it.asJsoup()) } .concatMap {
.map { it.toRequestBody("application/x-www-form-urlencoded; charset=UTF-8".toMediaTypeOrNull()) } val document = it.asJsoup()
.concatMap { client.newCall(POST(PAEG_LOAD_URL, pageLoadHeaders, it)).asObservableSuccess() } getGifPages(document, gifPages)
.map { pageListParse(it) } client.newCall(buildPageListRequest(document))
.asObservableSuccess()
}.map { response ->
apiPageListParse(response.asJsoup(), gifPages)
}
} }
override fun pageListParse(document: Document): List<Page> { private fun getGifPages(document: Document, gifPages: MutableList<Int>) {
return document.select("a").mapIndexed { i, element -> val imageFormats = document.selectFirst("script:containsData(var g_th)").data()
Page(i, element.attr("href"), element.select(".lazy.preloader[src]").attr("src").replace("t.", ".")) .substringAfter("$.parseJSON('").substringBefore("');").trim()
json.parseToJsonElement(imageFormats).jsonObject.forEach { pair ->
val isGif = pair.value.jsonPrimitive.content.startsWith("g")
if (isGif) gifPages.add(pair.key.toInt())
} }
} }
private fun apiPageListParse(document: Document, gifs: List<Int>): List<Page> {
return document.select("a").mapIndexed { i, element ->
Page(
i,
element.attr("href"),
if (gifs.any { page -> page == i + 1 }) {
element.select("img.lazy.preloader").attr("data-src").replace("t.jpg", ".gif")
} else {
element.select("img.lazy.preloader").attr("data-src").replace("t.", ".")
}
)
}
}
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException("Not used")
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
// Filters // Filters
private class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>, state: Int) : private class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>, state: Int) :
Filter.Select<String>("Sort By", sortOrderURIs.map { it.first }.toTypedArray(), state) Filter.Select<String>("Sort By", sortOrderURIs.map { it.first }.toTypedArray(), state)
private open class SearchFlagFilter(name: String, val uri: String, state: Boolean = true) : Filter.CheckBox(name, state) private open class SearchFlagFilter(name: String, val uri: String, state: Boolean = true) : Filter.CheckBox(name, state)
private class LanguageFilter(name: String, uri: String = name) : SearchFlagFilter(name, uri, false) private class LanguageFilter(name: String, uri: String = name) : SearchFlagFilter(name, uri, false)
private class LanguageFilters(flags: List<LanguageFilter>) : Filter.Group<LanguageFilter>("Other Languages", flags) private class LanguageFilters(flags: List<LanguageFilter>) : Filter.Group<LanguageFilter>("Other Languages", flags)
@ -261,7 +321,5 @@ class IMHentai(override val lang: String, private val imhLang: String) : ParsedH
const val LANGUAGE_KOREAN = "Korean" const val LANGUAGE_KOREAN = "Korean"
const val LANGUAGE_GERMAN = "German" const val LANGUAGE_GERMAN = "German"
const val LANGUAGE_RUSSIAN = "Russian" const val LANGUAGE_RUSSIAN = "Russian"
private const val PAEG_LOAD_URL: String = "https://imhentai.xxx/inc/thumbs_loader.php"
} }
} }

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.extension.all.imhentai
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://imhentai.xxx/gallery/xxxxxx intents and redirects them to
* the main Tachiyomi process.
*/
class IMHentaiUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "id:$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("IMHentaiUrlActivity", e.toString())
}
} else {
Log.e("IMHentaiUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}