AsmHentai - new source (#1106)

* AsmHentai - new source

* cleanup
This commit is contained in:
Mike 2024-02-08 01:30:57 -05:00 committed by Draff
parent 457475fc07
commit 8dd884535b
10 changed files with 352 additions and 0 deletions

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.asmhentai.ASMHUrlActivity"
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="asmhentai.com"
android:pathPattern="/g/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,8 @@
ext {
extName = 'AsmHentai'
extClass = '.ASMHFactory'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.extension.all.asmhentai
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class ASMHFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
AsmHentai("en", "english"),
AsmHentai("ja", "japanese"),
AsmHentai("zh", "chinese"),
AsmHentai("all", ""),
)
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.extension.all.asmhentai
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://asmhentai.com/g/xxxxxx intents and redirects them to
* the main Tachiyomi process.
*/
class ASMHUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${AsmHentai.PREFIX_ID_SEARCH}${pathSegments[1]}")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("ASMHUrlActivity", e.toString())
}
} else {
Log.e("ASMHUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -0,0 +1,274 @@
package eu.kanade.tachiyomi.extension.all.asmhentai
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.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
open class AsmHentai(override val lang: String, private val tlTag: String) : ParsedHttpSource() {
override val client: OkHttpClient = network.cloudflareClient
override val baseUrl = "https://asmhentai.com"
override val name = "AsmHentai"
override val supportsLatest = false
// Popular
override fun popularMangaRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (tlTag.isNotEmpty()) addPathSegments("language/$tlTag/")
if (page > 1) addQueryParameter("page", page.toString())
}
return GET(url.build(), headers)
}
override fun popularMangaSelector(): String = ".preview_item"
private fun Element.mangaTitle() = select("h2").text()
private fun Element.mangaUrl() = select(".image a").attr("abs:href")
private fun Element.mangaThumbnail() = select(".image img").attr("abs:src")
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.mangaTitle()
setUrlWithoutDomain(element.mangaUrl())
thumbnail_url = element.mangaThumbnail()
}
}
override fun popularMangaNextPageSelector(): String = "li.active + li:not(.disabled)"
// Latest
override fun latestUpdatesNextPageSelector(): String? {
throw UnsupportedOperationException()
}
override fun latestUpdatesRequest(page: Int): Request {
throw UnsupportedOperationException()
}
override fun latestUpdatesFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun latestUpdatesSelector(): String {
throw UnsupportedOperationException()
}
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(id))
.asObservableSuccess()
.map { response -> searchMangaByIdParse(response, id) }
}
query.toIntOrNull() != null -> {
client.newCall(searchMangaByIdRequest(query))
.asObservableSuccess()
.map { response -> searchMangaByIdParse(response, query) }
}
else -> super.fetchSearchManga(page, query, filters)
}
}
// any space except after a comma (we're going to replace spaces only between words)
private val spaceRegex = Regex("""(?<!,)\s+""")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tags = (filters.last() as TagFilter).state
val q = when {
tags.isBlank() -> query
query.isBlank() -> tags
else -> "$query,$tags"
}.replace(spaceRegex, "+")
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegments("search/")
addEncodedQueryParameter("q", q)
if (page > 1) addQueryParameter("page", page.toString())
}
return GET(url.build(), headers)
}
private class SMangaDto(
val title: String,
val url: String,
val thumbnail: String,
val lang: String,
)
override fun searchMangaParse(response: Response): MangasPage {
val doc = response.asJsoup()
val mangas = doc.select(searchMangaSelector())
.map {
SMangaDto(
title = it.mangaTitle(),
url = it.mangaUrl(),
thumbnail = it.mangaThumbnail(),
lang = it.select("a:has(.flag)").attr("href").removeSuffix("/").substringAfterLast("/"),
)
}
.let { unfiltered ->
if (tlTag.isNotEmpty()) unfiltered.filter { it.lang == tlTag } else unfiltered
}
.map {
SManga.create().apply {
title = it.title
setUrlWithoutDomain(it.url)
thumbnail_url = it.thumbnail
}
}
return MangasPage(mangas, doc.select(searchMangaNextPageSelector()).isNotEmpty())
}
private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/g/$id/", headers)
private fun searchMangaByIdParse(response: Response, id: String): MangasPage {
val details = mangaDetailsParse(response)
details.url = "/g/$id/"
return MangasPage(listOf(details), false)
}
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// Details
private fun Element.get(tag: String): String {
return select(".tags:contains($tag) .tag").joinToString { it.ownText() }
}
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
document.select(".book_page").first()!!.let { element ->
thumbnail_url = element.select(".cover img").attr("abs:src")
title = element.select("h1").text()
genre = element.get("Tags")
artist = element.get("Artists")
author = artist
description = listOf("Parodies", "Groups", "Languages", "Category")
.mapNotNull { tag ->
element.get(tag).let { if (it.isNotEmpty()) "$tag: $it" else null }
}
.joinToString("\n", postfix = "\n") +
element.select(".pages h3").text() +
element.select("h1 + h2").text()
.let { altTitle -> if (altTitle.isNotEmpty()) "\nAlternate Title: $altTitle" else "" }
}
}
}
// Chapters
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
name = "Chapter"
url = manga.url
},
),
)
}
override fun chapterListSelector(): String {
throw UnsupportedOperationException()
}
override fun chapterFromElement(element: Element): SChapter {
throw UnsupportedOperationException()
}
// Pages
// convert thumbnail URLs to full image URLs
private fun String.full(): String {
val fType = substringAfterLast("t")
return replace("t$fType", fType)
}
private fun Document.inputIdValueOf(string: String): String {
return select("input[id=$string]").attr("value")
}
override fun pageListParse(document: Document): List<Page> {
val thumbUrls = document.select(".preview_thumb img")
.map { it.attr("abs:data-src") }
.toMutableList()
// input only exists if pages > 10 and have to make a request to get the other thumbnails
val totalPages = document.inputIdValueOf("t_pages")
if (totalPages.isNotEmpty()) {
val token = document.select("[name=csrf-token]").attr("content")
val form = FormBody.Builder()
.add("_token", token)
.add("id", document.inputIdValueOf("load_id"))
.add("dir", document.inputIdValueOf("load_dir"))
.add("visible_pages", "10")
.add("t_pages", totalPages)
.add("type", "2") // 1 would be "more", 2 is "all remaining"
.build()
val xhrHeaders = headers.newBuilder()
.add("X-Requested-With", "XMLHttpRequest")
.build()
client.newCall(POST("$baseUrl/inc/thumbs_loader.php", xhrHeaders, form))
.execute()
.asJsoup()
.select("img")
.mapTo(thumbUrls) { it.attr("abs:data-src") }
}
return thumbUrls.mapIndexed { i, url -> Page(i, "", url.full()) }
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Separate tags with commas (,)"),
TagFilter(),
)
class TagFilter : Filter.Text("Tags")
companion object {
const val PREFIX_ID_SEARCH = "id:"
}
}