Temple Scan: migrate to individual extension (#17294)
@ -1,47 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.en.templescan
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
 | 
			
		||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
class TempleScan : Madara(
 | 
			
		||||
    "Temple Scan",
 | 
			
		||||
    "https://templescan.net",
 | 
			
		||||
    "en",
 | 
			
		||||
    SimpleDateFormat("dd.MM.yyyy", Locale.ENGLISH),
 | 
			
		||||
) {
 | 
			
		||||
    override val mangaSubString = "comic"
 | 
			
		||||
    override val useNewChapterEndpoint = true
 | 
			
		||||
    override fun popularMangaSelector() = "div.c-tabs-item > div > div"
 | 
			
		||||
    override val popularMangaUrlSelector = "div.series-box a"
 | 
			
		||||
    override val mangaDetailsSelectorStatus = ".post-content_item:contains(Status) .summary-content"
 | 
			
		||||
 | 
			
		||||
    override val client: OkHttpClient = super.client.newBuilder()
 | 
			
		||||
        .rateLimit(1)
 | 
			
		||||
        .build()
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaFromElement(element: Element): SManga {
 | 
			
		||||
        return super.popularMangaFromElement(element).apply {
 | 
			
		||||
            title = element.select(popularMangaUrlSelector).text()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchPage(page: Int): String {
 | 
			
		||||
        return if (page > 1) {
 | 
			
		||||
            "page/$page/"
 | 
			
		||||
        } else {
 | 
			
		||||
            ""
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun chapterFromElement(element: Element): SChapter {
 | 
			
		||||
        return super.chapterFromElement(element).apply {
 | 
			
		||||
            name = element.select(".chapter-manhwa-title").text()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -407,7 +407,6 @@ class MadaraGenerator : ThemeSourceGenerator {
 | 
			
		||||
        SingleLang("Taurus Fansub", "https://taurusfansub.com", "es"),
 | 
			
		||||
        SingleLang("Tecno Scan", "https://tecnoscann.com", "es"),
 | 
			
		||||
        SingleLang("TeenManhua", "https://teenmanhua.com", "en", overrideVersionCode = 1),
 | 
			
		||||
        SingleLang("Temple Scan", "https://templescan.net", "en"),
 | 
			
		||||
        SingleLang("The Beginning After The End", "https://www.thebeginningaftertheend.fr", "fr", overrideVersionCode = 1),
 | 
			
		||||
        SingleLang("The Guild", "https://theguildscans.com", "en"),
 | 
			
		||||
        SingleLang("The Sugar", "https://thesugarscan.com", "pt-BR"),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								src/en/templescan/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,24 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    package="eu.kanade.tachiyomi.extension">
 | 
			
		||||
 | 
			
		||||
    <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>
 | 
			
		||||
							
								
								
									
										12
									
								
								src/en/templescan/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,12 @@
 | 
			
		||||
apply plugin: 'com.android.application'
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
apply plugin: 'kotlinx-serialization'
 | 
			
		||||
 | 
			
		||||
ext {
 | 
			
		||||
    extName = 'Temple Scan'
 | 
			
		||||
    pkgNameSuffix = 'en.templescan'
 | 
			
		||||
    extClass = '.TempleScan'
 | 
			
		||||
    extVersionCode = 32
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB  | 
| 
		 Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB  | 
| 
		 Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB  | 
| 
		 Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB  | 
@ -0,0 +1,199 @@
 | 
			
		||||
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 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 = false
 | 
			
		||||
 | 
			
		||||
    override val versionId = 2
 | 
			
		||||
 | 
			
		||||
    override val client = network.cloudflareClient.newBuilder()
 | 
			
		||||
        .rateLimit(1)
 | 
			
		||||
        .build()
 | 
			
		||||
 | 
			
		||||
    private val json: Json by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val seriesCache: List<Series> by lazy {
 | 
			
		||||
        val response = client.newCall(GET(baseUrl, headers)).execute()
 | 
			
		||||
 | 
			
		||||
        if (response.isSuccessful.not()) {
 | 
			
		||||
            response.close()
 | 
			
		||||
            throw Exception("Http Error ${response.code}")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        response.asJsoup()
 | 
			
		||||
            .selectFirst("script:containsData(proyectos)")
 | 
			
		||||
            ?.data()
 | 
			
		||||
            ?.let { proyectosRegex.find(it)?.groupValues?.get(1) }
 | 
			
		||||
            ?.let(json::decodeFromString)
 | 
			
		||||
            ?: throw Exception(SeriesCacheFailureException)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 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 a")
 | 
			
		||||
 | 
			
		||||
        return elements.map { element ->
 | 
			
		||||
            SChapter.create().apply {
 | 
			
		||||
                setUrlWithoutDomain(element.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 = try {
 | 
			
		||||
            this.split(" ")[0].trim().toInt()
 | 
			
		||||
        } catch (e: NumberFormatException) {
 | 
			
		||||
            return 0L
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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 = Regex("""proyectos\s*=\s*([^\;]+)""")
 | 
			
		||||
        private const val SeriesCacheFailureException = "Unable to extract series information"
 | 
			
		||||
 | 
			
		||||
        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")
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||