Ninehentai json migration and other changes (#8552)
* Migration to kotlinx.serialization * Refactoring and changed search implementation * Add url intent * Small fixes
This commit is contained in:
parent
d061b6597f
commit
24b583ac6a
|
@ -1,2 +1,23 @@
|
|||
<?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.ninehentai.NineHentaiUrlActivity"
|
||||
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="9hentai.to"
|
||||
android:pathPattern="/g/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'NineHentai'
|
||||
pkgNameSuffix = 'all.ninehentai'
|
||||
extClass = '.NineHentai'
|
||||
extVersionCode = 12
|
||||
extVersionCode = 13
|
||||
libVersion = '1.2'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.ninehentai
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
data class Manga(
|
||||
val id: Int,
|
||||
var title: String,
|
||||
val image_server: String,
|
||||
val tags: List<Tag>,
|
||||
val total_page: Int
|
||||
)
|
||||
|
||||
class Tag(
|
||||
val id: Int,
|
||||
name: String,
|
||||
val description: String = "null",
|
||||
val type: Int = 1
|
||||
) : Filter.TriState(name)
|
||||
|
||||
data class SearchRequest(
|
||||
val text: String,
|
||||
val page: Int,
|
||||
val sort: Int,
|
||||
val pages: Map<String, IntArray> = mapOf("range" to intArrayOf(0, 2000)),
|
||||
val tag: Map<String, Items>
|
||||
)
|
||||
|
||||
data class Items(
|
||||
val included: MutableList<Tag>,
|
||||
val excluded: MutableList<Tag>
|
||||
)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,5 @@
|
|||
package eu.kanade.tachiyomi.extension.all.ninehentai
|
||||
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
|
@ -19,12 +12,22 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Calendar
|
||||
|
||||
@Nsfw
|
||||
|
@ -40,67 +43,102 @@ class NineHentai : HttpSource() {
|
|||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val gson = Gson()
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return POST(baseUrl + SEARCH_URL, headers, buildRequestBody(page = page, sort = 1))
|
||||
// Builds request for /api/getBooks endpoint
|
||||
private fun buildSearchRequest(
|
||||
searchText: String = "",
|
||||
page: Int,
|
||||
sort: Int = 0,
|
||||
range: List<Int> = listOf(0, 2000),
|
||||
includedTags: List<Tag> = listOf(),
|
||||
excludedTags: List<Tag> = listOf()
|
||||
): Request {
|
||||
val request = SearchRequest(
|
||||
text = searchText,
|
||||
page = page - 1, // Source starts counting from 0, not 1
|
||||
sort = sort,
|
||||
pages = Range(range),
|
||||
tag = Items(
|
||||
items = TagArrays(
|
||||
included = includedTags,
|
||||
excluded = excludedTags
|
||||
)
|
||||
)
|
||||
)
|
||||
val jsonString = buildJsonObject {
|
||||
put("search", json.encodeToJsonElement(request))
|
||||
}.toString()
|
||||
return POST("$baseUrl$SEARCH_URL", headers, jsonString.toRequestBody(MEDIA_TYPE))
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return POST(baseUrl + SEARCH_URL, headers, buildRequestBody(page = page))
|
||||
// Builds request for /api/getBookById endpoint
|
||||
private fun buildDetailRequest(id: Int): Request {
|
||||
val jsonString = buildJsonObject { put("id", id) }.toString()
|
||||
return POST("$baseUrl$MANGA_URL", headers, jsonString.toRequestBody(MEDIA_TYPE))
|
||||
}
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
getMangaList(response, page)
|
||||
// Popular and Latest
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = buildSearchRequest(page = page, sort = 1)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val results = json.parseToJsonElement(response.body!!.string()).jsonObject["results"]!!.jsonArray
|
||||
if (results.isEmpty()) return MangasPage(listOf(), false)
|
||||
return MangasPage(
|
||||
results.map {
|
||||
val manga = json.decodeFromJsonElement<Manga>(it)
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain("/g/${manga.id}")
|
||||
title = manga.title
|
||||
thumbnail_url = "${manga.image_server + manga.id}/cover.jpg"
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = buildSearchRequest(page = page)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
// Manga Details
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return SManga.create().apply {
|
||||
response.asJsoup().selectFirst("div#bigcontainer").let { info ->
|
||||
title = info.select("h1").text()
|
||||
thumbnail_url = info.selectFirst("div#cover v-lazy-image").attr("abs:src")
|
||||
status = SManga.COMPLETED
|
||||
artist = info.selectTextOrNull("div.field-name:contains(Artist:) a.tag")
|
||||
author = info.selectTextOrNull("div.field-name:contains(Group:) a.tag") ?: artist
|
||||
genre = info.selectTextOrNull("div.field-name:contains(Tag:) a.tag")
|
||||
// Additional details
|
||||
description = listOf(
|
||||
Pair("Alternative Title", info.selectTextOrNull("h2")),
|
||||
Pair("Pages", info.selectTextOrNull("div#info > div:contains(pages)")),
|
||||
Pair("Parody", info.selectTextOrNull("div.field-name:contains(Parody:) a.tag")),
|
||||
Pair("Category", info.selectTextOrNull("div.field-name:contains(Category:) a.tag")),
|
||||
Pair("Language", info.selectTextOrNull("div.field-name:contains(Language:) a.tag")),
|
||||
).filterNot { it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}: ${it.second}" }
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
getMangaList(response, page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
getMangaList(response, page)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMangaList(response: Response, page: Int): MangasPage {
|
||||
val jsonData = response.body!!.string()
|
||||
val jsonObject = JsonParser().parse(jsonData).asJsonObject
|
||||
val totalPages = jsonObject["total_count"].int
|
||||
val results = jsonObject["results"].array
|
||||
return MangasPage(parseSearch(results.toList()), page < totalPages)
|
||||
}
|
||||
|
||||
private fun parseSearch(jsonArray: List<JsonElement>): List<SManga> {
|
||||
val mutableList = mutableListOf<SManga>()
|
||||
jsonArray.forEach { json ->
|
||||
val manga = SManga.create()
|
||||
val gsonManga = gson.fromJson<Manga>(json)
|
||||
manga.url = "/g/${gsonManga.id}"
|
||||
manga.title = gsonManga.title
|
||||
manga.thumbnail_url = gsonManga.image_server + gsonManga.id + "/cover.jpg"
|
||||
manga.genre = gsonManga.tags.filter { it.type == 1 }.joinToString { it.name }
|
||||
manga.artist = gsonManga.tags.firstOrNull { it.type == 4 }?.name
|
||||
manga.initialized = true
|
||||
mutableList.add(manga)
|
||||
}
|
||||
return mutableList
|
||||
}
|
||||
|
||||
// Ensures no exceptions are thrown when scraping additional details
|
||||
private fun Element.selectTextOrNull(selector: String): String? {
|
||||
val list = this.select(selector)
|
||||
return if (list.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
list.joinToString(", ") { it.text() }
|
||||
}
|
||||
}
|
||||
|
||||
// Chapter
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val time = document.select("div#info div time").text()
|
||||
val time = response.asJsoup().select("div#info div time").text()
|
||||
return listOf(
|
||||
SChapter.create().apply {
|
||||
name = "Chapter"
|
||||
|
@ -142,58 +180,19 @@ class NineHentai : HttpSource() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val includedTags = mutableListOf<Tag>()
|
||||
val excludedTags = mutableListOf<Tag>()
|
||||
var sort = 0
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is GenreList -> {
|
||||
filter.state.forEach { f ->
|
||||
if (!f.isIgnored()) {
|
||||
if (f.isExcluded())
|
||||
excludedTags.add(f)
|
||||
else
|
||||
includedTags.add(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Sorting -> {
|
||||
sort = filter.state!!.index
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST(baseUrl + SEARCH_URL, headers, buildRequestBody(query, page, sort, includedTags, excludedTags))
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return SManga.create().apply {
|
||||
response.asJsoup().select("div.card-body").firstOrNull()?.let { info ->
|
||||
title = info.select("h1").text()
|
||||
genre = info.select("div.field-name:contains(Tag:) a.tag").joinToString { it.text() }
|
||||
artist = info.select("div.field-name:contains(Artist:) a.tag").joinToString { it.text() }
|
||||
thumbnail_url = info.select("div#cover v-lazy-image").attr("abs:src")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Page List
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val mangaId = chapter.url.substringAfter("/g/").toInt()
|
||||
return POST(baseUrl + MANGA_URL, headers, buildIdBody(mangaId))
|
||||
return buildDetailRequest(mangaId)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val jsonData = response.body!!.string()
|
||||
val jsonObject = JsonParser().parse(jsonData).asJsonObject
|
||||
val jsonArray = jsonObject.getAsJsonObject("results")
|
||||
var imageUrl: String
|
||||
var totalPages: Int
|
||||
var mangaId: String
|
||||
jsonArray.let { json ->
|
||||
mangaId = json["id"].string
|
||||
imageUrl = json["image_server"].string + mangaId
|
||||
totalPages = json["total_page"].int
|
||||
}
|
||||
val resultsObj = json.parseToJsonElement(response.body!!.string()).jsonObject["results"]!!
|
||||
val manga = json.decodeFromJsonElement<Manga>(resultsObj)
|
||||
val imageUrl = manga.image_server + manga.id
|
||||
var totalPages = manga.total_page
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
client.newCall(
|
||||
|
@ -212,39 +211,158 @@ class NineHentai : HttpSource() {
|
|||
return pages
|
||||
}
|
||||
|
||||
private fun buildRequestBody(searchText: String = "", page: Int, sort: Int = 0, includedTags: MutableList<Tag> = arrayListOf(), excludedTags: MutableList<Tag> = arrayListOf()): RequestBody {
|
||||
val json = gson.toJson(mapOf("search" to SearchRequest(text = searchText, page = page - 1, sort = sort, tag = mapOf("items" to Items(includedTags, excludedTags)))))
|
||||
return RequestBody.create(MEDIA_TYPE, json)
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
var sort = 0
|
||||
val range = mutableListOf(0, 2000)
|
||||
val includedTags = mutableListOf<Tag>()
|
||||
val excludedTags = mutableListOf<Tag>()
|
||||
for (filter in filterList) {
|
||||
when (filter) {
|
||||
is SortFilter -> {
|
||||
sort = filter.state!!.index
|
||||
}
|
||||
is MinPagesFilter -> {
|
||||
try {
|
||||
range[0] = filter.state.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
// Suppress and retain default value
|
||||
}
|
||||
}
|
||||
is MaxPagesFilter -> {
|
||||
try {
|
||||
range[1] = filter.state.toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
// Suppress and retain default value
|
||||
}
|
||||
}
|
||||
is IncludedFilter -> {
|
||||
includedTags += getTags(filter.state, 1)
|
||||
}
|
||||
is ExcludedFilter -> {
|
||||
excludedTags += getTags(filter.state, 1)
|
||||
}
|
||||
is GroupFilter -> {
|
||||
includedTags += getTags(filter.state, 2)
|
||||
}
|
||||
is ParodyFilter -> {
|
||||
includedTags += getTags(filter.state, 3)
|
||||
}
|
||||
is ArtistFilter -> {
|
||||
includedTags += getTags(filter.state, 4)
|
||||
}
|
||||
is CharacterFilter -> {
|
||||
includedTags += getTags(filter.state, 5)
|
||||
}
|
||||
is CategoryFilter -> {
|
||||
includedTags += getTags(filter.state, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buildSearchRequest(
|
||||
searchText = query,
|
||||
page = page,
|
||||
sort = sort,
|
||||
range = range,
|
||||
includedTags = includedTags,
|
||||
excludedTags = excludedTags
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildIdBody(id: Int): RequestBody {
|
||||
return RequestBody.create(MEDIA_TYPE, gson.toJson(mapOf("id" to id)))
|
||||
private fun getTags(queries: String, type: Int): List<Tag> {
|
||||
return queries.split(",").map(String::trim)
|
||||
.filterNot(String::isBlank).mapNotNull { query ->
|
||||
val jsonString = buildJsonObject {
|
||||
put("tag_name", query)
|
||||
put("tag_type", type)
|
||||
}.toString()
|
||||
lookupTags(jsonString)
|
||||
}
|
||||
}
|
||||
|
||||
private class GenreList(tags: List<Tag>) : Filter.Group<Tag>("Tags", tags)
|
||||
// Based on HentaiHand ext
|
||||
private fun lookupTags(request: String): Tag? {
|
||||
return client.newCall(POST("$baseUrl$TAG_URL", headers, request.toRequestBody(MEDIA_TYPE)))
|
||||
.asObservableSuccess()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { response ->
|
||||
// Returns the first matched tag, or null if there are no results
|
||||
val tagList = json.parseToJsonElement(response.body!!.string()).jsonObject["results"]!!.jsonArray.map {
|
||||
json.decodeFromJsonElement<Tag>(it)
|
||||
}
|
||||
if (tagList.isEmpty()) return@map null
|
||||
else tagList.first()
|
||||
}.toBlocking().first()
|
||||
}
|
||||
|
||||
private class Sorting : Filter.Sort(
|
||||
"Sorting",
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith("id:")) {
|
||||
val id = query.substringAfter("id:").toInt()
|
||||
return client.newCall(buildDetailRequest(id))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
fetchSingleManga(response)
|
||||
}
|
||||
}
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
private fun fetchSingleManga(response: Response): MangasPage {
|
||||
val resultsObj = json.parseToJsonElement(response.body!!.string()).jsonObject["results"]!!
|
||||
val manga = json.decodeFromJsonElement<Manga>(resultsObj)
|
||||
val list = listOf(
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain("/g/${manga.id}")
|
||||
title = manga.title
|
||||
thumbnail_url = "${manga.image_server + manga.id}/cover.jpg"
|
||||
}
|
||||
)
|
||||
return MangasPage(list, false)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
// Filters
|
||||
|
||||
private class SortFilter : Filter.Sort(
|
||||
"Sort by",
|
||||
arrayOf("Newest", "Popular Right now", "Most Fapped", "Most Viewed", "By Title"),
|
||||
Selection(1, false)
|
||||
)
|
||||
|
||||
private class MinPagesFilter : Filter.Text("Minimum Pages")
|
||||
private class MaxPagesFilter : Filter.Text("Maximum Pages")
|
||||
private class IncludedFilter : Filter.Text("Included Tags")
|
||||
private class ExcludedFilter : Filter.Text("Excluded Tags")
|
||||
private class ArtistFilter : Filter.Text("Artist")
|
||||
private class GroupFilter : Filter.Text("Group")
|
||||
private class ParodyFilter : Filter.Text("Parody")
|
||||
private class CharacterFilter : Filter.Text("Character")
|
||||
private class CategoryFilter : Filter.Text("Category")
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Sorting(),
|
||||
GenreList(NHTags.getTagsList())
|
||||
Filter.Header("Search by id with \"id:\" in front of query"),
|
||||
Filter.Separator(),
|
||||
SortFilter(),
|
||||
MinPagesFilter(),
|
||||
MaxPagesFilter(),
|
||||
IncludedFilter(),
|
||||
ExcludedFilter(),
|
||||
ArtistFilter(),
|
||||
GroupFilter(),
|
||||
ParodyFilter(),
|
||||
CharacterFilter(),
|
||||
CategoryFilter(),
|
||||
)
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw Exception("Not Used")
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = throw Exception("Not Used")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not Used")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw Exception("Not Used")
|
||||
|
||||
companion object {
|
||||
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
private const val SEARCH_URL = "/api/getBook"
|
||||
private const val MANGA_URL = "/api/getBookByID"
|
||||
private const val TAG_URL = "/api/getTag"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package eu.kanade.tachiyomi.extension.all.ninehentai
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Manga(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val image_server: String,
|
||||
val total_page: Int
|
||||
)
|
||||
|
||||
/*
|
||||
The basic search request JSON object looks like this:
|
||||
{
|
||||
"search": {
|
||||
"text": "",
|
||||
"page": 1,
|
||||
"sort": 1,
|
||||
"pages": {
|
||||
"range": [0, 2000]
|
||||
},
|
||||
"tag": {
|
||||
"items": {
|
||||
"included": [],
|
||||
"excluded": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
Sort = 0, Newest
|
||||
Sort = 1, Popular right now
|
||||
Sort = 2, Most Fapped
|
||||
Sort = 3, Most Viewed
|
||||
Sort = 4, By title
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class SearchRequest(
|
||||
val text: String,
|
||||
val page: Int,
|
||||
val sort: Int,
|
||||
val pages: Range,
|
||||
val tag: Items
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Range(
|
||||
val range: List<Int>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Items(
|
||||
val items: TagArrays
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TagArrays(
|
||||
val included: List<Tag>,
|
||||
val excluded: List<Tag>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Tag(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val type: Int = 1
|
||||
)
|
|
@ -0,0 +1,38 @@
|
|||
package eu.kanade.tachiyomi.extension.all.ninehentai
|
||||
|
||||
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://9hentai.to/g/xxxxxx intents and redirects them to
|
||||
* the main Tachiyomi process.
|
||||
*/
|
||||
class NineHentaiUrlActivity : 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("NineHentaiUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("NineHentaiUrlActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue