New source: es/HentaiMode (#949)

* feat: Create HentaiMode base

* feat: Implement popular manga page

* feat: Implement search manga page

* feat: Implement manga details page

* feat: Implement single-chapter "list"

* feat: Parse page list

* chore: Add source icons

* fix: Fix URL intent handler

* refactor: apply suggestion - >imagine using string interpolation

Thx beerpsi, i'll pay you a beer (without piss) soon

Co-authored-by: beerpsi <92439990+beerpiss@users.noreply.github.com>

* refactor: Follow chapter name convention

---------

Co-authored-by: beerpsi <92439990+beerpiss@users.noreply.github.com>
This commit is contained in:
Claudemirovsky 2024-02-03 14:57:22 -03:00 committed by Draff
parent dd3b5b0140
commit 742027746e
9 changed files with 239 additions and 0 deletions

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,168 @@
package eu.kanade.tachiyomi.extension.es.hentaimode
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class HentaiMode : ParsedHttpSource() {
override val name = "HentaiMode"
override val baseUrl = "https://hentaimode.com"
override val lang = "es"
override val supportsLatest = false
override val client = network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = "div.row div.book-list > a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.absUrl("href"))
title = element.selectFirst(".book-description > p")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
override fun popularMangaNextPageSelector() = null
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
throw UnsupportedOperationException()
}
override fun latestUpdatesSelector(): String {
throw UnsupportedOperationException()
}
override fun latestUpdatesFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun latestUpdatesNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =============================== Search ===============================
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/g/$id"))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
super.fetchSearchManga(page, query, filters)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
val doc = response.asJsoup()
val details = mangaDetailsParse(doc)
.apply { setUrlWithoutDomain(doc.location()) }
return MangasPage(listOf(details), false)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
require(query.length >= 3) { "Please use at least 3 characters!" }
return GET("$baseUrl/buscar?s=$query")
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = null
// =========================== Manga Details ============================
private val additionalInfos = listOf("Serie", "Tipo", "Personajes", "Idioma")
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
thumbnail_url = document.selectFirst("div#cover img")?.absUrl("src")
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
with(document.selectFirst("div#info-block > div#info")!!) {
title = selectFirst("h1")!!.text()
genre = getInfo("Categorías")
author = getInfo("Grupo")
artist = getInfo("Artista")
description = buildString {
additionalInfos.forEach { info ->
getInfo(info)?.also {
append(info)
append(": ")
append(it)
}
}
}
}
}
private fun Element.getInfo(text: String): String? {
return select("div.tag-container:containsOwn($text) a.tag")
.joinToString { it.text() }
.takeUnless(String::isBlank)
}
// ============================== Chapters ==============================
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapter = SChapter.create().apply {
url = manga.url.replace("/g/", "/leer/")
chapter_number = 1F
name = "Chapter"
}
return Observable.just(listOf(chapter))
}
override fun chapterListSelector(): String {
throw UnsupportedOperationException()
}
override fun chapterFromElement(element: Element): SChapter {
throw UnsupportedOperationException()
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
val script = document.selectFirst("script:containsData(page_image)")!!.data()
val pagePaths = script.substringAfter("pages = [")
.substringBefore(",]")
.substringBefore("]") // Just to make sure
.split(',')
.map {
it.substringAfter(":").substringAfter('"').substringBefore('"')
}
return pagePaths.mapIndexed { index, path ->
Page(index, imageUrl = baseUrl + path)
}
}
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
companion object {
const val PREFIX_SEARCH = "id:"
}
}

View File

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