Add LadronCorps (#3367)

* Add LadronCorps

* Cleanup

* Rename source

* Add rateLimit

* Add support for deep linking

* Fix searchManga

* Add icons

* Cleanup

* Add update_strategy

* Add chapter date

* Add status

* Fix package name

* Cleanup

* Rename function

* Cleanup

* Fix days parser

* Change to kotlinx serialization

* Resolve url paths in DTO

* Remove JSONObject

* Throws error field missing
This commit is contained in:
Chopper 2024-06-04 02:17:56 -03:00 committed by Draff
parent 270e70125c
commit 61b0ab972d
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
10 changed files with 355 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".es.ladroncorps.LadronCorpsUrlActivity"
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="www.ladroncorps.com"
android:pathPattern="/post/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,7 @@
ext {
extName = 'Ladron Corps'
extClass = '.LadronCorps'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,189 @@
package eu.kanade.tachiyomi.extension.es.ladroncorps
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.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class LadronCorps : HttpSource() {
override val name: String = "Ladron Corps"
override val baseUrl: String = "https://www.ladroncorps.com"
override val lang: String = "es"
override val supportsLatest: Boolean = false
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(3)
.build()
private val json by injectLazy<Json>()
private val authorization: String by lazy {
val response = client.newCall(GET("$baseUrl/_api/v2/dynamicmodel", headers)).execute()
val authDto = response.parseAs<AuthDto>()
authDto.randomToken()
}
private val apiHeaders: Headers by lazy {
headers.newBuilder()
.set("Authorization", authorization)
.build()
}
override fun latestUpdatesRequest(page: Int): Request =
throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response): MangasPage =
throw UnsupportedOperationException()
override fun popularMangaParse(response: Response): MangasPage {
val posts = response.parseAs<PopularMangaContainerDto>().posts
val mangas = posts.map {
SManga.create().apply {
title = it.title
thumbnail_url = "${it.cover.url}"
url = "${it.url}"
}
}
return MangasPage(mangas, mangas.isNotEmpty())
}
override fun popularMangaRequest(page: Int): Request {
val url = "$baseUrl/blog-frontend-adapter-public/v2/post-feed-page".toHttpUrl().newBuilder()
.addQueryParameter("includeContent", "false")
.addQueryParameter("languageCode", lang)
.addQueryParameter("page", "$page")
.addQueryParameter("pageSize", "20")
.addQueryParameter("type", "ALL_POSTS")
.build()
return GET(url, apiHeaders)
}
override fun searchMangaParse(response: Response): MangasPage {
val posts = response.parseAs<SearchDto>().posts
val mangas = posts.map {
SManga.create().apply {
title = it.title
thumbnail_url = it.cover.url
url = it.url
}
}
return MangasPage(mangas, false)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/_api/communities-blog-node-api/_api/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.build()
return GET(url, apiHeaders)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) {
val manga = SManga.create().apply {
url = "/post/${query.substringAfter(URL_SEARCH_PREFIX)}"
}
return fetchMangaDetails(manga).asObservable().map {
MangasPage(listOf(it), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup()
title = document.selectFirst("h1")!!.text()
description = document.select("div[data-hook='post-description'] p > span")
.joinToString("\n".repeat(2)) { it.text() }
genre = document.select("#post-footer li a")
.joinToString { it.text() }
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
setUrlWithoutDomain(document.location())
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return listOf(
SChapter.create().apply {
name = "Capitulo único"
date_upload = parseDate(document.selectFirst("span[data-hook='time-ago']")?.text() ?: "")
setUrlWithoutDomain(document.location())
},
)
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val selectors = "figure[data-hook='imageViewer'] img, img[data-hook='gallery-item-image-img']"
return document.select(selectors).mapIndexed { index, element ->
Page(index, document.location(), imageUrl = element.imgAttr())
}
}
override fun imageUrlParse(response: Response): String =
throw UnsupportedOperationException()
private fun Element.imgAttr(): String = when {
hasAttr("data-pin-media") -> absUrl("data-pin-media")
else -> absUrl("src")
}
private fun parseDate(date: String): Long =
try { dateFormat.parse(dateSanitize(date))!!.time } catch (_: Exception) { parseRelativeDate(date) }
private fun parseRelativeDate(date: String): Long {
val number = RELATIVE_DATE_REGEX.find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
date.contains("día", ignoreCase = true) -> cal.apply { add(Calendar.DATE, -number) }.timeInMillis
date.contains("mes", ignoreCase = true) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
date.contains("año", ignoreCase = true) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0
}
}
private fun dateSanitize(date: String): String =
if (D_MMM_REGEX.matches(date)) "$date ${Calendar.getInstance().get(Calendar.YEAR)}" else date
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromString(body.string())
}
companion object {
const val URL_SEARCH_PREFIX = "slug:"
val RELATIVE_DATE_REGEX = """(\d+)""".toRegex()
val D_MMM_REGEX = """\d+ \w+$""".toRegex()
val dateFormat = SimpleDateFormat("d MMM yyyy", Locale("es"))
}
}

View File

@ -0,0 +1,100 @@
package eu.kanade.tachiyomi.extension.es.ladroncorps
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class AuthDto(
@SerialName("apps")
val tokens: Map<String, TokenDto>,
) {
fun randomToken(): String {
return tokens.values.random().value
}
@Serializable
class TokenDto(
@SerialName("instance")
val value: String,
)
}
@Serializable
class PopularMangaContainerDto(val postFeedPage: Post) {
val posts: List<PopularMangaDto> get() = postFeedPage.posts.posts
@Serializable
class Post(val posts: Posts)
@Serializable
class Posts(val posts: List<PopularMangaDto>)
}
@Serializable
class PopularMangaDto(
var title: String,
@SerialName("coverMedia")
val cover: CoverDto,
val url: UrlDto,
) {
@Serializable
class CoverDto(
@SerialName("image")
val url: UrlDto,
)
/*
* There are two fields available to get the url; when the url field is missing,
* the path field contains the url path
* */
@Serializable
class UrlDto(
private val url: String?,
private val path: String?,
) {
override fun toString(): String {
return url ?: path!!
}
}
}
@Serializable
class SearchDto(
val posts: List<SearchMangaDto>,
)
@Serializable
class SearchMangaDto(
var title: String,
@SerialName("coverImage")
val cover: CoverDto,
private val slugs: List<String>,
) {
val slug: String get() = slugs.first()
val url: String get() = "/post/$slug"
@Serializable
class CoverDto(
private val src: SrcDto,
) {
val url: String get() = "$STATIC_MEDIA_URL/$src"
}
/*
* There are two fields available to get src data; when id is missing,
* file_name contains the src path
* */
@Serializable
class SrcDto(
private val id: String?,
private val file_name: String?,
) {
override fun toString(): String {
return id ?: file_name!!
}
}
companion object {
const val STATIC_MEDIA_URL = "https://static.wixstatic.com/media"
}
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.extension.es.ladroncorps
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 LadronCorpsUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${LadronCorps.URL_SEARCH_PREFIX}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}