Skip to content

Commit

Permalink
add Batcave (#5304)
Browse files Browse the repository at this point in the history
* add BatCave

* owntext

* semicolon proof

* better filter error parse
  • Loading branch information
AwkwardPeak7 authored and cuong-tran committed Sep 30, 2024
1 parent f97b414 commit aed1688
Show file tree
Hide file tree
Showing 9 changed files with 406 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/en/batcave/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ext {
extName = 'BatCave'
extClass = '.BatCave'
extVersionCode = 1
}

apply from: "$rootDir/common.gradle"
Binary file added src/en/batcave/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/en/batcave/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/en/batcave/res/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/en/batcave/res/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/en/batcave/res/mipmap-xxxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
239 changes: 239 additions & 0 deletions src/en/batcave/src/eu/kanade/tachiyomi/extension/en/batcave/BatCave.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
47 changes: 47 additions & 0 deletions src/en/batcave/src/eu/kanade/tachiyomi/extension/en/batcave/Dto.kt
Original file line number Diff line number Diff line change
@@ -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>,
)
Loading

0 comments on commit aed1688

Please sign in to comment.