LectorJPG: Update theme (#10367)
* update theme * update logo * review changes
@ -1,9 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'LectorJPG'
|
extName = 'LectorJPG'
|
||||||
extClass = '.LectorJpg'
|
extClass = '.LectorJpg'
|
||||||
themePkg = 'madara'
|
extVersionCode = 44
|
||||||
baseUrl = 'https://lectorjpg.com'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 14 KiB |
@ -1,81 +1,292 @@
|
|||||||
package eu.kanade.tachiyomi.extension.es.lectorjpg
|
package eu.kanade.tachiyomi.extension.es.lectorjpg
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import android.util.Base64
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import org.jsoup.nodes.Document
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
class LectorJpg : Madara(
|
class LectorJpg : HttpSource() {
|
||||||
"LectorJPG",
|
|
||||||
"https://lectorjpg.com",
|
|
||||||
"es",
|
|
||||||
dateFormat = SimpleDateFormat("d MMMM, yyyy", Locale("es")),
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val versionId = 2
|
override val versionId = 3
|
||||||
|
|
||||||
override val mangaSubString = "serie"
|
override val name = "LectorJPG"
|
||||||
|
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
override val lang = "es"
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
override val baseUrl = "https://lectorjpg.com"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 3, 1)
|
.rateLimitHost(baseUrl.toHttpUrl(), 3, 1)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div:not([class]):has(> div.break-words)"
|
class LimitedCache<K, V>() : LinkedHashMap<K, V>() {
|
||||||
|
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>): Boolean {
|
||||||
|
return size > 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
data class SearchKey(val page: Int, val query: String, val filters: String?)
|
||||||
|
|
||||||
|
private val latestMangaCursor = LimitedCache<Int, String?>()
|
||||||
|
private val searchMangaCursor = LimitedCache<SearchKey, String?>()
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET(baseUrl, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val mangas = document.select("div.relative div.flex.w-fit article").map { element ->
|
||||||
|
SManga.create().apply {
|
||||||
title = element.selectFirst("h3")!!.text()
|
title = element.selectFirst("h3")!!.text()
|
||||||
thumbnail_url = element.selectFirst("img")?.let { imageFromElement(it) }
|
url = element.selectFirst("a")!!.attr("href").substringAfterLast("/series/").removeSuffix("/")
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
thumbnail_url = element.selectFirst("div.bg-cover")?.imageFromStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MangasPage(mangas, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = "button.group > div.grid"
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val cursor = latestMangaCursor[page - 1] ?: createLatestCursor()
|
||||||
|
val url = "$baseUrl/serie-query".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("cursor", cursor)
|
||||||
|
.addQueryParameter("perPage", "35")
|
||||||
|
.addQueryParameter("type", "updated")
|
||||||
|
.fragment(page.toString())
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
return GET(url.build(), headers)
|
||||||
title = element.selectFirst("h3")!!.text()
|
|
||||||
thumbnail_url = element.selectFirst("div[style].bg-cover")?.let { imageFromElement(it) }
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val mangaDetailsSelectorTitle = "div.wp-manga div.grid > h1"
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
override val mangaDetailsSelectorStatus = "div.wp-manga div[alt=type]:eq(0) > span"
|
val page = response.request.url.fragment!!.toInt()
|
||||||
override val mangaDetailsSelectorGenre = "div.wp-manga div[alt=type]:gt(0) > span"
|
val result = response.parseAs<SeriesQueryDto>()
|
||||||
override val mangaDetailsSelectorDescription = "div.wp-manga div#expand_content"
|
latestMangaCursor[page] = result.nextCursor
|
||||||
override val mangaDetailsSelectorThumbnail = "div.grid.border div.bg-cover"
|
val mangas = result.data.map { it.toSManga() }
|
||||||
|
return MangasPage(mangas, result.hasNextPage())
|
||||||
override fun chapterListSelector() = "ul#list-chapters li > a"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
|
||||||
name = element.selectFirst("div.grid > span")!!.text()
|
|
||||||
date_upload = element.selectFirst("div.grid > div")?.text()?.let { parseChapterDate(it) } ?: 0
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseGenres(document: Document): List<Genre> {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
return document.select("div:has(> input[type=checkbox])")
|
val genresParam = filters
|
||||||
.orEmpty()
|
.filterIsInstance<GenreFilter>()
|
||||||
.map { li ->
|
.flatMap { filter -> filter.state.filter { it.state }.map { it.key } }
|
||||||
Genre(
|
.takeIf { it.isNotEmpty() }
|
||||||
li.selectFirst("label")!!.text(),
|
?.joinToString(",")
|
||||||
li.selectFirst("input[type=checkbox]")!!.`val`(),
|
|
||||||
|
val searchKey = SearchKey(page - 1, query, genresParam)
|
||||||
|
|
||||||
|
val cursor = searchMangaCursor[searchKey] ?: ""
|
||||||
|
val url = "$baseUrl/serie-query".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("cursor", cursor)
|
||||||
|
.addQueryParameter("perPage", "35")
|
||||||
|
.addQueryParameter("type", "query")
|
||||||
|
.addQueryParameter("name", query)
|
||||||
|
.fragment(page.toString())
|
||||||
|
|
||||||
|
if (genresParam != null) {
|
||||||
|
url.addQueryParameter("genres", genresParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GET(url.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val page = response.request.url.fragment!!.toInt()
|
||||||
|
val query = response.request.url.queryParameter("name") ?: ""
|
||||||
|
val genresParam = response.request.url.queryParameter("genres")
|
||||||
|
|
||||||
|
val searchKey = SearchKey(page, query, genresParam)
|
||||||
|
|
||||||
|
val result = response.parseAs<SeriesQueryDto>()
|
||||||
|
searchMangaCursor[searchKey] = result.nextCursor
|
||||||
|
val mangas = result.data.map { it.toSManga() }
|
||||||
|
return MangasPage(mangas, result.hasNextPage())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
return FilterList(
|
||||||
|
GenreFilter("Géneros", getGenreList()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl/series/${manga.url}", headers)
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = document.selectFirst("div.grid > h1")!!.text()
|
||||||
|
thumbnail_url = document.selectFirst("div.bg_main.bg-cover")?.imageFromStyle()
|
||||||
|
description = document.select("div.grid > div.container > p").text()
|
||||||
|
status = document.selectFirst("div.grid:has(>div.flex:has(>span:contains(Status))) > div:last-child").parseStatus()
|
||||||
|
genre = document.select("a[href*=/series?genres] > span").joinToString { it.text() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageFromElement(element: Element): String? {
|
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||||
return when {
|
|
||||||
element.hasAttr("data-src") -> element.attr("abs:data-src")
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src")
|
val document = response.asJsoup()
|
||||||
element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage()
|
return document.select("div.grid > a.group").map { element ->
|
||||||
element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc")
|
SChapter.create().apply {
|
||||||
element.hasAttr("style") -> element.attr("style").substringAfter("url(").substringBefore(")")
|
name = element.selectFirst("span.truncate")!!.text()
|
||||||
else -> element.attr("abs:src")
|
url = element.selectFirst("a")!!.attr("href")
|
||||||
|
date_upload = element.selectFirst("span.w-fit")?.text()?.let { parseChapterDate(it) } ?: 0L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return document.select("div.grid > img").mapIndexed { i, element ->
|
||||||
|
Page(i, imageUrl = element.attr("abs:src"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private val cursorDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLatestCursor(): String {
|
||||||
|
val now: String? = cursorDateFormat.format(Date())
|
||||||
|
val json = """{"last_update_at":"$now","id":0,"_pointsToNextItems":true}"""
|
||||||
|
return Base64.encodeToString(json.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element?.parseStatus(): Int {
|
||||||
|
return when (this?.text()?.lowercase()) {
|
||||||
|
"on-going" -> SManga.ONGOING
|
||||||
|
"end" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.imageFromStyle(): String? {
|
||||||
|
val style = this.attr("style").replace(""", "\"")
|
||||||
|
return style.substringAfterLast("url(").substringBefore(")").removeSurrounding("\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chapterDateFormat = SimpleDateFormat("dd/MM/yyyy", Locale("es"))
|
||||||
|
|
||||||
|
private fun parseChapterDate(date: String): Long {
|
||||||
|
if (date.contains("hace")) {
|
||||||
|
val cleanDate = date.substringAfter("hace").trim()
|
||||||
|
when {
|
||||||
|
"hora" in cleanDate -> {
|
||||||
|
val hours = cleanDate.substringBefore("hora").trim().toIntOrNull() ?: return 0L
|
||||||
|
return System.currentTimeMillis() - hours * 60 * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
"minuto" in cleanDate -> {
|
||||||
|
val minutes = cleanDate.substringBefore("minuto").trim().toIntOrNull() ?: return 0L
|
||||||
|
return System.currentTimeMillis() - minutes * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
"segundo" in cleanDate -> {
|
||||||
|
val seconds = cleanDate.substringBefore("segundo").trim().toIntOrNull() ?: return 0L
|
||||||
|
return System.currentTimeMillis() - seconds * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
"día" in cleanDate -> {
|
||||||
|
val days = cleanDate.substringBefore("día").trim().toIntOrNull() ?: return 0L
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.add(Calendar.DAY_OF_YEAR, -days)
|
||||||
|
return calendar.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
return 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date.equals("ayer", true)) {
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.add(Calendar.DAY_OF_YEAR, -1)
|
||||||
|
return calendar.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapterDateFormat.tryParse(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGenreList() = listOf(
|
||||||
|
Genre("BDSM", "bdsm"),
|
||||||
|
Genre("Bebes", "bebes"),
|
||||||
|
Genre("Bestias", "bestias"),
|
||||||
|
Genre("BL Sin Censura", "bl-sin-censura"),
|
||||||
|
Genre("Boys Love", "boys-love"),
|
||||||
|
Genre("Che Tenete Un Poco De Amor Propio", "che-tenete-un-poco-de-amor-propio"),
|
||||||
|
Genre("Ciencia Ficción", "ciencia-ficcion"),
|
||||||
|
Genre("Comedia", "comedia"),
|
||||||
|
Genre("Crimen", "crimen"),
|
||||||
|
Genre("Del Campo", "del-campo"),
|
||||||
|
Genre("Demonios", "demonios"),
|
||||||
|
Genre("Deportes", "deportes"),
|
||||||
|
Genre("Drama", "drama"),
|
||||||
|
Genre("Escolar", "escolar"),
|
||||||
|
Genre("Espacial", "espacial"),
|
||||||
|
Genre("Fantasía", "fantasia"),
|
||||||
|
Genre("Furro", "furro"),
|
||||||
|
Genre("Harem", "harem"),
|
||||||
|
Genre("Harem Inverso", "harem-inverso"),
|
||||||
|
Genre("Historia", "historia"),
|
||||||
|
Genre("Josei", "josei"),
|
||||||
|
Genre("Juego", "juego"),
|
||||||
|
Genre("Mafia", "mafia"),
|
||||||
|
Genre("Magia", "magia"),
|
||||||
|
Genre("Manhwa +19", "manhwa-19"),
|
||||||
|
Genre("Militar", "militar"),
|
||||||
|
Genre("Moderno", "moderno"),
|
||||||
|
Genre("Morocho Hermoso", "morocho-hermoso"),
|
||||||
|
Genre("Mucho Gogogo", "mucho-gogogo"),
|
||||||
|
Genre("Música", "musica"),
|
||||||
|
Genre("Novela", "novela"),
|
||||||
|
Genre("Odio-Amor", "odio-amor"),
|
||||||
|
Genre("Omegaverse", "omegaverse"),
|
||||||
|
Genre("Psicológico", "psicologico"),
|
||||||
|
Genre("Reencarnación", "reencarnacion"),
|
||||||
|
Genre("Relación Por Convivencia", "relacion-por-convivencia"),
|
||||||
|
Genre("Romance", "romance"),
|
||||||
|
Genre("Smut", "smut"),
|
||||||
|
Genre("Telenovela", "telenovela"),
|
||||||
|
Genre("Tetón", "teton"),
|
||||||
|
Genre("Toxicidad", "toxicidad"),
|
||||||
|
Genre("Toxicidad Nivel Chernóbil", "toxicidad-nivel-chernobil"),
|
||||||
|
Genre("Universitario", "universitario"),
|
||||||
|
Genre("Venganza", "venganza"),
|
||||||
|
Genre("Shoujo", "Shoujo"),
|
||||||
|
Genre("Shounen", "Shounen"),
|
||||||
|
Genre("Seinen", "Seinen"),
|
||||||
|
Genre("+18 Sin Censura", "+ 18 Sin Censura"),
|
||||||
|
Genre("NoBL\uD83D\uDC8C", "nobl"),
|
||||||
|
Genre("Girls Love", "gl"),
|
||||||
|
Genre("Adulto", "adulto"),
|
||||||
|
Genre("+18", "18"),
|
||||||
|
Genre("Sistema", "sistema"),
|
||||||
|
Genre("PuchiLovers", "puchilovers"),
|
||||||
|
Genre("Goheart Scan", "goheart-scan"),
|
||||||
|
Genre("Acción", "Acción"),
|
||||||
|
Genre("Aventura", "Aventura"),
|
||||||
|
Genre("Sobrenatural", "Sobrenatural"),
|
||||||
|
Genre("Transmigración", "Transmigración"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.es.lectorjpg
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SeriesQueryDto(
|
||||||
|
val data: List<SeriesDto> = emptyList(),
|
||||||
|
@SerialName("next_cursor") val nextCursor: String? = null,
|
||||||
|
) {
|
||||||
|
fun hasNextPage() = nextCursor != null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SeriesDto(
|
||||||
|
private val name: String,
|
||||||
|
private val slug: String,
|
||||||
|
@SerialName("cover_url") private val cover: String,
|
||||||
|
) {
|
||||||
|
fun toSManga() = SManga.create().apply {
|
||||||
|
title = name
|
||||||
|
url = slug
|
||||||
|
thumbnail_url = cover
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.es.lectorjpg
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
class Genre(title: String, val key: String) : Filter.CheckBox(title)
|
||||||
|
class GenreFilter(title: String, genres: List<Genre>) : Filter.Group<Genre>(title, genres)
|