mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-22 10:22:47 +01:00
add Batcave (#5304)
* add BatCave * owntext * semicolon proof * better filter error parse
This commit is contained in:
parent
0108df5725
commit
71ab04eecb
7
src/en/batcave/build.gradle
Normal file
7
src/en/batcave/build.gradle
Normal file
@ -0,0 +1,7 @@
|
||||
ext {
|
||||
extName = 'BatCave'
|
||||
extClass = '.BatCave'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/batcave/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/batcave/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
BIN
src/en/batcave/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/batcave/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
src/en/batcave/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/batcave/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/en/batcave/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/batcave/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
BIN
src/en/batcave/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/batcave/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
@ -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)
|
||||
}
|
||||
}
|
@ -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>,
|
||||
)
|
@ -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",
|
||||
)
|
Loading…
Reference in New Issue
Block a user