mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-22 18:32:39 +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