Add genre filter for catalogue (#428)

* Add genre filter for catalogue

* Implement genre filter for batoto

* hardcode filters for sources

* swtich filter id to string

* reset filters when switching sources

* Add filter support to mangafox

* Catalogue changes

* Indefinite snackbar on error, use plain subscriptions in catalogue presenter
This commit is contained in:
Robin Appelman
2016-08-28 22:59:00 +02:00
committed by inorichi
parent 4171e87b4b
commit 2fb3b50535
21 changed files with 484 additions and 251 deletions
@@ -47,5 +47,4 @@ interface Source {
* @param page the page.
*/
fun fetchImage(page: Page): Observable<Page>
}
@@ -58,6 +58,11 @@ abstract class OnlineSource(context: Context) : Source {
*/
val headers by lazy { headersBuilder().build() }
/**
* Genre filters.
*/
val filters by lazy { getFilterList() }
/**
* Default network client for doing requests.
*/
@@ -126,11 +131,11 @@ abstract class OnlineSource(context: Context) : Source {
* the current page and the next page url.
* @param query the search query.
*/
open fun fetchSearchManga(page: MangasPage, query: String): Observable<MangasPage> = client
.newCall(searchMangaRequest(page, query))
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
.newCall(searchMangaRequest(page, query, filters))
.asObservable()
.map { response ->
searchMangaParse(response, page, query)
searchMangaParse(response, page, query, filters)
page
}
@@ -141,9 +146,9 @@ abstract class OnlineSource(context: Context) : Source {
* @param page the page object.
* @param query the search query.
*/
open protected fun searchMangaRequest(page: MangasPage, query: String): Request {
open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
page.url = searchMangaInitialUrl(query, filters)
}
return GET(page.url, headers)
}
@@ -153,7 +158,7 @@ abstract class OnlineSource(context: Context) : Source {
*
* @param query the search query.
*/
abstract protected fun searchMangaInitialUrl(query: String): String
abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter>): String
/**
* Parse the response from the site. It should add a list of manga and the absolute url to the
@@ -163,7 +168,7 @@ abstract class OnlineSource(context: Context) : Source {
* @param page the page object to be filled.
* @param query the search query.
*/
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String)
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>)
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
@@ -428,4 +433,7 @@ abstract class OnlineSource(context: Context) : Source {
}
data class Filter(val id: String, val name: String)
open fun getFilterList(): List<Filter> = emptyList()
}
@@ -64,7 +64,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param page the page object to be filled.
* @param query the search query.
*/
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
val document = response.asJsoup()
for (element in document.select(searchMangaSelector())) {
Manga.create(id).apply {
@@ -179,5 +179,4 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
* @param document the parsed document.
*/
abstract protected fun imageUrlParse(document: Document): String
}
@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.getLanguages
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
@@ -14,6 +15,7 @@ import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import rx.Observable
import java.text.SimpleDateFormat
import java.util.*
@@ -68,9 +70,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
}
}
override fun searchMangaRequest(page: MangasPage, query: String): Request {
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
page.url = searchMangaInitialUrl(query, filters)
}
return when (map.search.method?.toLowerCase()) {
"post" -> POST(page.url, headers, map.search.createForm())
@@ -78,9 +80,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
}
}
override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query)
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = map.search.url.replace("\$query", query)
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
val document = response.asJsoup()
for (element in document.select(map.search.manga_css)) {
Manga.create(id).apply {
@@ -184,5 +186,4 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
throw Exception("image_regex and image_css are null")
}
}
}
@@ -84,9 +84,21 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
override fun popularMangaNextPageSelector() = "#show_more_row"
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1&genre_cond=and&genres=${getFilterParams(filters)}"
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
private fun getFilterParams(filters: List<Filter>): String = filters
.map {
";i" + it.id
}.joinToString()
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters)
}
return GET(page.url, headers)
}
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
val document = response.asJsoup()
for (element in document.select(searchMangaSelector())) {
Manga.create(id).apply {
@@ -96,7 +108,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
}
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}"
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}&order_cond=views&order=desc&genre_cond=and&genres=" + getFilterParams(filters)
}
}
@@ -211,7 +223,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
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)
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders)
}
override fun imageUrlParse(document: Document): String {
@@ -219,10 +231,10 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
}
override fun login(username: String, password: String) =
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global&section=login", headers))
.asObservable()
.flatMap { doLogin(it, username, password) }
.map { isAuthenticationSuccessful(it) }
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global&section=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()
@@ -242,7 +254,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
}
override fun isAuthenticationSuccessful(response: Response) =
response.priorResponse() != null && response.priorResponse().code() == 302
response.priorResponse() != null && response.priorResponse().code() == 302
override fun isLogged(): Boolean {
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
@@ -264,4 +276,48 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
}
}
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")`
// }).join(',\n')
// on https://bato.to/search
override fun getFilterList(): List<Filter> = listOf(
Filter("40", "4-Koma"),
Filter("1", "Action"),
Filter("2", "Adventure"),
Filter("39", "Award Winning"),
Filter("3", "Comedy"),
Filter("41", "Cooking"),
Filter("9", "Doujinshi"),
Filter("10", "Drama"),
Filter("12", "Ecchi"),
Filter("13", "Fantasy"),
Filter("15", "Gender Bender"),
Filter("17", "Harem"),
Filter("20", "Historical"),
Filter("22", "Horror"),
Filter("34", "Josei"),
Filter("27", "Martial Arts"),
Filter("30", "Mecha"),
Filter("42", "Medical"),
Filter("37", "Music"),
Filter("4", "Mystery"),
Filter("38", "Oneshot"),
Filter("5", "Psychological"),
Filter("6", "Romance"),
Filter("7", "School Life"),
Filter("8", "Sci-fi"),
Filter("32", "Seinen"),
Filter("35", "Shoujo"),
Filter("16", "Shoujo Ai"),
Filter("33", "Shounen"),
Filter("19", "Shounen Ai"),
Filter("21", "Slice of Life"),
Filter("23", "Smut"),
Filter("25", "Sports"),
Filter("26", "Supernatural"),
Filter("28", "Tragedy"),
Filter("36", "Webtoon"),
Filter("29", "Yaoi"),
Filter("31", "Yuri")
)
}
@@ -42,22 +42,34 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaNextPageSelector() = "li > a:contains( Next)"
override fun searchMangaRequest(page: MangasPage, query: String): Request {
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
page.url = searchMangaInitialUrl(query, filters)
}
val form = FormBody.Builder().apply {
add("authorArtist", "")
add("mangaName", query)
add("status", "")
add("genres", "")
}.build()
}
return POST(page.url, headers, form)
val filterIndexes = filters.map { it.id.toInt() }
val maxFilterIndex = filterIndexes.max()
if (maxFilterIndex !== null) {
for (i in 0..maxFilterIndex) {
form.add("genres", if (filterIndexes.contains(i)) {
"1"
} else {
"0"
})
}
}
return POST(page.url, headers, form.build())
}
override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/AdvanceSearch"
override fun searchMangaSelector() = popularMangaSelector()
@@ -73,7 +85,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it)}
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
}
@@ -109,10 +121,59 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
}
// Not used
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
override fun pageListParse(document: Document, pages: MutableList<Page>) {
}
override fun imageUrlRequest(page: Page) = GET(page.url)
override fun imageUrlParse(document: Document) = ""
// $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n')
// on http://kissmanga.com/AdvanceSearch
override fun getFilterList(): List<Filter> = listOf(
Filter("0", "Action"),
Filter("1", "Adult"),
Filter("2", "Adventure"),
Filter("3", "Comedy"),
Filter("4", "Comic"),
Filter("5", "Cooking"),
Filter("6", "Doujinshi"),
Filter("7", "Drama"),
Filter("8", "Ecchi"),
Filter("9", "Fantasy"),
Filter("10", "Gender Bender"),
Filter("11", "Harem"),
Filter("12", "Historical"),
Filter("13", "Horror"),
Filter("14", "Josei"),
Filter("15", "Lolicon"),
Filter("16", "Manga"),
Filter("17", "Manhua"),
Filter("18", "Manhwa"),
Filter("19", "Martial Arts"),
Filter("20", "Mature"),
Filter("21", "Mecha"),
Filter("22", "Medical"),
Filter("23", "Music"),
Filter("24", "Mystery"),
Filter("25", "One shot"),
Filter("26", "Psychological"),
Filter("27", "Romance"),
Filter("28", "School Life"),
Filter("29", "Sci-fi"),
Filter("30", "Seinen"),
Filter("31", "Shotacon"),
Filter("32", "Shoujo"),
Filter("33", "Shoujo Ai"),
Filter("34", "Shounen"),
Filter("35", "Shounen Ai"),
Filter("36", "Slice of Life"),
Filter("37", "Smut"),
Filter("38", "Sports"),
Filter("39", "Supernatural"),
Filter("40", "Tragedy"),
Filter("41", "Webtoon"),
Filter("42", "Yaoi"),
Filter("43", "Yuri")
)
}
@@ -36,8 +36,8 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
override fun popularMangaNextPageSelector() = "a:has(span.next)"
override fun searchMangaInitialUrl(query: String) =
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}"
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
@@ -118,4 +118,43 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
// $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n')
// on http://kissmanga.com/AdvanceSearch
override fun getFilterList(): List<Filter> = listOf(
Filter("genres[Action]", "Action"),
Filter("genres[Adult]", "Adult"),
Filter("genres[Adventure]", "Adventure"),
Filter("genres[Comedy]", "Comedy"),
Filter("genres[Doujinshi]", "Doujinshi"),
Filter("genres[Drama]", "Drama"),
Filter("genres[Ecchi]", "Ecchi"),
Filter("genres[Fantasy]", "Fantasy"),
Filter("genres[Gender Bender]", "Gender Bender"),
Filter("genres[Harem]", "Harem"),
Filter("genres[Historical]", "Historical"),
Filter("genres[Horror]", "Horror"),
Filter("genres[Josei]", "Josei"),
Filter("genres[Martial Arts]", "Martial Arts"),
Filter("genres[Mature]", "Mature"),
Filter("genres[Mecha]", "Mecha"),
Filter("genres[Mystery]", "Mystery"),
Filter("genres[One Shot]", "One Shot"),
Filter("genres[Psychological]", "Psychological"),
Filter("genres[Romance]", "Romance"),
Filter("genres[School Life]", "School Life"),
Filter("genres[Sci-fi]", "Sci-fi"),
Filter("genres[Seinen]", "Seinen"),
Filter("genres[Shoujo]", "Shoujo"),
Filter("genres[Shoujo Ai]", "Shoujo Ai"),
Filter("genres[Shounen]", "Shounen"),
Filter("genres[Shounen Ai]", "Shounen Ai"),
Filter("genres[Slice of Life]", "Slice of Life"),
Filter("genres[Smut]", "Smut"),
Filter("genres[Sports]", "Sports"),
Filter("genres[Supernatural]", "Supernatural"),
Filter("genres[Tragedy]", "Tragedy"),
Filter("genres[Webtoons]", "Webtoons"),
Filter("genres[Yaoi]", "Yaoi"),
Filter("genres[Yuri]", "Yuri")
)
}
@@ -34,7 +34,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
override fun searchMangaInitialUrl(query: String) =
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
"$baseUrl/search.php?name=$query&page=1&sort=views&order=za"
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
@@ -47,7 +47,7 @@ class Mangasee(context: Context, override val id: Int) : ParsedOnlineSource(cont
override fun popularMangaNextPageSelector() = "ul.pagination > li > a:contains(Next)"
override fun searchMangaInitialUrl(query: String) =
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
"$baseUrl/advanced-search/result.php?sortBy=alphabet&direction=ASC&textOnly=no&resPerPage=20&page=1&seriesName=$query"
override fun searchMangaSelector() = "div.row > div > div > div > h1"
@@ -6,8 +6,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.Language
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -38,16 +40,16 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
override fun searchMangaInitialUrl(query: String) =
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
"$baseUrl/search"
override fun searchMangaRequest(page: MangasPage, query: String): Request {
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<OnlineSource.Filter>): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query)
page.url = searchMangaInitialUrl(query, filters)
}
var builder = okhttp3.FormBody.Builder()
val builder = okhttp3.FormBody.Builder()
builder.add("query", query)
return POST(page.url, headers, builder.build())
@@ -36,7 +36,7 @@ class WieManga(context: Context, override val id: Int) : ParsedOnlineSource(cont
override fun popularMangaNextPageSelector() = null
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search/?wd=$query"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search/?wd=$query"
override fun searchMangaSelector() = ".searchresult td > div"
@@ -23,7 +23,7 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
override fun searchMangaInitialUrl(query: String) = "$baseUrl/?do=search&subaction=search&story=$query"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/?do=search&subaction=search&story=$query"
override fun popularMangaSelector() = "div.content_row"
@@ -24,7 +24,7 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search?q=$query"
override fun popularMangaSelector() = "div.desc"
@@ -24,7 +24,7 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search?q=$query"
override fun popularMangaSelector() = "div.desc"