Update domain for NineHentai (+revert removal) (#6842)
* Revert 9bc701d65ae9493772848b5d5fd927a87b3d7fb5 (partial) * NineHentai: update domain
This commit is contained in:
		
							parent
							
								
									39ae8f5e73
								
							
						
					
					
						commit
						44ad6961d3
					
				
							
								
								
									
										23
									
								
								src/en/ninehentai/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/en/ninehentai/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
 | 
			
		||||
    <application>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".en.ninehentai.NineHentaiUrlActivity"
 | 
			
		||||
            android:excludeFromRecents="true"
 | 
			
		||||
            android:exported="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.so"
 | 
			
		||||
                    android:pathPattern="/g/..*"
 | 
			
		||||
                    android:scheme="https" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
    </application>
 | 
			
		||||
</manifest>
 | 
			
		||||
							
								
								
									
										8
									
								
								src/en/ninehentai/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/en/ninehentai/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
ext {
 | 
			
		||||
    extName = 'NineHentai'
 | 
			
		||||
    extClass = '.NineHentai'
 | 
			
		||||
    extVersionCode = 4
 | 
			
		||||
    isNsfw = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 5.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 9.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/ninehentai/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 15 KiB  | 
@ -0,0 +1,379 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.en.ninehentai
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
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.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import kotlinx.serialization.decodeFromString
 | 
			
		||||
import kotlinx.serialization.encodeToString
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import kotlinx.serialization.json.buildJsonObject
 | 
			
		||||
import kotlinx.serialization.json.decodeFromJsonElement
 | 
			
		||||
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.Companion.toRequestBody
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import okio.Buffer
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.util.Calendar
 | 
			
		||||
 | 
			
		||||
class NineHentai : HttpSource() {
 | 
			
		||||
 | 
			
		||||
    override val baseUrl = "https://9hentai.so"
 | 
			
		||||
 | 
			
		||||
    override val name = "NineHentai"
 | 
			
		||||
 | 
			
		||||
    override val lang = "en"
 | 
			
		||||
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
 | 
			
		||||
    override val client: OkHttpClient = network.cloudflareClient
 | 
			
		||||
 | 
			
		||||
    private val json: Json by injectLazy()
 | 
			
		||||
 | 
			
		||||
    // 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 searchRequest = 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 = json.encodeToString(SearchRequestPayload(search = searchRequest))
 | 
			
		||||
        return POST("$baseUrl$SEARCH_URL", headers, jsonString.toRequestBody(MEDIA_TYPE))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseSearchResponse(response: Response): MangasPage {
 | 
			
		||||
        return response.use {
 | 
			
		||||
            val page = json.decodeFromString<SearchRequestPayload>(it.request.bodyString).search.page
 | 
			
		||||
            json.decodeFromString<SearchResponse>(it.body.string()).let { searchResponse ->
 | 
			
		||||
                MangasPage(
 | 
			
		||||
                    searchResponse.results.map {
 | 
			
		||||
                        SManga.create().apply {
 | 
			
		||||
                            url = "/g/${it.id}"
 | 
			
		||||
                            title = it.title
 | 
			
		||||
                            // Cover is the compressed first page (cover might change if page count changes)
 | 
			
		||||
                            thumbnail_url = "${it.image_server}${it.id}/1.jpg?${it.total_page}"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    searchResponse.totalCount - 1 > 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))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Popular
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaRequest(page: Int): Request = buildSearchRequest(page = page, sort = 1)
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaParse(response: Response): MangasPage = parseSearchResponse(response)
 | 
			
		||||
 | 
			
		||||
    // Latest
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int): Request = buildSearchRequest(page = page)
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesParse(response: Response): MangasPage = parseSearchResponse(response)
 | 
			
		||||
 | 
			
		||||
    // Search
 | 
			
		||||
    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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
                }
 | 
			
		||||
                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)
 | 
			
		||||
                }
 | 
			
		||||
                else -> { /* Do nothing */ }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return buildSearchRequest(
 | 
			
		||||
            searchText = query,
 | 
			
		||||
            page = page,
 | 
			
		||||
            sort = sort,
 | 
			
		||||
            range = range,
 | 
			
		||||
            includedTags = includedTags,
 | 
			
		||||
            excludedTags = excludedTags,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaParse(response: Response): MangasPage = parseSearchResponse(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") ?: "Unknown circle"
 | 
			
		||||
                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}" }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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 time = response.asJsoup().select("div#info div time").text()
 | 
			
		||||
        return listOf(
 | 
			
		||||
            SChapter.create().apply {
 | 
			
		||||
                name = "Chapter"
 | 
			
		||||
                date_upload = parseChapterDate(time)
 | 
			
		||||
                url = response.request.url.encodedPath
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseChapterDate(date: String): Long {
 | 
			
		||||
        val dateStringSplit = date.split(" ")
 | 
			
		||||
        val value = dateStringSplit[0].toInt()
 | 
			
		||||
 | 
			
		||||
        return when (dateStringSplit[1].removeSuffix("s")) {
 | 
			
		||||
            "sec" -> Calendar.getInstance().apply {
 | 
			
		||||
                add(Calendar.SECOND, value * -1)
 | 
			
		||||
            }.timeInMillis
 | 
			
		||||
            "min" -> Calendar.getInstance().apply {
 | 
			
		||||
                add(Calendar.MINUTE, value * -1)
 | 
			
		||||
            }.timeInMillis
 | 
			
		||||
            "hour" -> Calendar.getInstance().apply {
 | 
			
		||||
                add(Calendar.HOUR_OF_DAY, value * -1)
 | 
			
		||||
            }.timeInMillis
 | 
			
		||||
            "day" -> Calendar.getInstance().apply {
 | 
			
		||||
                add(Calendar.DATE, value * -1)
 | 
			
		||||
            }.timeInMillis
 | 
			
		||||
            "week" -> Calendar.getInstance().apply {
 | 
			
		||||
                add(Calendar.DATE, value * 7 * -1)
 | 
			
		||||
            }.timeInMillis
 | 
			
		||||
            "month" -> Calendar.getInstance().apply {
 | 
			
		||||
                add(Calendar.MONTH, value * -1)
 | 
			
		||||
            }.timeInMillis
 | 
			
		||||
            "year" -> Calendar.getInstance().apply {
 | 
			
		||||
                add(Calendar.YEAR, value * -1)
 | 
			
		||||
            }.timeInMillis
 | 
			
		||||
            else -> {
 | 
			
		||||
                return 0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Page List
 | 
			
		||||
 | 
			
		||||
    override fun pageListRequest(chapter: SChapter): Request {
 | 
			
		||||
        val mangaId = chapter.url.substringAfter("/g/").toInt()
 | 
			
		||||
        return buildDetailRequest(mangaId)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        client.newCall(
 | 
			
		||||
            GET(
 | 
			
		||||
                "$imageUrl/preview/${totalPages}t.jpg",
 | 
			
		||||
                headersBuilder().build(),
 | 
			
		||||
            ),
 | 
			
		||||
        ).execute().code.let { code ->
 | 
			
		||||
            if (code == 404) totalPages--
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (1..totalPages).map {
 | 
			
		||||
            Page(it - 1, "", "$imageUrl/$it.jpg")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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 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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Filters
 | 
			
		||||
 | 
			
		||||
    private class SortFilter : Filter.Select<String>(
 | 
			
		||||
        "Sort by",
 | 
			
		||||
        arrayOf("Newest", "Popular Right now", "Most Fapped", "Most Viewed", "By Title"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
        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 UnsupportedOperationException()
 | 
			
		||||
 | 
			
		||||
    private val Request.bodyString: String
 | 
			
		||||
        get() {
 | 
			
		||||
            val requestCopy = newBuilder().build()
 | 
			
		||||
            val buffer = Buffer()
 | 
			
		||||
 | 
			
		||||
            return runCatching { buffer.apply { requestCopy.body!!.writeTo(this) }.readUtf8() }
 | 
			
		||||
                .getOrNull() ?: ""
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    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,84 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.en.ninehentai
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.SerialName
 | 
			
		||||
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 SearchRequestPayload(
 | 
			
		||||
    val search: SearchRequest,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
data class SearchResponse(
 | 
			
		||||
    @SerialName("total_count") val totalCount: Int,
 | 
			
		||||
    val results: List<Manga>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@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.en.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.so/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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user