Remove batoto from catalogues
This commit is contained in:
parent
7b4ac7998a
commit
183e83684a
1 changed files with 11 additions and 367 deletions
|
@ -1,383 +1,27 @@
|
||||||
package eu.kanade.tachiyomi.source.online.english
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
import android.text.Html
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
|
||||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
|
||||||
import okhttp3.*
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.net.URI
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class Batoto : ParsedHttpSource(), LoginSource {
|
class Batoto : Source {
|
||||||
|
|
||||||
// TODO remove
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
override val id: Long = 1
|
override val id: Long = 1
|
||||||
|
|
||||||
override val name = "Batoto"
|
override val name = "Batoto"
|
||||||
|
|
||||||
override val baseUrl = "https://bato.to"
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
return Observable.error(Exception("RIP Batoto"))
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*")
|
|
||||||
|
|
||||||
private val dateFields = HashMap<String, Int>().apply {
|
|
||||||
put("second", Calendar.SECOND)
|
|
||||||
put("minute", Calendar.MINUTE)
|
|
||||||
put("hour", Calendar.HOUR)
|
|
||||||
put("day", Calendar.DATE)
|
|
||||||
put("week", Calendar.WEEK_OF_YEAR)
|
|
||||||
put("month", Calendar.MONTH)
|
|
||||||
put("year", Calendar.YEAR)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Cookie", "lang_option=English")
|
|
||||||
|
|
||||||
private val pageHeaders = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/reader")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return GET("$baseUrl/search_ajax?order_cond=update&order=desc&p=$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "tr:has(a)"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "tr:has(a)"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
val manga = SManga.create()
|
|
||||||
element.select("a[href*=bato.to]").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text().trim()
|
|
||||||
}
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
|
||||||
return popularMangaFromElement(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "#show_more_row"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = HttpUrl.parse("$baseUrl/search_ajax")!!.newBuilder()
|
|
||||||
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
|
||||||
var genres = ""
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is Status -> if (!filter.isIgnored()) {
|
|
||||||
url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
|
|
||||||
}
|
|
||||||
is GenreList -> {
|
|
||||||
filter.state.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is Genre -> if (!filter.isIgnored()) {
|
|
||||||
genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
|
|
||||||
}
|
|
||||||
is SelectField -> {
|
|
||||||
val sel = filter.values[filter.state].value
|
|
||||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TextField -> {
|
|
||||||
if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
|
||||||
}
|
|
||||||
is SelectField -> {
|
|
||||||
val sel = filter.values[filter.state].value
|
|
||||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
|
||||||
}
|
|
||||||
is Flag -> {
|
|
||||||
val sel = if (filter.state) filter.valTrue else filter.valFalse
|
|
||||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
|
||||||
}
|
|
||||||
is OrderBy -> {
|
|
||||||
url.addQueryParameter("order_cond", arrayOf("title", "author", "artist", "rating", "views", "update")[filter.state!!.index])
|
|
||||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "asc" else "desc")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
|
|
||||||
url.addQueryParameter("p", page.toString())
|
|
||||||
return GET(url.toString(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga {
|
|
||||||
return popularMangaFromElement(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
val mangaId = manga.url.substringAfterLast("r")
|
|
||||||
return GET("$baseUrl/comic_pop?id=$mangaId", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
val tbody = document.select("tbody").first()
|
|
||||||
val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
|
|
||||||
|
|
||||||
val manga = SManga.create()
|
|
||||||
manga.author = artistElement.selectText("td:eq(1)")
|
|
||||||
manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
|
|
||||||
manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)")
|
|
||||||
manga.thumbnail_url = document.select("img[src^=http://img.bato.to/forums/uploads/]").first()?.attr("src")
|
|
||||||
manga.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)"))
|
|
||||||
manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String?) = when (status) {
|
|
||||||
"Ongoing" -> SManga.ONGOING
|
|
||||||
"Complete" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
|
||||||
// Https is currently very slow. The replace also saves a redirection.
|
|
||||||
var newUrl = "http://bato.to" + manga.url
|
|
||||||
if ("/comic/_/comics/" !in newUrl) {
|
|
||||||
newUrl = newUrl.replace("/comic/_/", "/comic/_/comics/")
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.chapterListRequest(manga).newBuilder()
|
|
||||||
.url(newUrl)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val body = response.body()!!.string()
|
|
||||||
val matcher = staffNotice.matcher(body)
|
|
||||||
if (matcher.find()) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val notice = Html.fromHtml(matcher.group(1)).toString().trim()
|
|
||||||
throw Exception(notice)
|
|
||||||
}
|
|
||||||
|
|
||||||
val document = response.asJsoup(body)
|
|
||||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
val urlElement = element.select("a[href*=bato.to/reader").first()
|
|
||||||
|
|
||||||
val chapter = SChapter.create()
|
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
|
||||||
parseDateFromElement(it)
|
|
||||||
} ?: 0
|
|
||||||
chapter.scanlator = element.select("td").getOrNull(2)?.text()
|
|
||||||
return chapter
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseDateFromElement(dateElement: Element): Long {
|
|
||||||
val dateAsString = dateElement.text()
|
|
||||||
|
|
||||||
var date: Date
|
|
||||||
try {
|
|
||||||
date = SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString)
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
val m = datePattern.matcher(dateAsString)
|
|
||||||
|
|
||||||
if (m.matches()) {
|
|
||||||
val number = m.group(1)
|
|
||||||
val amount = if (number.contains("A")) 1 else Integer.parseInt(m.group(1))
|
|
||||||
val unit = m.group(2)
|
|
||||||
|
|
||||||
date = Calendar.getInstance().apply {
|
|
||||||
add(dateFields[unit]!!, -amount)
|
|
||||||
}.time
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.time
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val id = chapter.url.substringAfterLast("#")
|
|
||||||
return GET("$baseUrl/areader?id=$id&p=1", pageHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
val pages = mutableListOf<Page>()
|
|
||||||
val selectElement = document.select("#page_select").first()
|
|
||||||
if (selectElement != null) {
|
|
||||||
for ((i, element) in selectElement.select("option").withIndex()) {
|
|
||||||
pages.add(Page(i, element.attr("value")))
|
|
||||||
}
|
|
||||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
|
||||||
} else {
|
|
||||||
// For webtoons in one page
|
|
||||||
for ((i, element) in document.select("div > img").withIndex()) {
|
|
||||||
pages.add(Page(i, "", element.attr("src")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlRequest(page: Page): Request {
|
|
||||||
val pageUrl = page.url
|
|
||||||
val start = pageUrl.indexOf("#") + 1
|
|
||||||
val end = pageUrl.indexOf("_", start)
|
|
||||||
val id = pageUrl.substring(start, end)
|
|
||||||
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String {
|
|
||||||
return document.select("#comic_page").first().attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun login(username: String, password: String) =
|
|
||||||
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
|
||||||
.asObservable()
|
|
||||||
.flatMap { doLogin(it, username, password) }
|
|
||||||
.map { isAuthenticationSuccessful(it) }
|
|
||||||
|
|
||||||
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val form = doc.select("#login").first()
|
|
||||||
val url = form.attr("action")
|
|
||||||
val authKey = form.select("input[name=auth_key]").first()
|
|
||||||
|
|
||||||
val payload = FormBody.Builder().apply {
|
|
||||||
add(authKey.attr("name"), authKey.attr("value"))
|
|
||||||
add("ips_username", username)
|
|
||||||
add("ips_password", password)
|
|
||||||
add("invisible", "1")
|
|
||||||
add("rememberMe", "1")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return client.newCall(POST(url, headers, payload)).asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isAuthenticationSuccessful(response: Response) =
|
|
||||||
response.priorResponse() != null && response.priorResponse()!!.code() == 302
|
|
||||||
|
|
||||||
override fun isLogged(): Boolean {
|
|
||||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
if (!isLogged()) {
|
return Observable.error(Exception("RIP Batoto"))
|
||||||
val username = preferences.sourceUsername(this)
|
|
||||||
val password = preferences.sourcePassword(this)
|
|
||||||
|
|
||||||
if (username.isNullOrEmpty() || password.isNullOrEmpty()) {
|
|
||||||
return Observable.error(Exception("User not logged"))
|
|
||||||
} else {
|
|
||||||
return login(username, password).flatMap { super.fetchChapterList(manga) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
return super.fetchChapterList(manga)
|
return Observable.error(Exception("RIP Batoto"))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class ListValue(val name: String, val value: String) {
|
|
||||||
override fun toString(): String = name
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Status : Filter.TriState("Completed")
|
|
||||||
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
|
||||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
|
||||||
private class SelectField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.Select<ListValue>(name, values, state)
|
|
||||||
private class Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
|
|
||||||
private class GenreList(genres: List<Filter<*>>) : Filter.Group<Filter<*>>("Genres", genres)
|
|
||||||
private class OrderBy : Filter.Sort("Order by",
|
|
||||||
arrayOf("Title", "Author", "Artist", "Rating", "Views", "Last Update"),
|
|
||||||
Filter.Sort.Selection(4, false))
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
TextField("Author", "artist_name"),
|
|
||||||
SelectField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
|
|
||||||
Status(),
|
|
||||||
Flag("Exclude mature", "mature", "m", ""),
|
|
||||||
OrderBy(),
|
|
||||||
GenreList(getGenreList())
|
|
||||||
)
|
|
||||||
|
|
||||||
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
|
||||||
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
|
|
||||||
// }).join(',\n')
|
|
||||||
// on https://bato.to/search
|
|
||||||
private fun getGenreList() = listOf(
|
|
||||||
SelectField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
|
|
||||||
Genre("4-Koma", 40),
|
|
||||||
Genre("Action", 1),
|
|
||||||
Genre("Adventure", 2),
|
|
||||||
Genre("Award Winning", 39),
|
|
||||||
Genre("Comedy", 3),
|
|
||||||
Genre("Cooking", 41),
|
|
||||||
Genre("Doujinshi", 9),
|
|
||||||
Genre("Drama", 10),
|
|
||||||
Genre("Ecchi", 12),
|
|
||||||
Genre("Fantasy", 13),
|
|
||||||
Genre("Gender Bender", 15),
|
|
||||||
Genre("Harem", 17),
|
|
||||||
Genre("Historical", 20),
|
|
||||||
Genre("Horror", 22),
|
|
||||||
Genre("Josei", 34),
|
|
||||||
Genre("Martial Arts", 27),
|
|
||||||
Genre("Mecha", 30),
|
|
||||||
Genre("Medical", 42),
|
|
||||||
Genre("Music", 37),
|
|
||||||
Genre("Mystery", 4),
|
|
||||||
Genre("Oneshot", 38),
|
|
||||||
Genre("Psychological", 5),
|
|
||||||
Genre("Romance", 6),
|
|
||||||
Genre("School Life", 7),
|
|
||||||
Genre("Sci-fi", 8),
|
|
||||||
Genre("Seinen", 32),
|
|
||||||
Genre("Shoujo", 35),
|
|
||||||
Genre("Shoujo Ai", 16),
|
|
||||||
Genre("Shounen", 33),
|
|
||||||
Genre("Shounen Ai", 19),
|
|
||||||
Genre("Slice of Life", 21),
|
|
||||||
Genre("Smut", 23),
|
|
||||||
Genre("Sports", 25),
|
|
||||||
Genre("Supernatural", 26),
|
|
||||||
Genre("Tragedy", 28),
|
|
||||||
Genre("Webtoon", 36),
|
|
||||||
Genre("Yaoi", 29),
|
|
||||||
Genre("Yuri", 31),
|
|
||||||
Genre("[no chapters]", 44)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
Reference in a new issue