add Batcave (#5304)

* add BatCave

* owntext

* semicolon proof

* better filter error parse
This commit is contained in:
AwkwardPeak7 2024-09-30 21:08:46 +05:00 committed by GitHub
parent 0108df5725
commit 71ab04eecb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 406 additions and 0 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -0,0 +1,239 @@
package eu.kanade.tachiyomi.extension.en.batcave
import android.util.Log
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class BatCave : HttpSource() {
override val name = "BatCave"
override val lang = "en"
override val supportsLatest = true
override val baseUrl = "https://batcave.biz"
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", SortFilter.LATEST)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
val url = "$baseUrl/search/".toHttpUrl().newBuilder().apply {
addPathSegment(query.trim())
if (page > 1) {
addPathSegments("page/$page/")
}
}.build()
return GET(url, headers)
}
var filtersApplied = false
val url = "$baseUrl/comix/".toHttpUrl().newBuilder().apply {
filters.get<YearFilter>()?.addFilterToUrl(this)
?.also { filtersApplied = it }
filters.get<PublisherFilter>()?.addFilterToUrl(this)
?.also { filtersApplied = filtersApplied || it }
filters.get<GenreFilter>()?.addFilterToUrl(this)
?.also { filtersApplied = filtersApplied || it }
if (filtersApplied) {
setPathSegment(0, "ComicList")
}
if (page > 1) {
addPathSegments("page/$page/")
}
}.build().toString()
val sort = filters.get<SortFilter>()!!
return if (sort.getSort() == "") {
GET(url, headers)
} else {
val form = FormBody.Builder().apply {
add("dlenewssortby", sort.getSort())
add("dledirection", sort.getDirection())
if (filtersApplied) {
add("set_new_sort", "dle_sort_xfilter")
add("set_direction_sort", "dle_direction_xfilter")
} else {
add("set_new_sort", "dle_sort_cat_1")
add("set_direction_sort", "dle_direction_cat_1")
}
}.build()
POST(url, headers, form)
}
}
private var publishers: List<Pair<String, Int>> = emptyList()
private var genres: List<Pair<String, Int>> = emptyList()
private var filterParseFailed = false
override fun getFilterList(): FilterList {
val filters: MutableList<Filter<*>> = mutableListOf(
Filter.Header("Doesn't work with text search"),
SortFilter(),
YearFilter(),
)
if (publishers.isNotEmpty()) {
filters.add(
PublisherFilter(publishers),
)
}
if (genres.isNotEmpty()) {
filters.add(
GenreFilter(genres),
)
}
if (filters.size < 5) {
filters.add(
Filter.Header(
if (filterParseFailed) {
"Unable to load more filters"
} else {
"Press 'reset' to load more filters"
},
),
)
}
return FilterList(filters)
}
private fun parseFilters(documented: Document) {
val script = documented.selectFirst("script:containsData(__XFILTER__)")
if (script == null) {
filterParseFailed = true
return
}
val data = try {
script.data()
.substringAfter("=")
.trim()
.removeSuffix(";")
.parseAs<XFilters>()
} catch (e: SerializationException) {
Log.e(name, "filters", e)
filterParseFailed = true
return
}
publishers = data.filterItems.publisher.values.map { it.value to it.id }
genres = data.filterItems.genre.values.map { it.value to it.id }
filterParseFailed = false
return
}
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (response.request.url.pathSegments[0] != "search") {
parseFilters(document)
}
val entries = document.select("#dle-content > .readed").map { element ->
SManga.create().apply {
with(element.selectFirst(".readed__title > a")!!) {
setUrlWithoutDomain(absUrl("href"))
title = ownText()
}
thumbnail_url = element.selectFirst("img")?.absUrl("data-src")
}
}
val hasNextPage = document.selectFirst("div.pagination__pages")
?.children()?.last()?.tagName() == "a"
return MangasPage(entries, hasNextPage)
}
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
return SManga.create().apply {
title = document.selectFirst("header.page__header h1")!!.text()
thumbnail_url = document.selectFirst("div.page__poster img")?.absUrl("src")
description = document.selectFirst("div.page__text")?.wholeText()
author = document.selectFirst(".page__list > li:has(> div:contains(Publisher))")?.ownText()
status = when (document.selectFirst(".page__list > li:has(> div:contains(release type))")?.ownText()?.trim()) {
"Ongoing" -> SManga.ONGOING
"Complete" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val data = document.selectFirst(".page__chapters-list script:containsData(__DATA__)")!!.data()
.substringAfter("=")
.trim()
.removeSuffix(";")
.parseAs<Chapters>()
return data.chapters.map { chap ->
SChapter.create().apply {
url = "/reader/${data.comicId}/${chap.id}${data.xhash}"
name = chap.title
chapter_number = chap.number
date_upload = try {
dateFormat.parse(chap.date)?.time ?: 0
} catch (_: ParseException) {
0
}
}
}
}
private val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val data = document.selectFirst("script:containsData(__DATA__)")!!.data()
.substringAfter("=")
.trim()
.removeSuffix(";")
.parseAs<Images>()
return data.images.mapIndexed { idx, img ->
Page(idx, imageUrl = baseUrl + img.trim())
}
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
private inline fun <reified T> FilterList.get(): T? {
return filterIsInstance<T>().firstOrNull()
}
private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
}

View File

@ -0,0 +1,47 @@
package eu.kanade.tachiyomi.extension.en.batcave
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class XFilters(
@SerialName("filter_items") val filterItems: XFilterItems = XFilterItems(),
)
@Serializable
class XFilterItems(
@SerialName("p") val publisher: XFilterItem = XFilterItem(),
@SerialName("g") var genre: XFilterItem = XFilterItem(),
)
@Serializable
class XFilterItem(
val values: ArrayList<Values> = arrayListOf(),
)
@Serializable
class Values(
val id: Int,
val value: String,
)
@Serializable
class Chapters(
@SerialName("news_id") val comicId: Int,
val chapters: List<Chapter>,
val xhash: String,
)
@Serializable
class Chapter(
val id: Int,
@SerialName("posi") val number: Float,
val title: String,
val date: String,
)
@Serializable
class Images(
val images: List<String>,
)

View File

@ -0,0 +1,113 @@
package eu.kanade.tachiyomi.extension.en.batcave
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
interface UrlPartFilter {
fun addFilterToUrl(url: HttpUrl.Builder): Boolean
}
class CheckBoxItem(name: String, val value: Int) : Filter.CheckBox(name)
open class CheckBoxFilter(
name: String,
private val queryParameter: String,
values: List<Pair<String, Int>>,
) : Filter.Group<CheckBoxItem>(
name,
values.map { CheckBoxItem(it.first, it.second) },
),
UrlPartFilter {
override fun addFilterToUrl(url: HttpUrl.Builder): Boolean {
val checked = state.filter { it.state }
.also { if (it.isEmpty()) return false }
.joinToString(",") { it.value.toString() }
url.addPathSegments("$queryParameter=$checked/")
return true
}
}
class PublisherFilter(values: List<Pair<String, Int>>) :
CheckBoxFilter("Publisher", "p", values)
class GenreFilter(values: List<Pair<String, Int>>) :
CheckBoxFilter("Genre", "g", values)
class TextBox(name: String) : Filter.Text(name)
class YearFilter :
Filter.Group<TextBox>(
"Year of Issue",
listOf(
TextBox("from"),
TextBox("to"),
),
),
UrlPartFilter {
override fun addFilterToUrl(url: HttpUrl.Builder): Boolean {
var applied = false
val currentYear = yearFormat.format(Date()).toInt()
if (state[0].state.isNotBlank()) {
val from = try {
state[0].state.toInt()
} catch (_: NumberFormatException) {
throw Exception("year must be number")
}
assert(from in 1929..currentYear) {
"invalid start year (must be between 1929 and $currentYear)"
}
url.addPathSegments("y[from]=$from/")
applied = true
}
if (state[1].state.isNotBlank()) {
val to = try {
state[1].state.toInt()
} catch (_: NumberFormatException) {
throw Exception("year must be number")
}
assert(to in 1929..currentYear) {
"invalid start year (must be between 1929 and $currentYear)"
}
url.addPathSegments("y[to]=$to/")
applied = true
}
return applied
}
}
private val yearFormat = SimpleDateFormat("yyyy", Locale.ENGLISH)
class SortFilter(
select: Selection = Selection(0, false),
) : Filter.Sort(
"Sort",
sorts.map { it.first }.toTypedArray(),
select,
) {
fun getSort() = sorts[state?.index ?: 0].second
fun getDirection() = if (state?.ascending != false) {
"asc"
} else {
"desc"
}
companion object {
val POPULAR = FilterList(SortFilter(Selection(3, false)))
val LATEST = FilterList(SortFilter(Selection(2, false)))
}
}
private val sorts = listOf(
"Default" to "",
"Date" to "date",
"Date of change" to "editdate",
"Rating" to "rating",
"Read" to "news_read",
"Comments" to "comm_num",
"Title" to "title",
)