Skip to content

Commit

Permalink
TempleScan (#4316)
Browse files Browse the repository at this point in the history
* temple

* rewrite

* description & tags cleanup

* readd ratelimit

* cloudflareclient

* optimize
  • Loading branch information
AwkwardPeak7 authored and cuong-tran committed Jul 29, 2024
1 parent ab39d3e commit f51b1bb
Show file tree
Hide file tree
Showing 4 changed files with 345 additions and 13 deletions.
4 changes: 1 addition & 3 deletions src/en/templescan/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
ext {
extName = 'Temple Scan'
extClass = '.TempleScan'
themePkg = 'heancms'
baseUrl = 'https://templescan.net'
overrideVersionCode = 17
extVersionCode = 43
isNsfw = true
}

Expand Down
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
}
}
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(),
)
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""")

0 comments on commit f51b1bb

Please sign in to comment.