|
@@ -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)
|
|
|
- )
|
|
|
-
|
|
|
-}
|
|
|
+}
|