New source: Bakai (#270)

* chore: Add Bakai base

* feat: Implement popular manga page

* feat: Implement (basic) search

* fix: Prevent http 429 - Use stricter rate limit

* feat: Implement manga details page

* feat: Add single-entry chapter "list"

* feat: Parse page list

* chore: Add source icon
This commit is contained in:
Claudemirovsky 2024-01-15 11:18:47 -03:00 committed by Draff
parent 029932e3d8
commit 35673b2199
9 changed files with 235 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=".pt.bakai.BakaiUrlActivity"
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="bakai.org"
android:pathPattern="/hentai/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

12
src/pt/bakai/build.gradle Normal file
View File

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Bakai'
pkgNameSuffix = 'pt.bakai'
extClass = '.Bakai'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,160 @@
package eu.kanade.tachiyomi.extension.pt.bakai
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
import java.util.concurrent.TimeUnit
class Bakai : ParsedHttpSource() {
override val name = "Bakai"
override val baseUrl = "https://bakai.org"
override val lang = "pt-BR"
override val supportsLatest = false
override val client by lazy {
network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 1, 2, TimeUnit.SECONDS)
.build()
}
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home1/page/$page/")
override fun popularMangaSelector() = "#elCmsPageWrap ul > li > article"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
thumbnail_url = element.selectFirst("img")?.absUrl("src")
with(element.selectFirst("h2.ipsType_pageTitle a")!!) {
title = text()
setUrlWithoutDomain(attr("href"))
}
}
override fun popularMangaNextPageSelector() = "li.ipsPagination_next:not(.ipsPagination_inactive) > a[rel=next]"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
throw UnsupportedOperationException("Not used.")
}
override fun latestUpdatesSelector(): String {
throw UnsupportedOperationException("Not used.")
}
override fun latestUpdatesFromElement(element: Element): SManga {
throw UnsupportedOperationException("Not used.")
}
override fun latestUpdatesNextPageSelector(): String? {
throw UnsupportedOperationException("Not used.")
}
// =============================== 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/hentai/$id"))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
super.fetchSearchManga(page, query, filters)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
val details = mangaDetailsParse(response.use { it.asJsoup() })
return MangasPage(listOf(details), false)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/search1/".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("type", "cms_records1")
.addQueryParameter("page", page.toString())
.addQueryParameter("sortby", "relevancy")
.addQueryParameter("search_and_or", "or")
.build()
return GET(url, headers)
}
override fun searchMangaSelector() = "ol > li > div"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
thumbnail_url = element.selectFirst(".ipsThumb img")?.absUrl("src")
with(element.selectFirst("h2.ipsStreamItem_title a")!!) {
title = text()
setUrlWithoutDomain(attr("href"))
}
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1.ipsType_pageTitle")?.text() ?: "Hentai"
thumbnail_url = document.selectFirst("div.cCmsRecord_image img")?.absUrl("src")
artist = document.selectFirst("span.mangaInfo:contains(Artist:) + a")?.text()
genre = document.selectFirst("span.mangaInfo:contains(Tags:) + span")?.text()
description = document.selectFirst("h2.ipsFieldRow_desc")?.let {
// Alternative titles
"Títulos alternativos: ${it.text()}"
}
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
// ============================== Chapters ==============================
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapter = SChapter.create().apply {
name = "Hentai"
chapter_number = 1F
url = manga.url
}
return Observable.just(listOf(chapter))
}
override fun chapterListSelector(): String {
throw UnsupportedOperationException("Not used.")
}
override fun chapterFromElement(element: Element): SChapter {
throw UnsupportedOperationException("Not used.")
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
return document.select("div.ipsGrid div.ipsType_center > img")
.mapIndexed { index, item ->
Page(index, "", item.absUrl("data-src"))
}
}
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used.")
}
companion object {
const val PREFIX_SEARCH = "id:"
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.extension.pt.bakai
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://bakai.org/hentai/<item> intents
* and redirects them to the main Tachiyomi process.
*/
class BakaiUrlActivity : 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", "${Bakai.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)
}
}