diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt index ce4a40ee6c..415b1b8d59 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/Batoto.kt @@ -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().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 { - 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 { - val pages = mutableListOf() - 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 { - 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 { + return Observable.error(Exception("RIP Batoto")) } override fun fetchChapterList(manga: SManga): Observable> { - 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> { + 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, state: Int = 0) : Filter.Select(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.Group>("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) - ) - -} \ No newline at end of file +}