Add MangaPark, Mangago, Tapastic, nhentai, E-Hentai and Sen Manga sources (#36)
* Add MangaPark source * Add pagination to MangaPark source Enable HTTPS in MangaPark source * Add Mangago source * Add Tapastic source Fix UriSelectFilters returning incorrect default states * Add nhentai source * Fix tapastic source breaking on manga with square brackets in title * Fix issues found by j2ghz Fix tapastic source showing scheduled comics * Add E-Hentai source Bump Kotlin version for all extensions to 1.1.1 Minor code cleanup in nhentai source * Add Sen Manga source. Minor code cleanup. * Fix incorrect package name in Sen Manga source. * Fix incorrect Japanese language code on E-Hentai, nhentai and Sen Manga sources. * Bump Kotlin version to 1.1.2 * Code cleanup Fix a bug with thumbnails and URLs in E-Hentai that is currently not triggerable but may cause problems in the future * Code cleanup * Fix some minor incorrect spacing
This commit is contained in:
13
src/en/mangago/build.gradle
Normal file
13
src/en/mangago/build.gradle
Normal file
@ -0,0 +1,13 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
appName = 'Tachiyomi: Mangago'
|
||||
pkgNameSuffix = "en.mangago"
|
||||
extClass = '.Mangago'
|
||||
extVersionCode = 1
|
||||
extVersionSuffix = 1
|
||||
libVersion = '1.0'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
@ -0,0 +1,245 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangago
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Mangago source
|
||||
*/
|
||||
|
||||
class Mangago : ParsedHttpSource() {
|
||||
override val lang = "en"
|
||||
override val supportsLatest = true
|
||||
override val name = "Mangago"
|
||||
override val baseUrl = "https://www.mangago.me"
|
||||
|
||||
override val client = network.cloudflareClient!!
|
||||
|
||||
//Hybrid selector that selects manga from either the genre listing or the search results
|
||||
private val genreListingSelector = ".updatesli"
|
||||
private val genreListingNextPageSelector = ".current+li > a"
|
||||
|
||||
private val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||
|
||||
override fun popularMangaSelector() = genreListingSelector
|
||||
|
||||
private fun mangaFromElement(element: Element) = SManga.create().apply {
|
||||
val linkElement = element.select(".thm-effect")
|
||||
|
||||
setUrlWithoutDomain(linkElement.attr("href"))
|
||||
|
||||
title = linkElement.attr("title")
|
||||
|
||||
thumbnail_url = linkElement.first().child(0).attr("src")
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = genreListingNextPageSelector
|
||||
|
||||
//Hybrid selector that selects manga from either the genre listing or the search results
|
||||
override fun searchMangaSelector() = "$genreListingSelector, .pic_list .box"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = genreListingNextPageSelector
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/genre/all/$page/?f=1&o=1&sortby=view&e=")
|
||||
|
||||
override fun latestUpdatesSelector() = genreListingSelector
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
//If text search is active use text search, otherwise use genre search
|
||||
val url = if (query.isNotBlank()) {
|
||||
Uri.parse("$baseUrl/r/l_search/")
|
||||
.buildUpon()
|
||||
.appendQueryParameter("name", query)
|
||||
.appendQueryParameter("page", page.toString())
|
||||
.toString()
|
||||
} else {
|
||||
val uri = Uri.parse("$baseUrl/genre/").buildUpon()
|
||||
val genres = filters.flatMap {
|
||||
(it as? GenreGroup)?.stateList ?: emptyList()
|
||||
}
|
||||
//Append included genres
|
||||
val activeGenres = genres.filter { it.isIncluded() }
|
||||
uri.appendPath(if (activeGenres.isEmpty())
|
||||
"all"
|
||||
else
|
||||
activeGenres.joinToString(",", transform = { it.name }))
|
||||
//Append page number
|
||||
uri.appendPath(page.toString())
|
||||
//Append excluded genres
|
||||
uri.appendQueryParameter("e",
|
||||
genres.filter { it.isExcluded() }
|
||||
.joinToString(",", transform = GenreFilter::name))
|
||||
//Append uri filters
|
||||
filters.forEach {
|
||||
if (it is UriFilter)
|
||||
it.addToUri(uri)
|
||||
}
|
||||
uri.toString()
|
||||
}
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = genreListingNextPageSelector
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val coverElement = document.select(".left.cover > img")
|
||||
|
||||
title = coverElement.attr("alt")
|
||||
|
||||
thumbnail_url = coverElement.attr("src")
|
||||
|
||||
document.select(".manga_right td").forEach {
|
||||
when (it.getElementsByTag("label").text().trim().toLowerCase()) {
|
||||
"status:" -> {
|
||||
status = when (it.getElementsByTag("span").first().text().trim().toLowerCase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
"author:" -> {
|
||||
author = it.getElementsByTag("a").first().text()
|
||||
}
|
||||
"genre(s):" -> {
|
||||
genre = it.getElementsByTag("a").joinToString(transform = { it.text() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description = document.getElementsByClass("manga_summary").first().ownText().trim()
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/genre/all/$page/?f=1&o=1&sortby=update_date&e=")
|
||||
|
||||
override fun chapterListSelector() = "#chapter_table > tbody > tr"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
val link = element.getElementsByTag("a")
|
||||
|
||||
setUrlWithoutDomain(link.attr("href"))
|
||||
|
||||
name = link.text().trim()
|
||||
|
||||
date_upload = dateFormat.parse(element.getElementsByClass("no").text().trim()).time
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document)
|
||||
= document.getElementById("pagenavigation").getElementsByTag("a").mapIndexed { index, element ->
|
||||
Page(index, element.attr("href"))
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.getElementById("page1").attr("src")!!
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
//Mangago does not support genre filtering and text search at the same time
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
Filter.Header("Status"),
|
||||
StatusFilter("Completed", "f"),
|
||||
StatusFilter("Ongoing", "o"),
|
||||
GenreGroup(),
|
||||
SortFilter()
|
||||
)
|
||||
|
||||
private class GenreGroup : UriFilterGroup<GenreFilter>("Genres", listOf(
|
||||
GenreFilter("Yaoi"),
|
||||
GenreFilter("Doujinshi"),
|
||||
GenreFilter("Shounen Ai"),
|
||||
GenreFilter("Shoujo"),
|
||||
GenreFilter("Yuri"),
|
||||
GenreFilter("Romance"),
|
||||
GenreFilter("Fantasy"),
|
||||
GenreFilter("Smut"),
|
||||
GenreFilter("Adult"),
|
||||
GenreFilter("School Life"),
|
||||
GenreFilter("Mystery"),
|
||||
GenreFilter("Comedy"),
|
||||
GenreFilter("Ecchi"),
|
||||
GenreFilter("Shounen"),
|
||||
GenreFilter("Martial Arts"),
|
||||
GenreFilter("Shoujo Ai"),
|
||||
GenreFilter("Supernatural"),
|
||||
GenreFilter("Drama"),
|
||||
GenreFilter("Action"),
|
||||
GenreFilter("Adventure"),
|
||||
GenreFilter("Harem"),
|
||||
GenreFilter("Historical"),
|
||||
GenreFilter("Horror"),
|
||||
GenreFilter("Josei"),
|
||||
GenreFilter("Mature"),
|
||||
GenreFilter("Mecha"),
|
||||
GenreFilter("Psychological"),
|
||||
GenreFilter("Sci-fi"),
|
||||
GenreFilter("Seinen"),
|
||||
GenreFilter("Slice Of Life"),
|
||||
GenreFilter("Sports"),
|
||||
GenreFilter("Gender Bender"),
|
||||
GenreFilter("Tragedy"),
|
||||
GenreFilter("Bara"),
|
||||
GenreFilter("Shotacon")
|
||||
))
|
||||
|
||||
private class GenreFilter(name: String) : Filter.TriState(name)
|
||||
|
||||
private class StatusFilter(name: String, val uriParam: String) : Filter.CheckBox(name, true), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
uri.appendQueryParameter(uriParam, if (state) "1" else "0")
|
||||
}
|
||||
}
|
||||
|
||||
private class SortFilter : UriSelectFilter("Sort", "sortby", arrayOf(
|
||||
Pair("random", "Random"),
|
||||
Pair("view", "Views"),
|
||||
Pair("comment_count", "Comment Count"),
|
||||
Pair("create_date", "Creation Date"),
|
||||
Pair("update_date", "Update Date")
|
||||
))
|
||||
|
||||
/**
|
||||
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
|
||||
* If an entry is selected it is appended as a query parameter onto the end of the URI.
|
||||
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI.
|
||||
*/
|
||||
//vals: <name, display>
|
||||
private open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
||||
val firstIsUnspecified: Boolean = true,
|
||||
defaultValue: Int = 0) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
if (state != 0 || !firstIsUnspecified)
|
||||
uri.appendQueryParameter(uriParam, vals[state].first)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uri filter group
|
||||
*/
|
||||
private open class UriFilterGroup<V>(name: String, val stateList: List<V>) : Filter.Group<V>(name, stateList), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
stateList.forEach {
|
||||
if (it is UriFilter)
|
||||
it.addToUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a filter that is able to modify a URI.
|
||||
*/
|
||||
private interface UriFilter {
|
||||
fun addToUri(uri: Uri.Builder)
|
||||
}
|
||||
}
|
13
src/en/mangapark/build.gradle
Normal file
13
src/en/mangapark/build.gradle
Normal file
@ -0,0 +1,13 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
appName = 'Tachiyomi: MangaPark'
|
||||
pkgNameSuffix = "en.mangapark"
|
||||
extClass = '.MangaPark'
|
||||
extVersionCode = 1
|
||||
extVersionSuffix = 1
|
||||
libVersion = '1.0'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
@ -0,0 +1,291 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangapark
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* MangaPark source
|
||||
*/
|
||||
|
||||
class MangaPark : ParsedHttpSource() {
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
override val name = "MangaPark"
|
||||
override val baseUrl = "https://mangapark.me"
|
||||
|
||||
private val directorySelector = ".item"
|
||||
private val directoryUrl = "/genre"
|
||||
private val directoryNextPageSelector = ".paging.full > li:last-child > a"
|
||||
|
||||
private val dateFormat = SimpleDateFormat("MMM d, yyyy, HH:mm a", Locale.ENGLISH)
|
||||
|
||||
override fun popularMangaSelector() = directorySelector
|
||||
|
||||
private fun mangaFromElement(element: Element) = SManga.create().apply {
|
||||
val coverElement = element.getElementsByClass("cover").first()
|
||||
url = coverElement.attr("href")
|
||||
|
||||
title = coverElement.attr("title")
|
||||
|
||||
thumbnail_url = coverElement.getElementsByTag("img").attr("src")
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = directoryNextPageSelector
|
||||
|
||||
override fun searchMangaSelector() = ".item"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = ".paging > li:last-child > a"
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl$directoryUrl/$page?views")
|
||||
|
||||
override fun latestUpdatesSelector() = directorySelector
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val uri = Uri.parse("$baseUrl/search").buildUpon()
|
||||
uri.appendQueryParameter("q", query)
|
||||
filters.forEach {
|
||||
if (it is UriFilter)
|
||||
it.addToUri(uri)
|
||||
}
|
||||
uri.appendQueryParameter("page", page.toString())
|
||||
return GET(uri.toString())
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = directoryNextPageSelector
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val coverElement = document.select(".cover > img").first()
|
||||
|
||||
title = coverElement.attr("title")
|
||||
|
||||
thumbnail_url = coverElement.attr("src")
|
||||
|
||||
document.select(".attr > tbody > tr").forEach {
|
||||
val type = it.getElementsByTag("th").first().text().trim().toLowerCase()
|
||||
when (type) {
|
||||
"author(s)" -> {
|
||||
author = it.getElementsByTag("a").joinToString(transform = Element::text)
|
||||
}
|
||||
"artist(s)" -> {
|
||||
artist = it.getElementsByTag("a").joinToString(transform = Element::text)
|
||||
}
|
||||
"genre(s)" -> {
|
||||
genre = it.getElementsByTag("a").joinToString(transform = Element::text)
|
||||
}
|
||||
"status" -> {
|
||||
status = when (it.getElementsByTag("td").text().trim().toLowerCase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description = document.getElementsByClass("summary").text().trim()
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl$directoryUrl/$page?latest")
|
||||
|
||||
//TODO MangaPark has "versioning"
|
||||
//TODO Currently we just use the version that is expanded by default
|
||||
//TODO Maybe make it possible for users to view the other versions as well?
|
||||
override fun chapterListSelector() = ".stream:not(.collapsed) .volume .chapter li"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
url = element.select("em > a").last().attr("href")
|
||||
|
||||
name = element.getElementsByClass("ch").text()
|
||||
|
||||
date_upload = dateFormat.parse(element.getElementsByTag("i").text().trim()).time
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document)
|
||||
= document.getElementsByClass("img").map {
|
||||
Page(it.attr("i").toInt() - 1, "", it.attr("src"))
|
||||
}
|
||||
|
||||
//Unused, we can get image urls directly from the chapter page
|
||||
override fun imageUrlParse(document: Document)
|
||||
= throw UnsupportedOperationException("This method should not be called!")
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
AuthorArtistText(),
|
||||
SearchTypeFilter("Title query", "name-match"),
|
||||
SearchTypeFilter("Author/Artist query", "autart-match"),
|
||||
SortFilter(),
|
||||
GenreGroup(),
|
||||
GenreInclusionFilter(),
|
||||
ChapterCountFilter(),
|
||||
StatusFilter(),
|
||||
RatingFilter(),
|
||||
TypeFilter(),
|
||||
YearFilter()
|
||||
)
|
||||
|
||||
private class SearchTypeFilter(name: String, val uriParam: String) :
|
||||
Filter.Select<String>(name, STATE_MAP), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
uri.appendQueryParameter(uriParam, STATE_MAP[state])
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val STATE_MAP = arrayOf("contain", "begin", "end")
|
||||
}
|
||||
}
|
||||
|
||||
private class AuthorArtistText : Filter.Text("Author/Artist"), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
uri.appendQueryParameter("autart", state)
|
||||
}
|
||||
}
|
||||
|
||||
private class GenreFilter(val uriParam: String, displayName: String) : Filter.TriState(displayName)
|
||||
|
||||
private class GenreGroup : Filter.Group<GenreFilter>("Genres", listOf(
|
||||
GenreFilter("4-koma", "4 koma"),
|
||||
GenreFilter("action", "Action"),
|
||||
GenreFilter("adult", "Adult"),
|
||||
GenreFilter("adventure", "Adventure"),
|
||||
GenreFilter("award-winning", "Award winning"),
|
||||
GenreFilter("comedy", "Comedy"),
|
||||
GenreFilter("cooking", "Cooking"),
|
||||
GenreFilter("demons", "Demons"),
|
||||
GenreFilter("doujinshi", "Doujinshi"),
|
||||
GenreFilter("drama", "Drama"),
|
||||
GenreFilter("ecchi", "Ecchi"),
|
||||
GenreFilter("fantasy", "Fantasy"),
|
||||
GenreFilter("gender-bender", "Gender bender"),
|
||||
GenreFilter("harem", "Harem"),
|
||||
GenreFilter("historical", "Historical"),
|
||||
GenreFilter("horror", "Horror"),
|
||||
GenreFilter("josei", "Josei"),
|
||||
GenreFilter("magic", "Magic"),
|
||||
GenreFilter("martial-arts", "Martial arts"),
|
||||
GenreFilter("mature", "Mature"),
|
||||
GenreFilter("mecha", "Mecha"),
|
||||
GenreFilter("medical", "Medical"),
|
||||
GenreFilter("music", "Music"),
|
||||
GenreFilter("mystery", "Mystery"),
|
||||
GenreFilter("one-shot", "One shot"),
|
||||
GenreFilter("psychological", "Psychological"),
|
||||
GenreFilter("romance", "Romance"),
|
||||
GenreFilter("school-life", "School life"),
|
||||
GenreFilter("sci-fi", "Sci fi"),
|
||||
GenreFilter("seinen", "Seinen"),
|
||||
GenreFilter("shoujo", "Shoujo"),
|
||||
GenreFilter("shoujo-ai", "Shoujo ai"),
|
||||
GenreFilter("shounen", "Shounen"),
|
||||
GenreFilter("shounen-ai", "Shounen ai"),
|
||||
GenreFilter("slice-of-life", "Slice of life"),
|
||||
GenreFilter("smut", "Smut"),
|
||||
GenreFilter("sports", "Sports"),
|
||||
GenreFilter("supernatural", "Supernatural"),
|
||||
GenreFilter("tragedy", "Tragedy"),
|
||||
GenreFilter("webtoon", "Webtoon"),
|
||||
GenreFilter("yaoi", "Yaoi"),
|
||||
GenreFilter("yuri", "Yuri")
|
||||
)), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
uri.appendQueryParameter("genres", state.filter { it.isIncluded() }.map { it.uriParam }.joinToString(","))
|
||||
uri.appendQueryParameter("genres-exclude", state.filter { it.isExcluded() }.map { it.uriParam }.joinToString(","))
|
||||
}
|
||||
}
|
||||
|
||||
private class GenreInclusionFilter : UriSelectFilter("Genre inclusion", "genres-mode", arrayOf(
|
||||
Pair("and", "And mode"),
|
||||
Pair("or", "Or mode")
|
||||
))
|
||||
|
||||
private class ChapterCountFilter : UriSelectFilter("Chapter count", "chapters", arrayOf(
|
||||
Pair("any", "Any"),
|
||||
Pair("1", "1 +"),
|
||||
Pair("5", "5 +"),
|
||||
Pair("10", "10 +"),
|
||||
Pair("20", "20 +"),
|
||||
Pair("30", "30 +"),
|
||||
Pair("40", "40 +"),
|
||||
Pair("50", "50 +"),
|
||||
Pair("100", "100 +"),
|
||||
Pair("150", "150 +"),
|
||||
Pair("200", "200 +")
|
||||
))
|
||||
|
||||
private class StatusFilter : UriSelectFilter("Status", "status", arrayOf(
|
||||
Pair("any", "Any"),
|
||||
Pair("completed", "Completed"),
|
||||
Pair("ongoing", "Ongoing")
|
||||
))
|
||||
|
||||
private class RatingFilter : UriSelectFilter("Rating", "rating", arrayOf(
|
||||
Pair("any", "Any"),
|
||||
Pair("5", "5 stars"),
|
||||
Pair("4", "4 stars"),
|
||||
Pair("3", "3 stars"),
|
||||
Pair("2", "2 stars"),
|
||||
Pair("1", "1 star"),
|
||||
Pair("0", "0 stars")
|
||||
))
|
||||
|
||||
private class TypeFilter : UriSelectFilter("Type", "types", arrayOf(
|
||||
Pair("any", "Any"),
|
||||
Pair("manga", "Japanese Manga"),
|
||||
Pair("manhwa", "Korean Manhwa"),
|
||||
Pair("manhua", "Chinese Manhua"),
|
||||
Pair("unknown", "Unknown")
|
||||
))
|
||||
|
||||
private class YearFilter : UriSelectFilter("Release year", "years",
|
||||
arrayOf(Pair("any", "Any"),
|
||||
//Get all years between today and 1946
|
||||
*(Calendar.getInstance().get(Calendar.YEAR) downTo 1946).map {
|
||||
Pair(it.toString(), it.toString())
|
||||
}.toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
private class SortFilter : UriSelectFilter("Sort", "orderby", arrayOf(
|
||||
Pair("a-z", "A-Z"),
|
||||
Pair("views", "Views"),
|
||||
Pair("rating", "Rating"),
|
||||
Pair("latest", "Latest"),
|
||||
Pair("add", "New manga")
|
||||
), firstIsUnspecified = false, defaultValue = 1)
|
||||
|
||||
/**
|
||||
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
|
||||
* If an entry is selected it is appended as a query parameter onto the end of the URI.
|
||||
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI.
|
||||
*/
|
||||
//vals: <name, display>
|
||||
private open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
||||
val firstIsUnspecified: Boolean = true,
|
||||
defaultValue: Int = 0) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
if (state != 0 || !firstIsUnspecified)
|
||||
uri.appendQueryParameter(uriParam, vals[state].first)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a filter that is able to modify a URI.
|
||||
*/
|
||||
private interface UriFilter {
|
||||
fun addToUri(uri: Uri.Builder)
|
||||
}
|
||||
}
|
18
src/en/tapastic/build.gradle
Normal file
18
src/en/tapastic/build.gradle
Normal file
@ -0,0 +1,18 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
appName = 'Tachiyomi: Tapastic'
|
||||
pkgNameSuffix = "en.tapastic"
|
||||
extClass = '.Tapastic'
|
||||
extVersionCode = 1
|
||||
extVersionSuffix = 1
|
||||
libVersion = '1.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
provided "com.google.code.gson:gson:2.8.0"
|
||||
provided "com.github.salomonbrys.kotson:kotson:2.5.0"
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
@ -0,0 +1,212 @@
|
||||
package eu.kanade.tachiyomi.extension.en.tapastic
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class Tapastic : ParsedHttpSource() {
|
||||
override val lang = "en"
|
||||
override val supportsLatest = true
|
||||
override val name = "Tapastic"
|
||||
override val baseUrl = "https://tapas.io"
|
||||
|
||||
private val browseMangaSelector = ".content-item"
|
||||
private val nextPageSelector = "a.paging-btn.next"
|
||||
|
||||
private val jsonParser by lazy { JsonParser() }
|
||||
|
||||
override fun popularMangaSelector() = browseMangaSelector
|
||||
|
||||
private fun mangaFromElement(element: Element) = SManga.create().apply {
|
||||
val thumb = element.getElementsByClass("thumb-wrap")
|
||||
|
||||
url = thumb.attr("href")
|
||||
|
||||
title = element.getElementsByClass("title").text().trim()
|
||||
|
||||
thumbnail_url = thumb.select("img").attr("src")
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = nextPageSelector
|
||||
|
||||
override fun searchMangaSelector() = "$browseMangaSelector, .search-item-wrap"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = nextPageSelector
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics?pageNumber=$page&browse=POPULAR")
|
||||
|
||||
override fun latestUpdatesSelector() = browseMangaSelector
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
//If there is any search text, use text search, otherwise use filter search
|
||||
val uri = if (query.isNotBlank()) {
|
||||
Uri.parse("$baseUrl/search")
|
||||
.buildUpon()
|
||||
.appendQueryParameter("t", "COMICS")
|
||||
.appendQueryParameter("q", query)
|
||||
} else {
|
||||
val uri = Uri.parse("$baseUrl/comics").buildUpon()
|
||||
//Append uri filters
|
||||
filters.forEach {
|
||||
if (it is UriFilter)
|
||||
it.addToUri(uri)
|
||||
}
|
||||
uri
|
||||
}
|
||||
//Append page number
|
||||
uri.appendQueryParameter("pageNumber", page.toString())
|
||||
return GET(uri.toString())
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = nextPageSelector
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.getElementsByClass("series-header-title").text().trim()
|
||||
|
||||
author = document.getElementsByClass("name").text().trim()
|
||||
artist = author
|
||||
|
||||
description = document.getElementById("series-desc-body").text().trim()
|
||||
|
||||
genre = document.getElementsByClass("genre").text()
|
||||
|
||||
status = SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/comics?pageNumber=$page&browse=FRESH")
|
||||
|
||||
override fun chapterListParse(response: Response)
|
||||
//Chapters are stored in JavaScript as JSON!
|
||||
= response.asJsoup().getElementsByTag("script").filter {
|
||||
it.data().trim().startsWith("var _data")
|
||||
}.flatMap {
|
||||
val text = it.data()
|
||||
val episodeVar = text.indexOf("episodeList")
|
||||
if (episodeVar == -1)
|
||||
return@flatMap emptyList<SChapter>()
|
||||
|
||||
val episodeLeftBracket = text.indexOf('[', startIndex = episodeVar)
|
||||
if (episodeLeftBracket == -1)
|
||||
return@flatMap emptyList<SChapter>()
|
||||
|
||||
val endOfLine = text.indexOf('\n', startIndex = episodeLeftBracket)
|
||||
if (endOfLine == -1)
|
||||
return@flatMap emptyList<SChapter>()
|
||||
|
||||
val episodeRightBracket = text.lastIndexOf(']', startIndex = endOfLine)
|
||||
if (episodeRightBracket == -1)
|
||||
return@flatMap emptyList<SChapter>()
|
||||
|
||||
val episodeListText = text.substring(episodeLeftBracket..episodeRightBracket)
|
||||
|
||||
jsonParser.parse(episodeListText).array.map {
|
||||
val json = it.asJsonObject
|
||||
//Ensure that the chapter is published (tapastic allows scheduling chapters)
|
||||
if (json["orgScene"].int != 0)
|
||||
SChapter.create().apply {
|
||||
url = "/episode/${json["id"].string}"
|
||||
|
||||
name = json["title"].string
|
||||
|
||||
date_upload = json["publishDate"].long
|
||||
|
||||
chapter_number = json["scene"].float
|
||||
}
|
||||
else null
|
||||
}.filterNotNull().sortedByDescending(SChapter::chapter_number)
|
||||
}
|
||||
|
||||
override fun chapterListSelector()
|
||||
= throw UnsupportedOperationException("This method should not be called!")
|
||||
|
||||
override fun chapterFromElement(element: Element)
|
||||
= throw UnsupportedOperationException("This method should not be called!")
|
||||
|
||||
override fun pageListParse(document: Document)
|
||||
= document.getElementsByClass("art-image").mapIndexed { index, element ->
|
||||
Page(index, "", element.attr("src"))
|
||||
}
|
||||
|
||||
//Unused, we can get image urls directly from the chapter page
|
||||
override fun imageUrlParse(document: Document)
|
||||
= throw UnsupportedOperationException("This method should not be called!")
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
//Tapastic does not support genre filtering and text search at the same time
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
FilterFilter(),
|
||||
GenreFilter(),
|
||||
Filter.Separator(),
|
||||
Filter.Header("Sort is ignored when filter is active!"),
|
||||
SortFilter()
|
||||
)
|
||||
|
||||
private class FilterFilter : UriSelectFilter("Filter", "browse", arrayOf(
|
||||
Pair("ALL", "None"),
|
||||
Pair("POPULAR", "Popular"),
|
||||
Pair("TRENDING", "Trending"),
|
||||
Pair("FRESH", "Fresh"),
|
||||
Pair("TAPASTIC", "Staff Picks")
|
||||
), firstIsUnspecified = false, defaultValue = 1)
|
||||
|
||||
private class GenreFilter : UriSelectFilter("Genre", "genreIds", arrayOf(
|
||||
Pair("", "Any"),
|
||||
Pair("7", "Action"),
|
||||
Pair("2", "Comedy"),
|
||||
Pair("8", "Drama"),
|
||||
Pair("3", "Fantasy"),
|
||||
Pair("9", "Gaming"),
|
||||
Pair("6", "Horror"),
|
||||
Pair("10", "Mystery"),
|
||||
Pair("5", "Romance"),
|
||||
Pair("4", "Science Fiction"),
|
||||
Pair("1", "Slice of Life")
|
||||
))
|
||||
|
||||
private class SortFilter : UriSelectFilter("Sort", "sortType", arrayOf(
|
||||
Pair("SUBSCRIBE", "Subscribers"),
|
||||
Pair("LIKE", "Likes"),
|
||||
Pair("VIEW", "Views"),
|
||||
Pair("COMMENT", "Comments"),
|
||||
Pair("CREATED", "Date"),
|
||||
Pair("TITLE", "Name")
|
||||
))
|
||||
|
||||
/**
|
||||
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
|
||||
* If an entry is selected it is appended as a query parameter onto the end of the URI.
|
||||
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI.
|
||||
*/
|
||||
//vals: <name, display>
|
||||
private open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
||||
val firstIsUnspecified: Boolean = true,
|
||||
defaultValue: Int = 0) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
if (state != 0 || !firstIsUnspecified)
|
||||
uri.appendQueryParameter(uriParam, vals[state].first)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a filter that is able to modify a URI.
|
||||
*/
|
||||
private interface UriFilter {
|
||||
fun addToUri(uri: Uri.Builder)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user