Add new HeanCms as multisrc theme (#13713)
* Add new HeanCms as new multisrc theme. * Add missing runner and fix wrong version of RS.
|
@ -0,0 +1,11 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="HeanCmsGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||||
|
<module name="tachiyomi-extensions.multisrc.main" />
|
||||||
|
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.heancms.HeanCmsGenerator" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=heancms" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=heancms" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 7.2 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 60 KiB |
|
@ -0,0 +1,48 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.pt.reaperscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.heancms.Genre
|
||||||
|
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
|
class ReaperScans : HeanCms(
|
||||||
|
"Reaper Scans",
|
||||||
|
"https://reaperscans.com.br",
|
||||||
|
"pt-BR"
|
||||||
|
) {
|
||||||
|
|
||||||
|
override val client: OkHttpClient = super.client.newBuilder()
|
||||||
|
.rateLimitHost(apiUrl.toHttpUrl(), 1, 2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Site changed from Madara to HeanCms.
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
|
override fun getGenreList(): List<Genre> = listOf(
|
||||||
|
Genre("Artes Marciais", 2),
|
||||||
|
Genre("Aventura", 10),
|
||||||
|
Genre("Ação", 9),
|
||||||
|
Genre("Comédia", 14),
|
||||||
|
Genre("Drama", 15),
|
||||||
|
Genre("Escolar", 7),
|
||||||
|
Genre("Fantasia", 11),
|
||||||
|
Genre("Ficção científica", 16),
|
||||||
|
Genre("Guerra", 17),
|
||||||
|
Genre("Isekai", 18),
|
||||||
|
Genre("Jogo", 12),
|
||||||
|
Genre("Mangá", 24),
|
||||||
|
Genre("Manhua", 23),
|
||||||
|
Genre("Manhwa", 22),
|
||||||
|
Genre("Mecha", 19),
|
||||||
|
Genre("Mistério", 20),
|
||||||
|
Genre("Nacional", 8),
|
||||||
|
Genre("Realidade Virtual", 21),
|
||||||
|
Genre("Retorno", 3),
|
||||||
|
Genre("Romance", 5),
|
||||||
|
Genre("Segunda vida", 4),
|
||||||
|
Genre("Seinen", 1),
|
||||||
|
Genre("Shounen", 13),
|
||||||
|
Genre("Terror", 6)
|
||||||
|
)
|
||||||
|
}
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 39 KiB |
|
@ -0,0 +1,61 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.es.yugenmangas
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.heancms.Genre
|
||||||
|
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms
|
||||||
|
|
||||||
|
class YugenMangas : HeanCms("YugenMangas", "https://yugenmangas.com", "es") {
|
||||||
|
|
||||||
|
// Site changed from Madara to HeanCms.
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
|
override fun getGenreList(): List<Genre> = listOf(
|
||||||
|
Genre("+18", 1),
|
||||||
|
Genre("Acción", 36),
|
||||||
|
Genre("Adulto", 38),
|
||||||
|
Genre("Apocalíptico", 3),
|
||||||
|
Genre("Artes marciales (1)", 16),
|
||||||
|
Genre("Artes marciales (2)", 37),
|
||||||
|
Genre("Aventura", 2),
|
||||||
|
Genre("Boys Love", 4),
|
||||||
|
Genre("Ciencia ficción", 39),
|
||||||
|
Genre("Comedia", 5),
|
||||||
|
Genre("Demonios", 6),
|
||||||
|
Genre("Deporte", 26),
|
||||||
|
Genre("Drama", 7),
|
||||||
|
Genre("Ecchi", 8),
|
||||||
|
Genre("Familia", 9),
|
||||||
|
Genre("Fantasía", 10),
|
||||||
|
Genre("Girls Love", 11),
|
||||||
|
Genre("Gore", 12),
|
||||||
|
Genre("Harem", 13),
|
||||||
|
Genre("Harem inverso", 14),
|
||||||
|
Genre("Histórico", 48),
|
||||||
|
Genre("Horror", 41),
|
||||||
|
Genre("Isekai", 40),
|
||||||
|
Genre("Josei", 15),
|
||||||
|
Genre("Maduro", 42),
|
||||||
|
Genre("Magia", 17),
|
||||||
|
Genre("MangoScan", 35),
|
||||||
|
Genre("Mecha", 18),
|
||||||
|
Genre("Militar", 19),
|
||||||
|
Genre("Misterio", 20),
|
||||||
|
Genre("Psicológico", 21),
|
||||||
|
Genre("Realidad virtual", 46),
|
||||||
|
Genre("Recuentos de la vida", 25),
|
||||||
|
Genre("Reencarnación", 22),
|
||||||
|
Genre("Regresion", 23),
|
||||||
|
Genre("Romance", 24),
|
||||||
|
Genre("Seinen", 27),
|
||||||
|
Genre("Shonen", 28),
|
||||||
|
Genre("Shoujo", 29),
|
||||||
|
Genre("Sistema", 45),
|
||||||
|
Genre("Smut", 30),
|
||||||
|
Genre("Supernatural", 31),
|
||||||
|
Genre("Supervivencia", 32),
|
||||||
|
Genre("Tragedia", 33),
|
||||||
|
Genre("Transmigración", 34),
|
||||||
|
Genre("Vida Escolar", 47),
|
||||||
|
Genre("Yaoi", 43),
|
||||||
|
Genre("Yuri", 44)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.yugenmangas
|
package eu.kanade.tachiyomi.extension.pt.yugenmangas
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import kotlinx.serialization.decodeFromString
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -15,34 +14,8 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class YugenMangasFactory : SourceFactory {
|
class YugenMangas : Madara(
|
||||||
override fun createSources() = listOf(
|
"YugenMangas",
|
||||||
YugenMangasEs(),
|
|
||||||
YugenMangasBr()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class YugenMangas(
|
|
||||||
override val baseUrl: String,
|
|
||||||
lang: String,
|
|
||||||
dateFormat: SimpleDateFormat = SimpleDateFormat("MMMMM dd, yyyy", Locale.US)
|
|
||||||
) : Madara("YugenMangas", baseUrl, lang, dateFormat) {
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
||||||
name = element.selectFirst("p.chapter-manhwa-title")!!.text()
|
|
||||||
date_upload = parseChapterDate(element.selectFirst("span.chapter-release-date i")?.text())
|
|
||||||
|
|
||||||
val chapterUrl = element.selectFirst("a")!!.attr("abs:href")
|
|
||||||
setUrlWithoutDomain(
|
|
||||||
chapterUrl.substringBefore("?style=paged") +
|
|
||||||
if (!chapterUrl.endsWith(chapterUrlSuffix)) chapterUrlSuffix else ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class YugenMangasEs : YugenMangas("https://yugenmangas.com", "es")
|
|
||||||
|
|
||||||
class YugenMangasBr : YugenMangas(
|
|
||||||
"https://yugenmangas.com.br",
|
"https://yugenmangas.com.br",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
SimpleDateFormat("MMMMM dd, yyyy", Locale("pt", "BR"))
|
SimpleDateFormat("MMMMM dd, yyyy", Locale("pt", "BR"))
|
||||||
|
@ -64,19 +37,26 @@ class YugenMangasBr : YugenMangas(
|
||||||
|
|
||||||
override val useNewChapterEndpoint: Boolean = true
|
override val useNewChapterEndpoint: Boolean = true
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
|
name = element.selectFirst("p.chapter-manhwa-title")!!.text()
|
||||||
|
date_upload = parseChapterDate(element.selectFirst("span.chapter-release-date i")?.text())
|
||||||
|
|
||||||
|
val chapterUrl = element.selectFirst("a")!!.attr("abs:href")
|
||||||
|
setUrlWithoutDomain(
|
||||||
|
chapterUrl.substringBefore("?style=paged") +
|
||||||
|
if (!chapterUrl.endsWith(chapterUrlSuffix)) chapterUrlSuffix else ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private var userAgent: String? = null
|
private var userAgent: String? = null
|
||||||
private var checkedUa = false
|
private var checkedUa = false
|
||||||
|
|
||||||
private fun uaIntercept(chain: Interceptor.Chain): Response {
|
private fun uaIntercept(chain: Interceptor.Chain): Response {
|
||||||
if (userAgent == null && !checkedUa) {
|
if (userAgent == null && !checkedUa) {
|
||||||
val browser = BROWSERS.random()
|
val uaResponse = chain.proceed(GET(UA_DB_URL))
|
||||||
val uaResponse = chain.proceed(GET("$UA_DB_URL/$browser"))
|
|
||||||
|
|
||||||
if (uaResponse.isSuccessful) {
|
if (uaResponse.isSuccessful) {
|
||||||
userAgent = uaResponse.asJsoup()
|
userAgent = json.decodeFromString<List<String>>(uaResponse.body!!.string()).random()
|
||||||
.select(".listing-of-useragents span.code")
|
|
||||||
.firstOrNull()
|
|
||||||
?.text()
|
|
||||||
checkedUa = true
|
checkedUa = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +75,6 @@ class YugenMangasBr : YugenMangas(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val BROWSERS = arrayOf("chrome", "firefox", "edge", "opera", "vivaldi")
|
private const val UA_DB_URL = "https://tachiyomiorg.github.io/user-agents/user-agents.json"
|
||||||
private const val UA_DB_URL = "https://whatismybrowser.com/guides/the-latest-user-agent"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,289 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.heancms
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
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 kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class HeanCms(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
override val lang: String,
|
||||||
|
protected val apiUrl: String = baseUrl.replace("://", "://api.")
|
||||||
|
) : HttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
|
protected val json: Json by injectLazy()
|
||||||
|
|
||||||
|
protected val intl by lazy { HeanCmsIntl(lang) }
|
||||||
|
|
||||||
|
private var seriesSlugMap: Map<String, String>? = null
|
||||||
|
|
||||||
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
|
.add("Origin", baseUrl)
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val payloadObj = HeanCmsSearchDto(
|
||||||
|
order = "desc",
|
||||||
|
orderBy = "total_views",
|
||||||
|
status = "Ongoing",
|
||||||
|
type = "Comic"
|
||||||
|
)
|
||||||
|
|
||||||
|
val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val apiHeaders = headersBuilder()
|
||||||
|
.add("Accept", ACCEPT_JSON)
|
||||||
|
.add("Content-Type", payload.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("$apiUrl/series/querysearch", apiHeaders, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val mangaList = response.parseAs<List<HeanCmsSeriesDto>>()
|
||||||
|
.map { it.toSManga(apiUrl) }
|
||||||
|
|
||||||
|
fetchAllTitles()
|
||||||
|
|
||||||
|
return MangasPage(mangaList, hasNextPage = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val payloadObj = HeanCmsSearchDto(
|
||||||
|
order = "desc",
|
||||||
|
orderBy = "latest",
|
||||||
|
status = "Ongoing",
|
||||||
|
type = "Comic"
|
||||||
|
)
|
||||||
|
|
||||||
|
val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val apiHeaders = headersBuilder()
|
||||||
|
.add("Accept", ACCEPT_JSON)
|
||||||
|
.add("Content-Type", payload.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("$apiUrl/series/querysearch", apiHeaders, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val sortByFilter = filters.firstInstanceOrNull<SortByFilter>()
|
||||||
|
|
||||||
|
val payloadObj = HeanCmsSearchDto(
|
||||||
|
order = if (sortByFilter?.state?.ascending == true) "asc" else "desc",
|
||||||
|
orderBy = sortByFilter?.selected ?: "total_views",
|
||||||
|
status = filters.firstInstanceOrNull<StatusFilter>()?.selected?.value ?: "Ongoing",
|
||||||
|
type = "Comic",
|
||||||
|
tagIds = filters.firstInstanceOrNull<GenreFilter>()?.state
|
||||||
|
?.filter(Genre::state)
|
||||||
|
?.map(Genre::id)
|
||||||
|
.orEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
|
val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val apiHeaders = headersBuilder()
|
||||||
|
.add("Accept", ACCEPT_JSON)
|
||||||
|
.add("Content-Type", payload.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val apiUrl = "$apiUrl/series/querysearch".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("q", query)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
return POST(apiUrl, apiHeaders, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val query = response.request.url.queryParameter("q").orEmpty()
|
||||||
|
|
||||||
|
var mangaList = response.parseAs<List<HeanCmsSeriesDto>>()
|
||||||
|
.map { it.toSManga(apiUrl) }
|
||||||
|
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
mangaList = mangaList.filter { it.title.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAllTitles()
|
||||||
|
|
||||||
|
return MangasPage(mangaList, hasNextPage = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround to allow "Open in browser" use the real URL.
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
return client.newCall(seriesDetailsRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mangaDetailsParse(response).apply { initialized = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun seriesDetailsRequest(manga: SManga): Request {
|
||||||
|
val seriesSlug = manga.url
|
||||||
|
.substringAfterLast("/")
|
||||||
|
.replace(TIMESTAMP_REGEX, "")
|
||||||
|
|
||||||
|
val currentSlug = seriesSlugMap?.get(seriesSlug) ?: seriesSlug
|
||||||
|
|
||||||
|
val apiHeaders = headersBuilder()
|
||||||
|
.add("Accept", ACCEPT_JSON)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$apiUrl/series/$currentSlug#${manga.status}", apiHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val result = runCatching { response.parseAs<HeanCmsSeriesDto>() }
|
||||||
|
val seriesDetails = result.getOrNull()?.toSManga(apiUrl)
|
||||||
|
?: throw Exception(intl.urlChangedError(name))
|
||||||
|
|
||||||
|
return seriesDetails.apply {
|
||||||
|
status = response.request.url.fragment?.toIntOrNull() ?: SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request = seriesDetailsRequest(manga)
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val result = response.parseAs<HeanCmsSeriesDto>()
|
||||||
|
val seriesSlug = response.request.url.pathSegments.last()
|
||||||
|
|
||||||
|
return result.chapters.orEmpty()
|
||||||
|
.map { it.toSChapter(seriesSlug) }
|
||||||
|
.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val chapterId = chapter.url.substringAfterLast("#")
|
||||||
|
|
||||||
|
val apiHeaders = headersBuilder()
|
||||||
|
.add("Accept", ACCEPT_JSON)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$apiUrl/series/chapter/$chapterId", apiHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
return response.parseAs<HeanCmsReaderDto>().content?.images.orEmpty()
|
||||||
|
.mapIndexed { i, url -> Page(i, "", "$apiUrl/$url") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String = ""
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page): Request {
|
||||||
|
val imageHeaders = headersBuilder()
|
||||||
|
.add("Accept", ACCEPT_IMAGE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(page.imageUrl!!, imageHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun getStatusList(): List<Status> = listOf(
|
||||||
|
Status(intl.statusOngoing, "Ongoing"),
|
||||||
|
Status(intl.statusOnHiatus, "Hiatus"),
|
||||||
|
Status(intl.statusDropped, "Dropped"),
|
||||||
|
)
|
||||||
|
|
||||||
|
protected open fun getSortProperties(): List<SortProperty> = listOf(
|
||||||
|
SortProperty(intl.sortByTitle, "title"),
|
||||||
|
SortProperty(intl.sortByViews, "total_views"),
|
||||||
|
SortProperty(intl.sortByLatest, "latest"),
|
||||||
|
SortProperty(intl.sortByRecentlyAdded, "recently_added"),
|
||||||
|
)
|
||||||
|
|
||||||
|
protected open fun getGenreList(): List<Genre> = emptyList()
|
||||||
|
|
||||||
|
protected open fun fetchAllTitles() {
|
||||||
|
if (!seriesSlugMap.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = runCatching {
|
||||||
|
client.newCall(allTitlesRequest()).execute()
|
||||||
|
.let { parseAllTitles(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesSlugMap = result.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun allTitlesRequest(): Request {
|
||||||
|
val payloadObj = HeanCmsSearchDto(
|
||||||
|
order = "desc",
|
||||||
|
orderBy = "total_views",
|
||||||
|
status = "",
|
||||||
|
type = "Comic"
|
||||||
|
)
|
||||||
|
|
||||||
|
val payload = json.encodeToString(payloadObj).toRequestBody(JSON_MEDIA_TYPE)
|
||||||
|
|
||||||
|
val apiHeaders = headersBuilder()
|
||||||
|
.add("Accept", ACCEPT_JSON)
|
||||||
|
.add("Content-Type", payload.contentType().toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("$apiUrl/series/querysearch", apiHeaders, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun parseAllTitles(response: Response): Map<String, String> {
|
||||||
|
return response.parseAs<List<HeanCmsSeriesDto>>()
|
||||||
|
.filter { it.type == "Comic" }
|
||||||
|
.associateBy(
|
||||||
|
keySelector = { it.slug.replace(TIMESTAMP_REGEX, "") },
|
||||||
|
valueTransform = HeanCmsSeriesDto::slug
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
val genres = getGenreList()
|
||||||
|
|
||||||
|
val filters = listOfNotNull(
|
||||||
|
StatusFilter(intl.statusFilterTitle, getStatusList()),
|
||||||
|
SortByFilter(intl.sortByFilterTitle, getSortProperties()),
|
||||||
|
GenreFilter(intl.genreFilterTitle, genres).takeIf { genres.isNotEmpty() }
|
||||||
|
)
|
||||||
|
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T = use {
|
||||||
|
json.decodeFromString(it.body?.string().orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? =
|
||||||
|
filterIsInstance<R>().firstOrNull()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
|
||||||
|
private const val ACCEPT_JSON = "application/json, text/plain, */*"
|
||||||
|
|
||||||
|
private val JSON_MEDIA_TYPE = "application/json".toMediaType()
|
||||||
|
|
||||||
|
val TIMESTAMP_REGEX = "-\\d+$".toRegex()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.heancms
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeanCmsSeriesDto(
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("series_slug") val slug: String,
|
||||||
|
@SerialName("series_type") val type: String = "Comic",
|
||||||
|
val author: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val studio: String? = null,
|
||||||
|
val status: String? = null,
|
||||||
|
val thumbnail: String,
|
||||||
|
val title: String,
|
||||||
|
val tags: List<HeanCmsTagDto>? = emptyList(),
|
||||||
|
val chapters: List<HeanCmsChapterDto>? = emptyList()
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toSManga(apiUrl: String): SManga = SManga.create().apply {
|
||||||
|
val descriptionBody = this@HeanCmsSeriesDto.description?.let(Jsoup::parseBodyFragment)
|
||||||
|
|
||||||
|
title = this@HeanCmsSeriesDto.title
|
||||||
|
author = this@HeanCmsSeriesDto.author?.trim()
|
||||||
|
artist = this@HeanCmsSeriesDto.studio?.trim()
|
||||||
|
description = descriptionBody?.select("p")
|
||||||
|
?.joinToString("\n\n") { it.text() }
|
||||||
|
?.ifEmpty { descriptionBody.text().replace("\n", "\n\n") }
|
||||||
|
genre = tags.orEmpty()
|
||||||
|
.sortedBy(HeanCmsTagDto::name)
|
||||||
|
.joinToString { it.name }
|
||||||
|
thumbnail_url = "$apiUrl/cover/$thumbnail"
|
||||||
|
status = when (this@HeanCmsSeriesDto.status) {
|
||||||
|
"Ongoing" -> SManga.ONGOING
|
||||||
|
"Hiatus" -> SManga.ON_HIATUS
|
||||||
|
"Dropped" -> SManga.CANCELLED
|
||||||
|
"Completed", "Finished" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
url = "/series/${slug.replace(HeanCms.TIMESTAMP_REGEX, "")}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeanCmsTagDto(val name: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeanCmsChapterDto(
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("chapter_name") val name: String,
|
||||||
|
@SerialName("chapter_slug") val slug: String,
|
||||||
|
val index: String,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toSChapter(seriesSlug: String): SChapter = SChapter.create().apply {
|
||||||
|
name = this@HeanCmsChapterDto.name.trim()
|
||||||
|
date_upload = runCatching { DATE_FORMAT.parse(createdAt.substringBefore("."))?.time }
|
||||||
|
.getOrNull() ?: 0L
|
||||||
|
url = "/series/$seriesSlug/$slug#$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATE_FORMAT by lazy {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeanCmsReaderDto(
|
||||||
|
val content: HeanCmsReaderContentDto? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeanCmsReaderContentDto(
|
||||||
|
val images: List<String>? = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeanCmsSearchDto(
|
||||||
|
val order: String,
|
||||||
|
@SerialName("order_by") val orderBy: String,
|
||||||
|
@SerialName("series_status") val status: String,
|
||||||
|
@SerialName("series_type") val type: String,
|
||||||
|
@SerialName("tags_ids") val tagIds: List<Int> = emptyList()
|
||||||
|
)
|
|
@ -0,0 +1,32 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.heancms
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
class Genre(title: String, val id: Int) : Filter.CheckBox(title)
|
||||||
|
|
||||||
|
class GenreFilter(title: String, genres: List<Genre>) : Filter.Group<Genre>(title, genres)
|
||||||
|
|
||||||
|
open class EnhancedSelect<T>(name: String, values: Array<T>) : Filter.Select<T>(name, values) {
|
||||||
|
val selected: T
|
||||||
|
get() = values[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Status(val name: String, val value: String) {
|
||||||
|
override fun toString(): String = name
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusFilter(title: String, statuses: List<Status>) :
|
||||||
|
EnhancedSelect<Status>(title, statuses.toTypedArray())
|
||||||
|
|
||||||
|
data class SortProperty(val name: String, val value: String) {
|
||||||
|
override fun toString(): String = name
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortByFilter(title: String, private val sortProperties: List<SortProperty>) : Filter.Sort(
|
||||||
|
title,
|
||||||
|
sortProperties.map { it.name }.toTypedArray(),
|
||||||
|
Selection(1, ascending = false)
|
||||||
|
) {
|
||||||
|
val selected: String
|
||||||
|
get() = sortProperties[state!!.index].value
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.heancms
|
||||||
|
|
||||||
|
import generator.ThemeSourceData.SingleLang
|
||||||
|
import generator.ThemeSourceGenerator
|
||||||
|
|
||||||
|
class HeanCmsGenerator : ThemeSourceGenerator {
|
||||||
|
|
||||||
|
override val themePkg = "heancms"
|
||||||
|
|
||||||
|
override val themeClass = "HeanCms"
|
||||||
|
|
||||||
|
override val baseVersionCode: Int = 1
|
||||||
|
|
||||||
|
override val sources = listOf(
|
||||||
|
SingleLang("Reaper Scans", "https://reaperscans.com.br", "pt-BR", overrideVersionCode = 33),
|
||||||
|
SingleLang("YugenMangas", "https://yugenmangas.com", "es", isNsfw = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
HeanCmsGenerator().createAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.heancms
|
||||||
|
|
||||||
|
class HeanCmsIntl(lang: String) {
|
||||||
|
|
||||||
|
val availableLang: String = if (lang in AVAILABLE_LANGS) lang else ENGLISH
|
||||||
|
|
||||||
|
val genreFilterTitle: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Gêneros"
|
||||||
|
SPANISH -> "Géneros"
|
||||||
|
else -> "Genres"
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusFilterTitle: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Estado"
|
||||||
|
SPANISH -> "Estado"
|
||||||
|
else -> "Status"
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusOngoing: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Em andamento"
|
||||||
|
SPANISH -> "En curso"
|
||||||
|
else -> "Ongoing"
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusOnHiatus: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Em hiato"
|
||||||
|
SPANISH -> "En hiatus"
|
||||||
|
else -> "Ongoing"
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusDropped: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Cancelada"
|
||||||
|
SPANISH -> "Abandonada"
|
||||||
|
else -> "Dropped"
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortByFilterTitle: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Ordenar por"
|
||||||
|
SPANISH -> "Ordenar por"
|
||||||
|
else -> "Sort by"
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortByTitle: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Título"
|
||||||
|
SPANISH -> "Titulo"
|
||||||
|
else -> "Title"
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortByViews: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Visualizações"
|
||||||
|
SPANISH -> "Número de vistas"
|
||||||
|
else -> "Views"
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortByLatest: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Recentes"
|
||||||
|
SPANISH -> "Recientes"
|
||||||
|
else -> "Latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortByRecentlyAdded: String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE -> "Data de criação"
|
||||||
|
SPANISH -> "Añadido recientemente"
|
||||||
|
else -> "Recently added"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun urlChangedError(sourceName: String): String = when (availableLang) {
|
||||||
|
BRAZILIAN_PORTUGUESE ->
|
||||||
|
"A URL da série mudou. Migre de $sourceName " +
|
||||||
|
"para $sourceName para atualizar a URL."
|
||||||
|
SPANISH ->
|
||||||
|
"La URL de la serie ha cambiado. Migre de $sourceName a " +
|
||||||
|
"$sourceName para actualizar la URL."
|
||||||
|
else ->
|
||||||
|
"The URL of the series has changed. Migrate from $sourceName " +
|
||||||
|
"to $sourceName to update the URL."
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BRAZILIAN_PORTUGUESE = "pt-BR"
|
||||||
|
const val ENGLISH = "en"
|
||||||
|
const val SPANISH = "es"
|
||||||
|
|
||||||
|
val AVAILABLE_LANGS = arrayOf(BRAZILIAN_PORTUGUESE, ENGLISH, SPANISH)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,6 @@ class MadaraGenerator : ThemeSourceGenerator {
|
||||||
MultiLang("Olympus Scanlation", "https://olympusscanlation.com", listOf("es", "pt-BR")),
|
MultiLang("Olympus Scanlation", "https://olympusscanlation.com", listOf("es", "pt-BR")),
|
||||||
MultiLang("Reaper Scans", "https://reaperscans.com", listOf("en", "fr", "id", "tr"), className = "ReaperScansFactory", pkgName = "reaperscans", overrideVersionCode = 7),
|
MultiLang("Reaper Scans", "https://reaperscans.com", listOf("en", "fr", "id", "tr"), className = "ReaperScansFactory", pkgName = "reaperscans", overrideVersionCode = 7),
|
||||||
MultiLang("Seven King Scanlation", "https://sksubs.net", listOf("es", "en"), isNsfw = true),
|
MultiLang("Seven King Scanlation", "https://sksubs.net", listOf("es", "en"), isNsfw = true),
|
||||||
MultiLang("YugenMangas", "https://yugenmangas.com", listOf("es", "pt-BR"), overrideVersionCode = 3),
|
|
||||||
SingleLang("1st Kiss Manga.love", "https://1stkissmanga.love", "en", className = "FirstKissMangaLove", overrideVersionCode = 1),
|
SingleLang("1st Kiss Manga.love", "https://1stkissmanga.love", "en", className = "FirstKissMangaLove", overrideVersionCode = 1),
|
||||||
SingleLang("1st Kiss Manhua", "https://1stkissmanhua.com", "en", className = "FirstKissManhua", overrideVersionCode = 3),
|
SingleLang("1st Kiss Manhua", "https://1stkissmanhua.com", "en", className = "FirstKissManhua", overrideVersionCode = 3),
|
||||||
SingleLang("1st Kiss", "https://1stkissmanga.io", "en", className = "FirstKissManga", pkgName = "firstkissmanga", overrideVersionCode = 7),
|
SingleLang("1st Kiss", "https://1stkissmanga.io", "en", className = "FirstKissManga", pkgName = "firstkissmanga", overrideVersionCode = 7),
|
||||||
|
@ -486,6 +485,7 @@ class MadaraGenerator : ThemeSourceGenerator {
|
||||||
SingleLang("YaoiToon", "https://yaoitoon.com", "en", isNsfw = true),
|
SingleLang("YaoiToon", "https://yaoitoon.com", "en", isNsfw = true),
|
||||||
SingleLang("Yetişkin Rüya Manga", "https://yetiskin.ruyamanga.com", "tr", isNsfw = true, className = "YetiskinRuyaManga"),
|
SingleLang("Yetişkin Rüya Manga", "https://yetiskin.ruyamanga.com", "tr", isNsfw = true, className = "YetiskinRuyaManga"),
|
||||||
SingleLang("YonaBar", "https://yonabar.com", "ar", isNsfw = true, overrideVersionCode = 2),
|
SingleLang("YonaBar", "https://yonabar.com", "ar", isNsfw = true, overrideVersionCode = 2),
|
||||||
|
SingleLang("YugenMangas", "https://yugenmangas.com.br", "pt-BR"),
|
||||||
SingleLang("Yuri Verso", "https://yuri.live", "pt-BR", overrideVersionCode = 3),
|
SingleLang("Yuri Verso", "https://yuri.live", "pt-BR", overrideVersionCode = 3),
|
||||||
SingleLang("Zinmanga", "https://zinmanga.com", "en", overrideVersionCode = 1),
|
SingleLang("Zinmanga", "https://zinmanga.com", "en", overrideVersionCode = 1),
|
||||||
SingleLang("Zinmanhwa", "https://zinmanhwa.com", "en"),
|
SingleLang("Zinmanhwa", "https://zinmanhwa.com", "en"),
|
||||||
|
|