parent
c64cf2948e
commit
9ad04a5b01
|
@ -5,7 +5,7 @@ ext {
|
||||||
appName = 'Tachiyomi: Tapas'
|
appName = 'Tachiyomi: Tapas'
|
||||||
pkgNameSuffix = 'en.tapastic'
|
pkgNameSuffix = 'en.tapastic'
|
||||||
extClass = '.Tapastic'
|
extClass = '.Tapastic'
|
||||||
extVersionCode = 4
|
extVersionCode = 5
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,64 +1,62 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.tapastic
|
package eu.kanade.tachiyomi.extension.en.tapastic
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.*
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class Tapastic : ParsedHttpSource() {
|
class Tapastic : ParsedHttpSource() {
|
||||||
|
|
||||||
|
//Info
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
override val name = "Tapastic"
|
override val name = "Tapastic"
|
||||||
override val baseUrl = "https://tapas.io"
|
override val baseUrl = "https://tapas.io"
|
||||||
|
|
||||||
private val browseMangaSelector = ".content-item"
|
//Popular
|
||||||
private val nextPageSelector = "a.paging-btn.next"
|
|
||||||
|
|
||||||
private val gson by lazy { Gson() }
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/comics?b=POPULAR&g=&f=NONE&pageNumber=$page&pageSize=20&")
|
||||||
|
|
||||||
override fun popularMangaSelector() = browseMangaSelector
|
override fun popularMangaNextPageSelector() = "div[data-has-next=true]"
|
||||||
|
override fun popularMangaSelector() = "li.js-list-item"
|
||||||
private fun mangaFromElement(element: Element) = SManga.create().apply {
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
val thumb = element.getElementsByClass("thumb-wrap")
|
url = element.select(".item__thumb a").attr("href")
|
||||||
|
title = element.select(".item__thumb img").attr("alt")
|
||||||
url = thumb.attr("href")
|
thumbnail_url = element.select(".item__thumb img").attr("src")
|
||||||
|
|
||||||
title = element.getElementsByClass("title").text().trim()
|
|
||||||
|
|
||||||
thumbnail_url = thumb.select("img").attr("src")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = mangaFromElement(element)
|
//Latest
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = nextPageSelector
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/comics?b=FRESH&g=&f=NONE&pageNumber=$page&pageSize=20&")
|
||||||
|
|
||||||
override fun searchMangaSelector() = "$browseMangaSelector, .search-item-wrap"
|
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
|
||||||
|
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||||
|
popularMangaFromElement(element)
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
//Search
|
||||||
|
|
||||||
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 {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
//If there is any search text, use text search, otherwise use filter search
|
//If there is any search text, use text search, otherwise use filter search
|
||||||
val uri = if (query.isNotBlank()) {
|
val uri = if (query.isNotBlank()) {
|
||||||
Uri.parse("$baseUrl/search")
|
Uri.parse("$baseUrl/search")
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.appendQueryParameter("t", "COMICS")
|
.appendQueryParameter("t", "COMICS")
|
||||||
.appendQueryParameter("q", query)
|
.appendQueryParameter("q", query)
|
||||||
} else {
|
} else {
|
||||||
val uri = Uri.parse("$baseUrl/comics").buildUpon()
|
val uri = Uri.parse("$baseUrl/comics").buildUpon()
|
||||||
//Append uri filters
|
//Append uri filters
|
||||||
|
@ -73,104 +71,153 @@ class Tapastic : ParsedHttpSource() {
|
||||||
return GET(uri.toString())
|
return GET(uri.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = nextPageSelector
|
override fun searchMangaNextPageSelector() =
|
||||||
|
"${popularMangaNextPageSelector()}, a.paging__button--next"
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun searchMangaSelector() = "${popularMangaSelector()}, .search-item-wrap"
|
||||||
title = document.getElementsByClass("series-header-title").text().trim()
|
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
|
url = element.select(".item__thumb a, .title-section .title a").attr("href")
|
||||||
author = document.getElementsByClass("name").text().trim()
|
val browseTitle = element.select(".item__thumb img")
|
||||||
artist = author
|
title = if (browseTitle != null) {
|
||||||
|
browseTitle.attr("alt")
|
||||||
description = document.getElementById("series-desc-body").text().trim()
|
} else {
|
||||||
|
element.select(".title-section .title a").text()
|
||||||
genre = document.getElementsByClass("genre").joinToString { it.text() }
|
}
|
||||||
|
thumbnail_url = element.select(".item__thumb img, .thumb-wrap img").attr("src")
|
||||||
status = SManga.UNKNOWN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/comics?pageNumber=$page&browse=FRESH")
|
//Details
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
title = document.select(".desc__title").text().trim()
|
||||||
|
author = document.select(".tag__author").text().trim()
|
||||||
|
artist = author
|
||||||
|
description = document.select(".js-series-description").text().trim()
|
||||||
|
genre = document.select("div.info__genre a, div.item__genre a")
|
||||||
|
.joinToString(", ") { it.text() }
|
||||||
|
}
|
||||||
|
|
||||||
|
//Chapters
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
return GET(baseUrl + manga.url + "?sort_order=desc", headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
//Chapters are stored in JavaScript as JSON!
|
var document = response.asJsoup()
|
||||||
return response.asJsoup().select("script:containsData(_data)").first()?.data().let { script ->
|
val baseUri = document.baseUri().substringBefore("?")
|
||||||
if (script.isNullOrEmpty() || !script.contains("episodeList : [")) {
|
val chapters = mutableListOf<SChapter>()
|
||||||
emptyList()
|
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
||||||
} else {
|
var nextPage = document.select(".paging__button--next:not(.disabled)")
|
||||||
gson.fromJson<JsonArray>(script.substringAfter("episodeList : ").substringBefore(",\n"))
|
while (!nextPage.isNullOrEmpty()) {
|
||||||
//Ensure that the chapter is published (source allows scheduling chapters)
|
document = client.newCall(GET(baseUri + nextPage.attr("href"))).execute().asJsoup()
|
||||||
.filter { it["orgScene"].int != 0 }
|
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
||||||
.map { json ->
|
nextPage = document.select(".paging__button--next:not(.disabled)")
|
||||||
SChapter.create().apply {
|
}
|
||||||
url = "/episode/${json["id"].string}"
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
name = (if (json["locked"].asBoolean) "\uD83D\uDD12" else "") + json["title"].string
|
override fun chapterListSelector() = "li.content__item"
|
||||||
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
|
val lock = !element.select(".sp-ico-episode-lock, .sp-ico-schedule-white").isNullOrEmpty()
|
||||||
|
name = if (lock) {
|
||||||
|
"\uD83D\uDD12 "
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
} + element.select(".info__title").text().trim()
|
||||||
|
|
||||||
date_upload = json["publishDate"].long
|
url = if (lock) {
|
||||||
|
"locked"
|
||||||
|
} else {
|
||||||
|
element.select("a").first().attr("href")
|
||||||
|
}
|
||||||
|
chapter_number =
|
||||||
|
element.select(".info__header").text().substringAfter("Episode")
|
||||||
|
.substringBefore("Early access").trim().toFloat()
|
||||||
|
|
||||||
chapter_number = json["scene"].float
|
date_upload =
|
||||||
}
|
parseDate(element.select(".info__tag").text().substringAfter(":").substringBefore("•").trim())
|
||||||
}.reversed()
|
}
|
||||||
}
|
|
||||||
|
private fun parseDate(date: String): Long {
|
||||||
|
return SimpleDateFormat("MMM dd, yyyy", Locale.US).parse(date)?.time ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//Pages
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
if (chapter.url == "locked") throw Exception("Chapter Locked. If logged in, refresh chapter list.")
|
||||||
|
return GET(baseUrl + chapter.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
|
||||||
|
document.select("img.content__img").forEach {
|
||||||
|
add(Page(size, "", it.attr("src")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListSelector()
|
override fun imageUrlParse(document: Document) =
|
||||||
= throw UnsupportedOperationException("This method should not be called!")
|
throw UnsupportedOperationException("This method should not be called!")
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element)
|
//Filters
|
||||||
= 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(
|
override fun getFilterList() = FilterList(
|
||||||
//Tapastic does not support genre filtering and text search at the same time
|
//Tapastic does not support genre filtering and text search at the same time
|
||||||
Filter.Header("NOTE: Ignored if using text search!"),
|
Filter.Header("NOTE: Ignored if using text search!"),
|
||||||
Filter.Separator(),
|
Filter.Separator(),
|
||||||
FilterFilter(),
|
FilterFilter(),
|
||||||
GenreFilter(),
|
GenreFilter(),
|
||||||
Filter.Separator(),
|
StatusFilter(),
|
||||||
Filter.Header("Sort is ignored when filter is active!"),
|
Filter.Separator(),
|
||||||
SortFilter()
|
Filter.Header("Sort is ignored when filter is active!"),
|
||||||
|
SortFilter()
|
||||||
)
|
)
|
||||||
|
|
||||||
private class FilterFilter : UriSelectFilter("Filter", "browse", arrayOf(
|
private class FilterFilter : UriSelectFilter(
|
||||||
|
"Filter", "b", arrayOf(
|
||||||
Pair("ALL", "None"),
|
Pair("ALL", "None"),
|
||||||
Pair("POPULAR", "Popular"),
|
Pair("POPULAR", "Popular"),
|
||||||
Pair("TRENDING", "Trending"),
|
Pair("TRENDING", "Trending"),
|
||||||
Pair("FRESH", "Fresh"),
|
Pair("FRESH", "Fresh"),
|
||||||
Pair("TAPASTIC", "Staff Picks")
|
Pair("BINGE", "Binge"),
|
||||||
), firstIsUnspecified = false, defaultValue = 1)
|
Pair("ORIGINAL", "Tapas Originals")
|
||||||
|
), firstIsUnspecified = false, defaultValue = 1
|
||||||
|
)
|
||||||
|
|
||||||
private class GenreFilter : UriSelectFilter("Genre", "genreIds", arrayOf(
|
private class GenreFilter : UriSelectFilter(
|
||||||
|
"Genre", "g", arrayOf(
|
||||||
Pair("", "Any"),
|
Pair("", "Any"),
|
||||||
Pair("7", "Action"),
|
Pair("7", "Action"),
|
||||||
Pair("22", "Boys Love"),
|
Pair("22", "Boys Love"),
|
||||||
Pair("2", "Comedy"),
|
Pair("2", "Comedy"),
|
||||||
Pair("8", "Drama"),
|
Pair("8", "Drama"),
|
||||||
Pair("3", "Fantasy"),
|
Pair("3", "Fantasy"),
|
||||||
|
Pair("24", "Girls Love"),
|
||||||
Pair("9", "Gaming"),
|
Pair("9", "Gaming"),
|
||||||
Pair("6", "Horror"),
|
Pair("6", "Horror"),
|
||||||
|
Pair("25", "LGBTQ+"),
|
||||||
Pair("10", "Mystery"),
|
Pair("10", "Mystery"),
|
||||||
Pair("5", "Romance"),
|
Pair("5", "Romance"),
|
||||||
Pair("4", "Science Fiction"),
|
Pair("4", "Science Fiction"),
|
||||||
Pair("1", "Slice of Life")
|
Pair("1", "Slice of Life")
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
private class SortFilter : UriSelectFilter("Sort", "sortType", arrayOf(
|
private class StatusFilter : UriSelectFilter(
|
||||||
Pair("SUBSCRIBE", "Subscribers"),
|
"Status", "f", arrayOf(
|
||||||
|
Pair("NONE", "All"),
|
||||||
|
Pair("F2R", "Free to read"),
|
||||||
|
Pair("PRM", "Premium")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class SortFilter : UriSelectFilter(
|
||||||
|
"Sort", "s", arrayOf(
|
||||||
|
Pair("DATE", "Date"),
|
||||||
Pair("LIKE", "Likes"),
|
Pair("LIKE", "Likes"),
|
||||||
Pair("VIEW", "Views"),
|
Pair("SUBSCRIBE", "Subscribers")
|
||||||
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.
|
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
|
||||||
|
@ -178,10 +225,13 @@ class Tapastic : ParsedHttpSource() {
|
||||||
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the 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>
|
//vals: <name, display>
|
||||||
private open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
private open class UriSelectFilter(
|
||||||
val firstIsUnspecified: Boolean = true,
|
displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
||||||
defaultValue: Int = 0) :
|
val firstIsUnspecified: Boolean = true,
|
||||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
defaultValue: Int = 0
|
||||||
|
) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue),
|
||||||
|
UriFilter {
|
||||||
override fun addToUri(uri: Uri.Builder) {
|
override fun addToUri(uri: Uri.Builder) {
|
||||||
if (state != 0 || !firstIsUnspecified)
|
if (state != 0 || !firstIsUnspecified)
|
||||||
uri.appendQueryParameter(uriParam, vals[state].first)
|
uri.appendQueryParameter(uriParam, vals[state].first)
|
||||||
|
|
Loading…
Reference in New Issue