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
|
||||
|
||||
import android.text.Html
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
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 eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
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 {
|
||||
|
||||
// TODO remove
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
class Batoto : Source {
|
||||
|
||||
override val id: Long = 1
|
||||
|
||||
override val name = "Batoto"
|
||||
|
||||
override val baseUrl = "https://bato.to"
|
||||
|
||||
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 fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return Observable.error(Exception("RIP Batoto"))
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
if (!isLogged()) {
|
||||
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 {
|
||||
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
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return Observable.error(Exception("RIP Batoto"))
|
||||
}
|
||||
|
||||
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