-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* temple * rewrite * description & tags cleanup * readd ratelimit * cloudflareclient * optimize
- Loading branch information
1 parent
ab39d3e
commit f51b1bb
Showing
4 changed files
with
345 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
src/en/templescan/src/eu/kanade/tachiyomi/extension/en/templescan/Dto.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package eu.kanade.tachiyomi.extension.en.templescan | ||
|
||
import eu.kanade.tachiyomi.source.model.SManga | ||
import kotlinx.serialization.SerialName | ||
import kotlinx.serialization.Serializable | ||
import java.text.ParseException | ||
import java.text.SimpleDateFormat | ||
import java.util.Locale | ||
|
||
@Serializable | ||
class BrowseSeries( | ||
@SerialName("series_slug") private val slug: String, | ||
val title: String, | ||
@SerialName("alternative_names") val alternativeNames: String? = null, | ||
private val thumbnail: String? = null, | ||
val status: String? = null, | ||
@SerialName("update_chapter") private val updatedAt: String? = null, | ||
@SerialName("created_at") private val createdAt: String? = null, | ||
@SerialName("total_views") val views: Long = 0, | ||
) { | ||
val updated: Long by lazy { | ||
dateFormat.tryParse(updatedAt) | ||
} | ||
|
||
val created: Long by lazy { | ||
dateFormat.tryParse(createdAt) | ||
} | ||
|
||
fun toSManga() = SManga.create().apply { | ||
url = "/comic/$slug" | ||
title = this@BrowseSeries.title | ||
thumbnail_url = thumbnail | ||
} | ||
} | ||
|
||
@Serializable | ||
class SeriesDetails( | ||
@SerialName("series_slug") val slug: String, | ||
val title: String, | ||
val thumbnail: String? = null, | ||
val author: String? = null, | ||
val studio: String? = null, | ||
@SerialName("release_year") val year: String? = null, | ||
@SerialName("alternative_names") val alternativeNames: String? = null, | ||
val adult: Boolean = false, | ||
val badge: String? = null, | ||
val status: String? = null, | ||
) | ||
|
||
@Serializable | ||
class ChapterList( | ||
@SerialName("Season") val seasons: List<Chapters>, | ||
) { | ||
@Serializable | ||
class Chapters( | ||
@SerialName("Chapter") val chapters: List<Chapter>, | ||
) { | ||
@Serializable | ||
class Chapter( | ||
@SerialName("chapter_name") val name: String, | ||
@SerialName("chapter_title") val title: String? = null, | ||
@SerialName("chapter_slug") val slug: String, | ||
val price: Int, | ||
@SerialName("created_at") private val createdAt: String? = null, | ||
) { | ||
val created: Long by lazy { | ||
dateFormat.tryParse(createdAt) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH) | ||
|
||
private fun SimpleDateFormat.tryParse(date: String?): Long { | ||
date ?: return 0L | ||
|
||
return try { | ||
parse(date)?.time ?: 0L | ||
} catch (_: ParseException) { | ||
0L | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
src/en/templescan/src/eu/kanade/tachiyomi/extension/en/templescan/Filters.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package eu.kanade.tachiyomi.extension.en.templescan | ||
|
||
import eu.kanade.tachiyomi.source.model.Filter | ||
import eu.kanade.tachiyomi.source.model.FilterList | ||
|
||
abstract class SelectFilter( | ||
name: String, | ||
private val options: List<Pair<String, String>>, | ||
defaultValue: String? = null, | ||
) : Filter.Select<String>( | ||
name, | ||
options.map { it.first }.toTypedArray(), | ||
options.indexOfFirst { it.second == defaultValue }.coerceAtLeast(0), | ||
) { | ||
val selected get() = options[state].second.takeUnless { it.isBlank() } | ||
} | ||
|
||
class StatusFilter : SelectFilter( | ||
"Status", | ||
listOf( | ||
"", | ||
"Ongoing", | ||
"Hiatus", | ||
"Completed", | ||
"Canceled", | ||
"Dropped", | ||
).map { it to it }, | ||
) | ||
|
||
class OrderFilter(default: String? = null) : SelectFilter( | ||
"Order by", | ||
listOf( | ||
"Update Chapter" to "updated", | ||
"Created At" to "created", | ||
"Trending" to "views", | ||
), | ||
default, | ||
) { | ||
companion object { | ||
val POPULAR = FilterList(OrderFilter("views")) | ||
val LATEST = FilterList(OrderFilter("updated")) | ||
} | ||
} | ||
|
||
fun getFilters() = FilterList( | ||
StatusFilter(), | ||
OrderFilter(), | ||
) |
223 changes: 213 additions & 10 deletions
223
src/en/templescan/src/eu/kanade/tachiyomi/extension/en/templescan/TempleScan.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,225 @@ | ||
package eu.kanade.tachiyomi.extension.en.templescan | ||
|
||
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms | ||
import eu.kanade.tachiyomi.network.GET | ||
import eu.kanade.tachiyomi.network.interceptor.rateLimit | ||
import eu.kanade.tachiyomi.source.model.Filter | ||
import eu.kanade.tachiyomi.source.model.FilterList | ||
import eu.kanade.tachiyomi.source.model.MangasPage | ||
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.HttpSource | ||
import eu.kanade.tachiyomi.util.asJsoup | ||
import kotlinx.serialization.decodeFromString | ||
import kotlinx.serialization.json.Json | ||
import okhttp3.Request | ||
import okhttp3.Response | ||
import rx.Observable | ||
import uy.kohesive.injekt.injectLazy | ||
import kotlin.math.min | ||
|
||
class TempleScan : HeanCms( | ||
"Temple Scan", | ||
"https://templescan.net", | ||
"en", | ||
apiUrl = "https://templescan.net/apiv1", | ||
) { | ||
class TempleScan : HttpSource() { | ||
|
||
override val name = "Temple Scan" | ||
|
||
override val lang = "en" | ||
|
||
override val baseUrl = "https://templescan.net" | ||
|
||
override val supportsLatest = true | ||
|
||
override val versionId = 3 | ||
|
||
override val client = super.client.newBuilder() | ||
override fun headersBuilder() = super.headersBuilder() | ||
.set("referer", "$baseUrl/") | ||
.set("origin", baseUrl) | ||
|
||
override val client = network.cloudflareClient.newBuilder() | ||
.rateLimit(1) | ||
.build() | ||
|
||
override val mangaSubDirectory = "comic" | ||
private val json: Json by injectLazy() | ||
|
||
override fun fetchPopularManga(page: Int): Observable<MangasPage> { | ||
return fetchSearchManga(page, "", OrderFilter.POPULAR) | ||
} | ||
|
||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { | ||
return fetchSearchManga(page, "", OrderFilter.LATEST) | ||
} | ||
|
||
private lateinit var seriesCache: List<BrowseSeries> | ||
|
||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { | ||
if (page == 1) { | ||
client.newCall(searchMangaRequest(page, query, filters)) | ||
.execute() | ||
.use(::parseSearchResponse) | ||
} | ||
|
||
return Observable.just(parseDirectory(page, query, filters)) | ||
} | ||
|
||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||
return GET("$baseUrl/comics", headers) | ||
} | ||
|
||
private fun parseSearchResponse(response: Response) { | ||
val document = response.asJsoup() | ||
val script = document.selectFirst("script:containsData(allComics)")!! | ||
.data().unescape() | ||
|
||
with(script) { | ||
val raw = substringAfter("""allComics":""") | ||
.substringBeforeLast("}]]") | ||
|
||
seriesCache = raw.parseAs() | ||
} | ||
} | ||
|
||
private fun parseDirectory(page: Int, query: String, filters: FilterList): MangasPage { | ||
val status = filters.get<StatusFilter>()?.selected | ||
val mangaList = seriesCache.filter { series -> | ||
|
||
val queryFilter = query.isBlank() || | ||
series.title.contains(query, ignoreCase = true) || | ||
series.alternativeNames?.contains(query, ignoreCase = true) == true | ||
|
||
val statusFilter = status == null || series.status == status | ||
|
||
override val enableLogin = true | ||
queryFilter && statusFilter | ||
}.let { | ||
val order = filters.get<OrderFilter>()?.selected | ||
|
||
when (order) { | ||
"updated" -> it.sortedByDescending { series -> series.updated } | ||
"created" -> it.sortedByDescending { series -> series.created } | ||
"views" -> it.sortedByDescending { series -> series.views } | ||
else -> it | ||
} | ||
} | ||
|
||
return MangasPage( | ||
mangas = mangaList.subList((page - 1) * 20, min(page * 20, mangaList.size)) | ||
.map { it.toSManga() }, | ||
hasNextPage = page * 20 < mangaList.size, | ||
) | ||
} | ||
|
||
override fun getFilterList() = getFilters() | ||
|
||
override fun mangaDetailsParse(response: Response): SManga { | ||
val document = response.asJsoup() | ||
val details = DETAILS_REGEX.find(document.body().outerHtml())!!.groupValues[1] | ||
.unescape() | ||
.parseAs<SeriesDetails>() | ||
|
||
val tags = mutableListOf<String>() | ||
|
||
return SManga.create().apply { | ||
url = "/comic/${details.slug}" | ||
title = details.title | ||
thumbnail_url = details.thumbnail | ||
status = when (details.status) { | ||
"Ongoing" -> SManga.ONGOING | ||
"Hiatus" -> SManga.ON_HIATUS | ||
"Completed" -> SManga.COMPLETED | ||
"Canceled" -> SManga.CANCELLED | ||
"Dropped" -> SManga.CANCELLED | ||
else -> SManga.UNKNOWN | ||
} | ||
author = details.author | ||
artist = details.studio | ||
description = buildString { | ||
document.selectFirst("div:has(> p:contains(description))")?.run { | ||
selectFirst("p:contains(description)")?.remove() | ||
selectFirst("div.mt-7:contains(Additional)")?.remove() | ||
selectFirst("div.mt-7:contains(tag)")?.also { | ||
tags += it.select("div.flex > p[class^=bg]").eachText() | ||
}?.remove() | ||
selectFirst("p:contains(tag), p:contains(genre)")?.let { | ||
tags += it.text().substringAfter(":") | ||
.split(",") | ||
.map(String::trim) | ||
// sometimes description <p> have the tag/genre, instead of it being separate | ||
val tmp = clone() | ||
tmp.selectFirst("p:contains(tag), p:contains(genre)") | ||
?.remove() | ||
if (tmp.text().isNotBlank()) { | ||
it.remove() | ||
} | ||
} | ||
|
||
this@buildString.append(wholeText().trim()) | ||
} | ||
|
||
if (!details.alternativeNames.isNullOrBlank()) { | ||
if (isNotBlank()) { | ||
append("\n\n") | ||
} | ||
append("Alternative Name: ", details.alternativeNames, "\n") | ||
} | ||
} | ||
genre = buildList { | ||
add(details.badge) | ||
add(details.year) | ||
if (details.adult) { | ||
add("Adult") | ||
} | ||
addAll(tags.distinct()) | ||
}.filterNotNull().joinToString() | ||
} | ||
} | ||
|
||
override fun chapterListParse(response: Response): List<SChapter> { | ||
val chapters = DETAILS_REGEX.find(response.body.string())!!.groupValues[1] | ||
.unescape() | ||
.parseAs<ChapterList>() | ||
val mangaSlug = response.request.url.pathSegments.last() | ||
|
||
return chapters.seasons.flatMap { season -> | ||
season.chapters.filter { | ||
it.price == 0 | ||
}.map { chapter -> | ||
SChapter.create().apply { | ||
url = "/comic/$mangaSlug/${chapter.slug}" | ||
name = buildString { | ||
append(chapter.name) | ||
if (!chapter.title.isNullOrBlank()) { | ||
append(": ", chapter.title) | ||
} | ||
} | ||
date_upload = chapter.created | ||
} | ||
} | ||
} | ||
} | ||
|
||
override fun pageListParse(response: Response): List<Page> { | ||
return response.asJsoup().select("img[alt^=chapter]").mapIndexed { idx, img -> | ||
Page(idx, imageUrl = img.absUrl("src")) | ||
} | ||
} | ||
|
||
private fun String.unescape(): String { | ||
return UNESCAPE_REGEX.replace(this, "$1") | ||
} | ||
|
||
private inline fun <reified T> String.parseAs(): T { | ||
return json.decodeFromString(this) | ||
} | ||
|
||
private inline fun <reified T : Filter<*>> FilterList.get(): T? { | ||
return filterIsInstance<T>().firstOrNull() | ||
} | ||
|
||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() | ||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() | ||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() | ||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() | ||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() | ||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() | ||
} | ||
|
||
private val UNESCAPE_REGEX = """\\(.)""".toRegex() | ||
private val DETAILS_REGEX = Regex("""info\\":(\{.*\}).*userIsFollowed""") |