Temple Scan: migrate to HeanCms (#18899)

This commit is contained in:
AwkwardPeak7 2023-11-10 18:28:29 +05:00 committed by GitHub
parent 5e121bdb9e
commit 60d8459188
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 58 additions and 303 deletions

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.extension.en.templescan
import eu.kanade.tachiyomi.multisrc.heancms.Genre
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms
import eu.kanade.tachiyomi.network.interceptor.rateLimit
class TempleScan : HeanCms("Temple Scan", "https://templescan.net", "en") {
override val versionId = 3
override val client = super.client.newBuilder()
.rateLimit(1)
.build()
override val useNewQueryEndpoint = true
override val coverPath = ""
override val mangaSubDirectory = "comic"
override fun getGenreList() = listOf(
Genre("Drama", 1),
Genre("Josei", 2),
Genre("Romance", 3),
Genre("Girls Love", 4),
Genre("Reincarnation", 5),
Genre("Fantasia", 6),
Genre("Ecchi", 7),
Genre("Adventure", 8),
Genre("Boys Love", 9),
Genre("School Life", 10),
Genre("Action", 11),
Genre("Military", 13),
Genre("Comedy", 14),
Genre("Apocalypse", 15),
)
}

View File

@ -63,6 +63,8 @@ abstract class HeanCms(
protected open val coverPath: String = "cover/"
protected open val mangaSubDirectory: String = "series"
protected open val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ", Locale.US)
override fun headersBuilder(): Headers.Builder = Headers.Builder()
@ -116,7 +118,7 @@ abstract class HeanCms(
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
}
it.toSManga(apiUrl, coverPath, slugStrategy)
it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
}
fetchAllTitles()
@ -130,7 +132,7 @@ abstract class HeanCms(
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
}
it.toSManga(apiUrl, coverPath, slugStrategy)
it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
}
fetchAllTitles()
@ -186,9 +188,9 @@ abstract class HeanCms(
val manga = SManga.create().apply {
url = if (slugStrategy != SlugStrategy.NONE) {
val mangaId = getIdBySlug(slug)
"/series/${slug.toPermSlugIfNeeded()}#$mangaId"
"/$mangaSubDirectory/${slug.toPermSlugIfNeeded()}#$mangaId"
} else {
"/series/$slug"
"/$mangaSubDirectory/$slug"
}
}
@ -285,7 +287,7 @@ abstract class HeanCms(
.filter { it.type == "Comic" }
.map {
it.slug = it.slug.toPermSlugIfNeeded()
it.toSManga(apiUrl, coverPath, seriesSlugMap.orEmpty(), slugStrategy)
it.toSManga(apiUrl, coverPath, mangaSubDirectory, seriesSlugMap.orEmpty(), slugStrategy)
}
return MangasPage(mangaList, false)
@ -298,7 +300,7 @@ abstract class HeanCms(
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
}
it.toSManga(apiUrl, coverPath, slugStrategy)
it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
}
fetchAllTitles()
@ -312,7 +314,7 @@ abstract class HeanCms(
preferences.slugMap = preferences.slugMap.toMutableMap()
.also { map -> map[it.slug.toPermSlugIfNeeded()] = it.slug }
}
it.toSManga(apiUrl, coverPath, slugStrategy)
it.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
}
fetchAllTitles()
@ -332,7 +334,7 @@ abstract class HeanCms(
seriesSlug
}
return "$baseUrl/series/$currentSlug"
return "$baseUrl/$mangaSubDirectory/$currentSlug"
}
override fun mangaDetailsRequest(manga: SManga): Request {
@ -380,7 +382,7 @@ abstract class HeanCms(
.also { it[seriesResult.slug.toPermSlugIfNeeded()] = seriesResult.slug }
}
val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, slugStrategy)
val seriesDetails = seriesResult.toSManga(apiUrl, coverPath, mangaSubDirectory, slugStrategy)
return seriesDetails.apply {
status = status.takeUnless { it == SManga.UNKNOWN }
@ -404,13 +406,13 @@ abstract class HeanCms(
return result.seasons.orEmpty()
.flatMap { it.chapters.orEmpty() }
.filterNot { it.price == 1 }
.map { it.toSChapter(result.slug, dateFormat, slugStrategy) }
.map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat, slugStrategy) }
.filter { it.date_upload <= currentTimestamp }
}
return result.chapters.orEmpty()
.filterNot { it.price == 1 }
.map { it.toSChapter(result.slug, dateFormat, slugStrategy) }
.map { it.toSChapter(result.slug, mangaSubDirectory, dateFormat, slugStrategy) }
.filter { it.date_upload <= currentTimestamp }
.reversed()
}
@ -419,7 +421,7 @@ abstract class HeanCms(
if (slugStrategy == SlugStrategy.NONE) return baseUrl + chapter.url
val seriesSlug = chapter.url
.substringAfter("/series/")
.substringAfter("/$mangaSubDirectory/")
.substringBefore("/")
.toPermSlugIfNeeded()
@ -432,7 +434,7 @@ abstract class HeanCms(
override fun pageListRequest(chapter: SChapter): Request {
if (useNewQueryEndpoint) {
if (slugStrategy != SlugStrategy.NONE) {
val seriesPermSlug = chapter.url.substringAfter("/series/").substringBefore("/")
val seriesPermSlug = chapter.url.substringAfter("/$mangaSubDirectory/").substringBefore("/")
val seriesSlug = preferences.slugMap[seriesPermSlug] ?: seriesPermSlug
val chapterUrl = chapter.url.replaceFirst(seriesPermSlug, seriesSlug)
return GET(baseUrl + chapterUrl, headers)

View File

@ -36,6 +36,7 @@ data class HeanCmsSearchDto(
fun toSManga(
apiUrl: String,
coverPath: String,
mangaSubDirectory: String,
slugMap: Map<String, HeanCms.HeanCmsTitle>,
slugStrategy: SlugStrategy,
): SManga = SManga.create().apply {
@ -44,7 +45,7 @@ data class HeanCmsSearchDto(
title = this@HeanCmsSearchDto.title
thumbnail_url = thumbnail?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
?: thumbnailFileName?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
url = "/series/$slugOnly"
url = "/$mangaSubDirectory/$slugOnly"
}
}
@ -67,6 +68,7 @@ data class HeanCmsSeriesDto(
fun toSManga(
apiUrl: String,
coverPath: String,
mangaSubDirectory: String,
slugStrategy: SlugStrategy,
): SManga = SManga.create().apply {
val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment)
@ -85,9 +87,9 @@ data class HeanCmsSeriesDto(
?.toAbsoluteThumbnailUrl(apiUrl, coverPath)
status = this@HeanCmsSeriesDto.status?.toStatus() ?: SManga.UNKNOWN
url = if (slugStrategy != SlugStrategy.NONE) {
"/series/$slugOnly#$id"
"/$mangaSubDirectory/$slugOnly#$id"
} else {
"/series/$slug"
"/$mangaSubDirectory/$slug"
}
}
}
@ -112,6 +114,7 @@ data class HeanCmsChapterDto(
) {
fun toSChapter(
seriesSlug: String,
mangaSubDirectory: String,
dateFormat: SimpleDateFormat,
slugStrategy: SlugStrategy,
): SChapter = SChapter.create().apply {
@ -119,7 +122,7 @@ data class HeanCmsChapterDto(
name = this@HeanCmsChapterDto.name.trim()
date_upload = runCatching { dateFormat.parse(createdAt)?.time }
.getOrNull() ?: 0L
url = "/series/$seriesSlugOnly/$slug#$id"
url = "/$mangaSubDirectory/$seriesSlugOnly/$slug#$id"
}
}

View File

@ -16,6 +16,7 @@ class HeanCmsGenerator : ThemeSourceGenerator {
SingleLang("Omega Scans", "https://omegascans.org", "en", isNsfw = true, overrideVersionCode = 18),
SingleLang("Perf Scan", "https://perf-scan.fr", "fr"),
SingleLang("Reaper Scans", "https://reaperscans.net", "pt-BR", overrideVersionCode = 36),
SingleLang("Temple Scan", "https://templescan.net", "en", isNsfw = true, overrideVersionCode = 16),
SingleLang("YugenMangas", "https://yugenmangas.net", "es", isNsfw = true, overrideVersionCode = 9),
)

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".en.templescan.TempleScanUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="templescan.net"
android:pathPattern="/comic/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Temple Scan'
pkgNameSuffix = 'en.templescan'
extClass = '.TempleScan'
extVersionCode = 33
}
apply from: "$rootDir/common.gradle"

View File

@ -1,217 +0,0 @@
package eu.kanade.tachiyomi.extension.en.templescan
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.Calendar
import kotlin.math.min
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 = 2
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1)
.build()
private val json: Json by injectLazy()
private val homeDocument: Document by lazy {
val response = client.newCall(GET(baseUrl, headers)).execute()
if (response.isSuccessful.not()) {
response.close()
throw Exception("Http Error ${response.code}")
}
response.use { it.asJsoup() }
}
private val seriesCache: List<Series> by lazy {
homeDocument.selectFirst("script:containsData(proyectos)")
?.data()
?.let { proyectosRegex.find(it)?.groupValues?.get(1) }
?.let(json::decodeFromString)
?: throw Exception("Unable to extract series information")
}
private lateinit var filteredSeriesCache: List<Series>
private fun List<Series>.toMangasPage(page: Int): MangasPage {
val end = min(page * limit, this.size)
val entries = this.subList((page - 1) * limit, end)
.map(Series::toSManga)
return MangasPage(entries, end < this.size)
}
@Serializable
data class Series(
@SerialName("nombre") val name: String,
val slug: String,
@SerialName("portada") val cover: String,
) {
fun toSManga() = SManga.create().apply {
url = "/comic/$slug"
title = name
thumbnail_url = cover
}
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
val mangasPage = seriesCache.toMangasPage(page)
return Observable.just(mangasPage)
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
val slugs = homeDocument.select("section:contains(new release) figure")
.mapNotNull { element ->
element.selectFirst("a")?.attr("abs:href")
?.substringAfter("/comic/")
?.substringBefore("/")
}
val entries = slugs.mapNotNull { slug ->
seriesCache.firstOrNull { it.slug == slug }?.toSManga()
}
val mangasPage = MangasPage(entries, false)
return Observable.just(mangasPage)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) {
val url = "/comic/${query.substringAfter(SEARCH_PREFIX)}"
val manga = SManga.create().apply { this.url = url }
return fetchMangaDetails(manga).map {
val newManga = it.apply { this.url = url }
MangasPage(listOf(newManga), false)
}
}
if (page == 1) {
filteredSeriesCache = seriesCache.filter {
it.name.contains(query.trim(), true)
}
}
val mangasPage = filteredSeriesCache.toMangasPage(page)
return Observable.just(mangasPage)
}
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
return SManga.create().apply {
thumbnail_url = document.select(".max-w-80 > img").attr("abs:src")
description = document.select("section[id=section-sinopsis] p").text()
title = document.select("h1").text()
genre = document.select("div.flex div:contains(gen) + div a").joinToString { it.text().trim() }
author = document.selectFirst("div.flex div:contains(aut) + div")?.text()
}
}
override fun chapterListParse(response: Response): List<SChapter> {
val elements = response.asJsoup()
.select("div.contenedor-capitulo-miniatura")
return elements.map { element ->
SChapter.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
name = element.select("div[id=name]").text()
date_upload = element.select("time").text().let {
runCatching { it.parseRelativeDate() }.getOrDefault(0L)
}
}
}
}
override fun pageListParse(response: Response): List<Page> {
val elements = response.asJsoup()
.select("main div img")
return elements.mapIndexed { index, element ->
Page(index, "", element.attr("abs:src"))
}
}
private fun String.parseRelativeDate(): Long {
val now = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
var parsedDate = 0L
val relativeDate = this.split(" ")[0].trim().toInt()
when {
"second" in this -> {
parsedDate = now.apply { add(Calendar.SECOND, -relativeDate) }.timeInMillis
}
"minute" in this -> {
parsedDate = now.apply { add(Calendar.MINUTE, -relativeDate) }.timeInMillis
}
"hour" in this -> {
parsedDate = now.apply { add(Calendar.HOUR, -relativeDate) }.timeInMillis
}
"day" in this -> {
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -relativeDate) }.timeInMillis
}
"week" in this -> {
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -relativeDate) }.timeInMillis
}
"month" in this -> {
parsedDate = now.apply { add(Calendar.MONTH, -relativeDate) }.timeInMillis
}
"year" in this -> {
parsedDate = now.apply { add(Calendar.YEAR, -relativeDate) }.timeInMillis
}
}
return parsedDate
}
companion object {
private val proyectosRegex by lazy {
Regex("""proyectos\s*=\s*([^;]+)""")
}
private const val limit = 20
const val SEARCH_PREFIX = "slug:"
}
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException("Not Used")
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException("Not Used")
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not Used")
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException("Not Used")
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not Used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Not Used")
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException("Not Used")
}

View File

@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.extension.en.templescan
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class TempleScanUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val slug = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${TempleScan.SEARCH_PREFIX}$slug")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("TempleScanUrlActivity", e.toString())
}
} else {
Log.e("TempleScanUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}