Compare commits
48 Commits
d726f9eee1
...
bbb2922515
Author | SHA1 | Date |
---|---|---|
WarmSeeker6 | bbb2922515 | |
lamaxama | 597f1ad8d1 | |
Chopper | 4c13971c15 | |
Chopper | 64659b0f11 | |
BrutuZ | c411229164 | |
kana-shii | d86505e788 | |
bapeey | 9aecf7e174 | |
bapeey | 9385d11c43 | |
bapeey | dcf3bde0a4 | |
bapeey | 26ddebcc20 | |
Secozzi | f0d8933cf8 | |
AwkwardPeak7 | 9238b633a2 | |
AwkwardPeak7 | 96ff217f86 | |
AwkwardPeak7 | 7423444c40 | |
Chopper | c2a996152e | |
AwkwardPeak7 | 7475e54eaa | |
AwkwardPeak7 | 71d2a50a96 | |
bapeey | ada1d19b34 | |
Vetle Ledaal | 398e59a3e3 | |
AwkwardPeak7 | 955c098d8e | |
Vetle Ledaal | a009e6b4d1 | |
Chaos Pjeles | 6da99b2e55 | |
Hasan | 53b99172d4 | |
bapeey | 1ad2cfa0e1 | |
bapeey | 440abc28d8 | |
bapeey | 7c27192ab2 | |
bapeey | 5a118fec87 | |
kana-shii | 2a81be31c1 | |
bapeey | 15b3fc9866 | |
AwkwardPeak7 | 8ffd960733 | |
AwkwardPeak7 | 0d3409399b | |
Chopper | e79e16f7d9 | |
bapeey | 40619db2b2 | |
Chopper | be7c034bd2 | |
bapeey | 9d43ba0711 | |
Norsze | 46aa7fd5cf | |
Secozzi | 490eab456b | |
Secozzi | 485447d7b2 | |
bapeey | fb22115b58 | |
Norsze | 130a22e847 | |
Vetle Ledaal | 13f8712813 | |
Vetle Ledaal | ce0bdea748 | |
Norsze | 36fc99d48b | |
Secozzi | 9c5d99e898 | |
haruki-takeshi | 3d0c1ef64e | |
bapeey | 885d951788 | |
AwkwardPeak7 | 165d83f01b | |
AwkwardPeak7 | bb0db200aa |
|
@ -45,7 +45,7 @@ jobs:
|
||||||
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0
|
uses: gradle/actions/setup-gradle@6cec5d49d4d6d4bb982fbed7047db31ea6d38f11 # v3.3.0
|
||||||
|
|
||||||
- name: Build extensions
|
- name: Build extensions
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 1
|
|
@ -0,0 +1,90 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.liliana
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
interface UrlPartFilter {
|
||||||
|
fun addUrlParameter(url: HttpUrl.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val options: List<Pair<String, String>>,
|
||||||
|
private val urlParameter: String,
|
||||||
|
) : UrlPartFilter, Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.map { it.first }.toTypedArray(),
|
||||||
|
) {
|
||||||
|
override fun addUrlParameter(url: HttpUrl.Builder) {
|
||||||
|
url.addQueryParameter(urlParameter, options[state].second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriStateFilter(name: String, val id: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
abstract class TriStateGroupFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
private val includeUrlParameter: String,
|
||||||
|
private val excludeUrlParameter: String,
|
||||||
|
) : UrlPartFilter, Filter.Group<TriStateFilter>(
|
||||||
|
name,
|
||||||
|
options.map { TriStateFilter(it.first, it.second) },
|
||||||
|
) {
|
||||||
|
override fun addUrlParameter(url: HttpUrl.Builder) {
|
||||||
|
url.addQueryParameter(
|
||||||
|
includeUrlParameter,
|
||||||
|
state.filter { it.isIncluded() }.joinToString(",") { it.id },
|
||||||
|
)
|
||||||
|
url.addQueryParameter(
|
||||||
|
excludeUrlParameter,
|
||||||
|
state.filter { it.isExcluded() }.joinToString(",") { it.id },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenreFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : TriStateGroupFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"genres",
|
||||||
|
"notGenres",
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChapterCountFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : SelectFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"chapter_count",
|
||||||
|
)
|
||||||
|
|
||||||
|
class StatusFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : SelectFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"status",
|
||||||
|
)
|
||||||
|
|
||||||
|
class GenderFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : SelectFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"sex",
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortFilter(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : SelectFilter(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
"sort",
|
||||||
|
)
|
|
@ -0,0 +1,353 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.liliana
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
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.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
abstract class Liliana(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
final override val lang: String,
|
||||||
|
private val usesPostSearch: Boolean = false,
|
||||||
|
) : ParsedHttpSource() {
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector(): String = "div#main div.grid > div"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
|
thumbnail_url = element.selectFirst("img")?.imgAttr()
|
||||||
|
with(element.selectFirst(".text-center a")!!) {
|
||||||
|
title = text()
|
||||||
|
setUrlWithoutDomain(attr("abs:href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span"
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/all-manga/$page/?sort=last_update&status=0", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||||
|
popularMangaParse(response)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
if (query.isNotBlank() && usesPostSearch) {
|
||||||
|
val formBody = FormBody.Builder()
|
||||||
|
.add("search", query)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val formHeaders = headersBuilder().apply {
|
||||||
|
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
add("Host", baseUrl.toHttpUrl().host)
|
||||||
|
add("Origin", baseUrl)
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return POST("$baseUrl/ajax/search", formHeaders, formBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
addPathSegment("search")
|
||||||
|
addQueryParameter("keyword", query)
|
||||||
|
} else {
|
||||||
|
addPathSegment("filter")
|
||||||
|
filters.filterIsInstance<UrlPartFilter>().forEach {
|
||||||
|
it.addUrlParameter(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addPathSegment(page.toString())
|
||||||
|
addPathSegment("")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
if (response.request.method == "GET") {
|
||||||
|
return popularMangaParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaList = response.parseAs<SearchResponseDto>().list.map { manga ->
|
||||||
|
SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(manga.url)
|
||||||
|
title = manga.name
|
||||||
|
thumbnail_url = baseUrl + manga.cover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangaList, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SearchResponseDto(
|
||||||
|
val list: List<MangaDto>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class MangaDto(
|
||||||
|
val cover: String,
|
||||||
|
val name: String,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Filters ==============================
|
||||||
|
|
||||||
|
protected var genreName = ""
|
||||||
|
protected var genreData = listOf<Pair<String, String>>()
|
||||||
|
protected var chapterCountName = ""
|
||||||
|
protected var chapterCountData = listOf<Pair<String, String>>()
|
||||||
|
protected var statusName = ""
|
||||||
|
protected var statusData = listOf<Pair<String, String>>()
|
||||||
|
protected var genderName = ""
|
||||||
|
protected var genderData = listOf<Pair<String, String>>()
|
||||||
|
protected var sortName = ""
|
||||||
|
protected var sortData = listOf<Pair<String, String>>()
|
||||||
|
private var fetchFilterAttempts = 0
|
||||||
|
|
||||||
|
protected suspend fun fetchFilters() {
|
||||||
|
if (
|
||||||
|
fetchFilterAttempts < 3 &&
|
||||||
|
arrayOf(genreData, chapterCountData, statusData, genderData, sortData).any { it.isEmpty() }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val doc = client.newCall(filtersRequest())
|
||||||
|
.await()
|
||||||
|
.asJsoup()
|
||||||
|
|
||||||
|
parseFilters(doc)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("$name: Filters", e.stackTraceToString())
|
||||||
|
}
|
||||||
|
fetchFilterAttempts++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun filtersRequest() = GET("$baseUrl/filter", headers)
|
||||||
|
|
||||||
|
protected open fun parseFilters(document: Document) {
|
||||||
|
genreName = document.selectFirst("div.advanced-genres > h3")?.text() ?: ""
|
||||||
|
genreData = document.select("div.advanced-genres > div > .advance-item").map {
|
||||||
|
it.text() to it.selectFirst("span")!!.attr("data-genre")
|
||||||
|
}
|
||||||
|
|
||||||
|
chapterCountName = document.getSelectName("select-count")
|
||||||
|
chapterCountData = document.getSelectData("select-count")
|
||||||
|
|
||||||
|
statusName = document.getSelectName("select-status")
|
||||||
|
statusData = document.getSelectData("select-status")
|
||||||
|
|
||||||
|
genderName = document.getSelectName("select-gender")
|
||||||
|
genderData = document.getSelectData("select-gender")
|
||||||
|
|
||||||
|
sortName = document.getSelectName("select-sort")
|
||||||
|
sortData = document.getSelectData("select-sort")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Document.getSelectName(selectorClass: String): String {
|
||||||
|
return this.selectFirst(".select-div > label.$selectorClass")?.text() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Document.getSelectData(selectorId: String): List<Pair<String, String>> {
|
||||||
|
return this.select("#$selectorId > option").map {
|
||||||
|
it.text() to it.attr("value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
launchIO { fetchFilters() }
|
||||||
|
|
||||||
|
val filters = mutableListOf<Filter<*>>()
|
||||||
|
|
||||||
|
if (genreData.isNotEmpty()) {
|
||||||
|
filters.add(GenreFilter(genreName, genreData))
|
||||||
|
}
|
||||||
|
if (chapterCountData.isNotEmpty()) {
|
||||||
|
filters.add(ChapterCountFilter(chapterCountName, chapterCountData))
|
||||||
|
}
|
||||||
|
if (statusData.isNotEmpty()) {
|
||||||
|
filters.add(StatusFilter(statusName, statusData))
|
||||||
|
}
|
||||||
|
if (genderData.isNotEmpty()) {
|
||||||
|
filters.add(GenderFilter(genderName, genderData))
|
||||||
|
}
|
||||||
|
if (sortData.isNotEmpty()) {
|
||||||
|
filters.add(SortFilter(sortName, sortData))
|
||||||
|
}
|
||||||
|
if (filters.size < 5) {
|
||||||
|
filters.add(0, Filter.Header("Press 'reset' to load more filters"))
|
||||||
|
} else {
|
||||||
|
filters.add(0, Filter.Header("NOTE: Ignored if using text search!"))
|
||||||
|
filters.add(1, Filter.Separator())
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
protected fun launchIO(block: suspend () -> Unit) = scope.launch { block() }
|
||||||
|
|
||||||
|
// =========================== Manga Details ============================
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||||
|
description = document.selectFirst("div#syn-target")?.text()
|
||||||
|
thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr()
|
||||||
|
title = document.selectFirst(".a2 header h1")!!.text()
|
||||||
|
genre = document.select(".a2 div > a[rel='tag'].label").joinToString { it.text() }
|
||||||
|
author = document.selectFirst("div.y6x11p i.fas.fa-user + span.dt")?.text()?.takeUnless {
|
||||||
|
it.equals("updating", true)
|
||||||
|
}
|
||||||
|
status = document.selectFirst("div.y6x11p i.fas.fa-rss + span.dt").parseStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||||
|
"ongoing", "đang tiến hành", "進行中" -> SManga.ONGOING
|
||||||
|
"completed", "hoàn thành", "完了" -> SManga.COMPLETED
|
||||||
|
"on-hold", "tạm ngưng", "保留" -> SManga.ON_HIATUS
|
||||||
|
"canceled", "đã huỷ", "キャンセル" -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Chapters ==============================
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "ul > li.chapter"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
|
element.selectFirst("time[datetime]")?.also {
|
||||||
|
date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L
|
||||||
|
}
|
||||||
|
with(element.selectFirst("a")!!) {
|
||||||
|
name = text()
|
||||||
|
setUrlWithoutDomain(attr("abs:href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Pages ================================
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageListResponseDto(
|
||||||
|
val status: Boolean = false,
|
||||||
|
val msg: String? = null,
|
||||||
|
val html: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")?.data()
|
||||||
|
?: throw Exception("Failed to get chapter id")
|
||||||
|
|
||||||
|
val chapterId = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
||||||
|
|
||||||
|
val pageHeaders = headersBuilder().apply {
|
||||||
|
add("Accept", "application/json, text/javascript, *//*; q=0.01")
|
||||||
|
add("Host", baseUrl.toHttpUrl().host)
|
||||||
|
set("Referer", response.request.url.toString())
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val ajaxResponse = client.newCall(
|
||||||
|
GET("$baseUrl/ajax/image/list/chap/$chapterId", pageHeaders),
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
val data = ajaxResponse.parseAs<PageListResponseDto>()
|
||||||
|
|
||||||
|
if (!data.status) {
|
||||||
|
throw Exception(data.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageListParse(
|
||||||
|
Jsoup.parseBodyFragment(
|
||||||
|
data.html,
|
||||||
|
response.request.url.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select("div.separator").mapIndexed { i, page ->
|
||||||
|
val url = page.selectFirst("a")!!.attr("abs:href")
|
||||||
|
Page(i, document.location(), url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page): Request {
|
||||||
|
val imgHeaders = headersBuilder().apply {
|
||||||
|
add("Accept", "image/avif,image/webp,*/*")
|
||||||
|
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||||
|
}.build()
|
||||||
|
return GET(page.imageUrl!!, imgHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
// From mangathemesia
|
||||||
|
private fun Element.imgAttr(): String = when {
|
||||||
|
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
|
else -> attr("abs:src")
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
|
return json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'CosplayTele'
|
||||||
|
extClass = '.CosplayTele'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,228 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.cosplaytele
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
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.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class CosplayTele : ParsedHttpSource() {
|
||||||
|
override val baseUrl = "https://cosplaytele.com"
|
||||||
|
override val lang = "all"
|
||||||
|
override val name = "CosplayTele"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||||
|
val linkEl = element.selectFirst("h5 a")!!
|
||||||
|
manga.title = linkEl.text()
|
||||||
|
manga.setUrlWithoutDomain(linkEl.attr("abs:href"))
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = ".next.page-number"
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.box"
|
||||||
|
|
||||||
|
// Popular
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String? {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val popularPageLimit = 20
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/wp-json/wordpress-popular-posts/v1/popular-posts?offset=${page * popularPageLimit}&limit=$popularPageLimit&range=last7days")
|
||||||
|
override fun popularMangaSelector(): String = ""
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val jsonObject = json.decodeFromString<JsonArray>(response.body.string())
|
||||||
|
val mangas = jsonObject.map { item ->
|
||||||
|
val head = item.jsonObject["yoast_head_json"]!!.jsonObject
|
||||||
|
SManga.create().apply {
|
||||||
|
title = head["og_title"]!!.jsonPrimitive.content
|
||||||
|
thumbnail_url = head["og_image"]!!.jsonArray[0].jsonObject["url"]!!.jsonPrimitive.content
|
||||||
|
setUrlWithoutDomain(head["og_url"]!!.jsonPrimitive.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MangasPage(mangas, mangas.size >= popularPageLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||||
|
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
|
|
||||||
|
val categoryFilter = filterList.findInstance<UriPartFilter>()
|
||||||
|
return when {
|
||||||
|
categoryFilter?.state != 0 -> GET(
|
||||||
|
baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
addPathSegments(categoryFilter!!.toUriPart())
|
||||||
|
addPathSegment("page")
|
||||||
|
addPathSegment(page.toString())
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
addQueryParameter("s", query)
|
||||||
|
}
|
||||||
|
}.build(),
|
||||||
|
)
|
||||||
|
query.isNotEmpty() -> GET(
|
||||||
|
"$baseUrl/page/$page/".toHttpUrl().newBuilder().apply {
|
||||||
|
addQueryParameter("s", query)
|
||||||
|
}.build(),
|
||||||
|
)
|
||||||
|
else -> latestUpdatesRequest(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = latestUpdatesSelector()
|
||||||
|
|
||||||
|
// Details
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.title = document.select(".entry-title").text()
|
||||||
|
manga.description = document.select(".entry-title").text()
|
||||||
|
manga.genre = getTags(document).joinToString(", ")
|
||||||
|
manga.status = SManga.COMPLETED
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTags(document: Element): List<String> {
|
||||||
|
val pattern = """.*/(tag|category)/.*""".toRegex()
|
||||||
|
return document.select("#main a").filter { a -> pattern.matches(a.attr("href")) }.map { a ->
|
||||||
|
val link = a.attr("href").split(".com/")[1]
|
||||||
|
val tag = a.text()
|
||||||
|
if (tag.isNotEmpty()) {
|
||||||
|
categories[tag] = link
|
||||||
|
}
|
||||||
|
tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(element.select("link[rel=\"canonical\"]").attr("href"))
|
||||||
|
chapter.name = "Gallery"
|
||||||
|
chapter.date_upload = getDate(element.select("time.updated").attr("datetime"))
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "html"
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
document.select(".gallery-item img").forEachIndexed { i, it ->
|
||||||
|
val itUrl = it.attr("src")
|
||||||
|
pages.add(Page(i, imageUrl = itUrl))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch { fetchFilters() }
|
||||||
|
val filters = mutableListOf<Filter<*>>(
|
||||||
|
Filter.Header("NOTE: Only one filter will be applied!"),
|
||||||
|
Filter.Separator(),
|
||||||
|
UriPartFilter("Category", categories.entries.toTypedArray()),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filtersState == FilterState.Unfetched) {
|
||||||
|
filters.add(1, Filter.Header("Use 'reset' to load all filters"))
|
||||||
|
}
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
open class UriPartFilter(
|
||||||
|
displayName: String,
|
||||||
|
private val valuePair: Array<MutableMap.MutableEntry<String, String>>,
|
||||||
|
) : Filter.Select<String>(displayName, valuePair.map { it.key }.toTypedArray()) {
|
||||||
|
fun toUriPart() = valuePair[state].value
|
||||||
|
}
|
||||||
|
|
||||||
|
private var categories = mutableMapOf(
|
||||||
|
Pair("All", ""),
|
||||||
|
Pair("Cosplay Nude", "category/nude"),
|
||||||
|
Pair("Cosplay Ero", "category/no-nude"),
|
||||||
|
Pair("Cosplay", "category/cosplay"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private var filtersState = FilterState.Unfetched
|
||||||
|
private var filterAttempts = 0
|
||||||
|
|
||||||
|
private enum class FilterState {
|
||||||
|
Fetching, Fetched, Unfetched
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchFilters() {
|
||||||
|
if (filtersState == FilterState.Unfetched && filterAttempts < 3) {
|
||||||
|
filtersState = FilterState.Fetching
|
||||||
|
filterAttempts++
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.newCall(GET("$baseUrl/explore-categories/", headers))
|
||||||
|
.await()
|
||||||
|
.asJsoup().let { document -> getTags(document) }
|
||||||
|
filtersState = FilterState.Fetched
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(name, e.stackTraceToString())
|
||||||
|
filtersState = FilterState.Unfetched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||||
|
|
||||||
|
private fun getDate(str: String): Long {
|
||||||
|
try {
|
||||||
|
val format = str.split("T")[0]
|
||||||
|
return DATE_FORMAT.parse(format)?.time ?: 0L
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
return 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATE_FORMAT by lazy {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'ARESNOV'
|
extName = 'SCARManga'
|
||||||
extClass = '.ARESNOV'
|
extClass = '.ScarManga'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://manhuascarlet.com'
|
baseUrl = 'https://scarmanga.com'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 43 KiB |
|
@ -1,41 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.aresnov
|
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import java.lang.IllegalArgumentException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class ARESNOV : MangaThemesia(
|
|
||||||
"ARESNOV",
|
|
||||||
"https://manhuascarlet.com",
|
|
||||||
"ar",
|
|
||||||
mangaUrlDirectory = "/series",
|
|
||||||
dateFormat = SimpleDateFormat("MMMMM dd, yyyy", Locale("ar")),
|
|
||||||
) {
|
|
||||||
override val seriesAuthorSelector = ".imptdt:contains(المؤلف) i"
|
|
||||||
override val seriesArtistSelector = ".imptdt:contains(الرسام) i"
|
|
||||||
override val seriesTypeSelector = ".imptdt:contains(النوع) i"
|
|
||||||
override val seriesStatusSelector = ".imptdt:contains(الحالة) i"
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
// "ts_reader.run({" in base64
|
|
||||||
val script = document.selectFirst("script[src^=data:text/javascript;base64,dHNfcmVhZGVyLnJ1bih7]")
|
|
||||||
?: return super.pageListParse(document)
|
|
||||||
val data = Base64.decode(script.attr("src").substringAfter("base64,"), Base64.DEFAULT).toString(Charsets.UTF_8)
|
|
||||||
val imageListJson = JSON_IMAGE_LIST_REGEX.find(data)?.destructured?.toList()?.get(0).orEmpty()
|
|
||||||
val imageList = try {
|
|
||||||
json.parseToJsonElement(imageListJson).jsonArray
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageList.mapIndexed { i, jsonEl ->
|
|
||||||
Page(i, imageUrl = jsonEl.jsonPrimitive.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.ar.aresnov
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class ScarManga : MangaThemesia(
|
||||||
|
"SCARManga",
|
||||||
|
"https://scarmanga.com",
|
||||||
|
"ar",
|
||||||
|
mangaUrlDirectory = "/series",
|
||||||
|
dateFormat = SimpleDateFormat("MMMMM dd, yyyy", Locale("ar")),
|
||||||
|
) {
|
||||||
|
override val id = 1046935749022479891
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Hizomanga'
|
|
||||||
extClass = '.Hizomanga'
|
|
||||||
themePkg = 'madara'
|
|
||||||
baseUrl = 'https://hizomanga.me'
|
|
||||||
overrideVersionCode = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,5 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.hizomanga
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
||||||
|
|
||||||
class Hizomanga : Madara("Hizomanga", "https://hizomanga.me", "ar")
|
|
|
@ -2,8 +2,8 @@ ext {
|
||||||
extName = 'Manga Flame'
|
extName = 'Manga Flame'
|
||||||
extClass = '.MangaFlame'
|
extClass = '.MangaFlame'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://arisescans.com'
|
baseUrl = 'https://mangaflame.org'
|
||||||
overrideVersionCode = 2
|
overrideVersionCode = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.mangaflame
|
package eu.kanade.tachiyomi.extension.ar.mangaflame
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class MangaFlame : MangaThemesia(
|
class MangaFlame : MangaThemesia(
|
||||||
"Manga Flame",
|
"Manga Flame",
|
||||||
"https://arisescans.com",
|
"https://mangaflame.org",
|
||||||
"ar",
|
"ar",
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||||
) {
|
) {
|
||||||
override val id = 1501237443119573205
|
override val id = 1501237443119573205
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
|
||||||
.readTimeout(3, TimeUnit.MINUTES)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 28 KiB |
|
@ -1,15 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.ar.ozulscans
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class KingOfManga : MangaThemesiaAlt(
|
|
||||||
"King Of Manga",
|
|
||||||
"https://king-ofmanga.com",
|
|
||||||
"ar",
|
|
||||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
|
||||||
) {
|
|
||||||
// Ozul Scans -> King of Manga
|
|
||||||
override val id = 3453769904666687440
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anchira'
|
extName = 'Anchira'
|
||||||
extClass = '.Anchira'
|
extClass = '.Anchira'
|
||||||
extVersionCode = 11
|
extVersionCode = 12
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,8 +82,11 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
url = "/g/${it.id}/${it.key}"
|
url = "/g/${it.id}/${it.key}"
|
||||||
title = it.title
|
title = it.title
|
||||||
thumbnail_url = "$cdnUrl/${it.id}/${it.key}/m/${it.thumbnailIndex + 1}"
|
thumbnail_url = "$cdnUrl/${it.id}/${it.key}/m/${it.thumbnailIndex + 1}"
|
||||||
artist = it.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
|
val art = it.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
|
||||||
|
.ifEmpty { null }
|
||||||
|
artist = art
|
||||||
author = it.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
|
author = it.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
|
||||||
|
.ifEmpty { art }
|
||||||
genre = prepareTags(it.tags, preferences.useTagGrouping)
|
genre = prepareTags(it.tags, preferences.useTagGrouping)
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
status = SManga.COMPLETED
|
status = SManga.COMPLETED
|
||||||
|
@ -240,8 +243,11 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
title = data.title
|
title = data.title
|
||||||
thumbnail_url =
|
thumbnail_url =
|
||||||
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
|
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
|
||||||
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
|
val art = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
|
||||||
|
.ifEmpty { null }
|
||||||
|
artist = art
|
||||||
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
|
author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name }
|
||||||
|
.ifEmpty { art }
|
||||||
genre = prepareTags(data.tags, preferences.useTagGrouping)
|
genre = prepareTags(data.tags, preferences.useTagGrouping)
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
status = SManga.COMPLETED
|
status = SManga.COMPLETED
|
||||||
|
@ -398,7 +404,7 @@ class Anchira : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
private class SortFilter : Filter.Sort(
|
private class SortFilter : Filter.Sort(
|
||||||
"Sort",
|
"Sort",
|
||||||
arrayOf("Title", "Pages", "Date published", "Date uploaded", "Popularity"),
|
arrayOf("Title", "Pages", "Date uploaded", "Date published", "Popularity"),
|
||||||
Selection(2, false),
|
Selection(2, false),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ data class Entry(
|
||||||
val key: String,
|
val key: String,
|
||||||
@SerialName("published_at") val publishedAt: Long = 0L,
|
@SerialName("published_at") val publishedAt: Long = 0L,
|
||||||
val title: String,
|
val title: String,
|
||||||
@SerialName("thumb_index") val thumbnailIndex: Int = 1,
|
@SerialName("thumb_index") val thumbnailIndex: Int = 0,
|
||||||
val tags: List<Tag> = emptyList(),
|
val tags: List<Tag> = emptyList(),
|
||||||
val url: String? = null,
|
val url: String? = null,
|
||||||
val pages: Int = 1,
|
val pages: Int = 1,
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Arven Scans'
|
extName = 'Vortex Scans'
|
||||||
extClass = '.ArvenScans'
|
extClass = '.VortexScans'
|
||||||
themePkg = 'mangathemesia'
|
extVersionCode = 31
|
||||||
baseUrl = 'https://arvenscans.com'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 50 KiB |
|
@ -1,13 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.arvenscans
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class ArvenScans : MangaThemesia("Arven Scans", "https://arvenscans.com", "en", "/series") {
|
|
||||||
|
|
||||||
override val client: OkHttpClient = super.client.newBuilder()
|
|
||||||
.rateLimit(20, 5, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SearchResponse(
|
||||||
|
val posts: List<Manga>,
|
||||||
|
val totalCount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Manga(
|
||||||
|
val id: Int,
|
||||||
|
val slug: String,
|
||||||
|
private val postTitle: String,
|
||||||
|
private val postContent: String? = null,
|
||||||
|
val isNovel: Boolean,
|
||||||
|
private val featuredImage: String? = null,
|
||||||
|
private val alternativeTitles: String? = null,
|
||||||
|
private val author: String? = null,
|
||||||
|
private val artist: String? = null,
|
||||||
|
private val seriesType: String? = null,
|
||||||
|
private val seriesStatus: String? = null,
|
||||||
|
private val genres: List<Name>? = emptyList(),
|
||||||
|
) {
|
||||||
|
fun toSManga(baseUrl: String) = SManga.create().apply {
|
||||||
|
url = "$slug#$id"
|
||||||
|
title = postTitle
|
||||||
|
thumbnail_url = "$baseUrl/_next/image".toHttpUrl().newBuilder().apply {
|
||||||
|
addQueryParameter("url", featuredImage)
|
||||||
|
addQueryParameter("w", "828")
|
||||||
|
addQueryParameter("q", "75")
|
||||||
|
}.toString()
|
||||||
|
author = this@Manga.author?.takeUnless { it.isEmpty() }
|
||||||
|
artist = this@Manga.artist?.takeUnless { it.isEmpty() }
|
||||||
|
description = buildString {
|
||||||
|
postContent?.takeUnless { it.isEmpty() }?.let { desc ->
|
||||||
|
val tmpDesc = desc.replace("\n", "<br>")
|
||||||
|
|
||||||
|
append(Jsoup.parse(tmpDesc).text())
|
||||||
|
}
|
||||||
|
alternativeTitles?.takeUnless { it.isEmpty() }?.let { altName ->
|
||||||
|
append("\n\n")
|
||||||
|
append("Alternative Names: ")
|
||||||
|
append(altName)
|
||||||
|
}
|
||||||
|
}.trim()
|
||||||
|
genre = getGenres()
|
||||||
|
status = when (seriesStatus) {
|
||||||
|
"ONGOING", "COMING_SOON" -> SManga.ONGOING
|
||||||
|
"COMPLETED" -> SManga.COMPLETED
|
||||||
|
"CANCELLED", "DROPPED" -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGenres() = buildList {
|
||||||
|
when (seriesType) {
|
||||||
|
"MANGA" -> add("Manga")
|
||||||
|
"MANHUA" -> add("Manhua")
|
||||||
|
"MANHWA" -> add("Manhwa")
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
genres?.forEach { add(it.name) }
|
||||||
|
}.distinct().joinToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Name(val name: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Post<T>(val post: T)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterListResponse(
|
||||||
|
val isNovel: Boolean,
|
||||||
|
val slug: String,
|
||||||
|
val chapters: List<Chapter>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Chapter(
|
||||||
|
private val id: Int,
|
||||||
|
private val slug: String,
|
||||||
|
private val number: JsonPrimitive,
|
||||||
|
private val createdBy: Name,
|
||||||
|
private val createdAt: String,
|
||||||
|
private val chapterStatus: String,
|
||||||
|
) {
|
||||||
|
fun isPublic() = chapterStatus == "PUBLIC"
|
||||||
|
|
||||||
|
fun toSChapter(mangaSlug: String) = SChapter.create().apply {
|
||||||
|
url = "/series/$mangaSlug/$slug#$id"
|
||||||
|
name = "Chapter $number"
|
||||||
|
scanlator = createdBy.name
|
||||||
|
date_upload = try {
|
||||||
|
dateFormat.parse(createdAt)!!.time
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
|
@ -0,0 +1,101 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
interface UrlPartFilter {
|
||||||
|
fun addUrlParameter(url: HttpUrl.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val urlParameter: String,
|
||||||
|
private val options: List<Pair<String, String>>,
|
||||||
|
) : UrlPartFilter, Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.map { it.first }.toTypedArray(),
|
||||||
|
) {
|
||||||
|
override fun addUrlParameter(url: HttpUrl.Builder) {
|
||||||
|
url.addQueryParameter(urlParameter, options[state].second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckBoxFilter(name: String, val value: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
|
abstract class CheckBoxGroup(
|
||||||
|
name: String,
|
||||||
|
private val urlParameter: String,
|
||||||
|
options: List<Pair<String, String>>,
|
||||||
|
) : UrlPartFilter, Filter.Group<CheckBoxFilter>(
|
||||||
|
name,
|
||||||
|
options.map { CheckBoxFilter(it.first, it.second) },
|
||||||
|
) {
|
||||||
|
override fun addUrlParameter(url: HttpUrl.Builder) {
|
||||||
|
val checked = state.filter { it.state }.map { it.value }
|
||||||
|
|
||||||
|
if (checked.isNotEmpty()) {
|
||||||
|
url.addQueryParameter(urlParameter, checked.joinToString(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusFilter : SelectFilter(
|
||||||
|
"Status",
|
||||||
|
"seriesStatus",
|
||||||
|
listOf(
|
||||||
|
Pair("", ""),
|
||||||
|
Pair("Ongoing", "ONGOING"),
|
||||||
|
Pair("Completed", "COMPLETED"),
|
||||||
|
Pair("Cancelled", "CANCELLED"),
|
||||||
|
Pair("Dropped", "DROPPED"),
|
||||||
|
Pair("Mass Released", "MASS_RELEASED"),
|
||||||
|
Pair("Coming Soon", "COMING_SOON"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class TypeFilter : SelectFilter(
|
||||||
|
"Type",
|
||||||
|
"seriesType",
|
||||||
|
listOf(
|
||||||
|
Pair("", ""),
|
||||||
|
Pair("Manga", "MANGA"),
|
||||||
|
Pair("Manhua", "MANHUA"),
|
||||||
|
Pair("Manhwa", "MANHWA"),
|
||||||
|
Pair("Russian", "RUSSIAN"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class GenreFilter : CheckBoxGroup(
|
||||||
|
"Genres",
|
||||||
|
"genreIds",
|
||||||
|
listOf(
|
||||||
|
Pair("Action", "1"),
|
||||||
|
Pair("Adventure", "13"),
|
||||||
|
Pair("Comedy", "7"),
|
||||||
|
Pair("Drama", "2"),
|
||||||
|
Pair("elf", "25"),
|
||||||
|
Pair("Fantas", "28"),
|
||||||
|
Pair("Fantasy", "8"),
|
||||||
|
Pair("Historical", "19"),
|
||||||
|
Pair("Horror", "9"),
|
||||||
|
Pair("Josei", "21"),
|
||||||
|
Pair("Manhwa", "5"),
|
||||||
|
Pair("Martial Arts", "6"),
|
||||||
|
Pair("Mature", "12"),
|
||||||
|
Pair("Monsters", "14"),
|
||||||
|
Pair("Reincarnation", "16"),
|
||||||
|
Pair("Revenge", "17"),
|
||||||
|
Pair("Romance", "20"),
|
||||||
|
Pair("School Life", "23"),
|
||||||
|
Pair("Seinen", "10"),
|
||||||
|
Pair("shojo", "26"),
|
||||||
|
Pair("Shoujo", "22"),
|
||||||
|
Pair("Shounen", "3"),
|
||||||
|
Pair("Slice Of Life", "18"),
|
||||||
|
Pair("Sports", "4"),
|
||||||
|
Pair("Supernatural", "11"),
|
||||||
|
Pair("System", "15"),
|
||||||
|
Pair("terror", "24"),
|
||||||
|
Pair("Video Games", "27"),
|
||||||
|
),
|
||||||
|
)
|
|
@ -0,0 +1,145 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.arvenscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
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.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class VortexScans : HttpSource() {
|
||||||
|
|
||||||
|
override val name = "Vortex Scans"
|
||||||
|
|
||||||
|
override val baseUrl = "https://vortexscans.com"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
private val json by injectLazy<Json>()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private val titleCache by lazy {
|
||||||
|
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
|
||||||
|
val data = response.parseAs<SearchResponse>()
|
||||||
|
|
||||||
|
data.posts
|
||||||
|
.filterNot { it.isNovel }
|
||||||
|
.associateBy { it.slug }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/home", headers)
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
|
||||||
|
.map { it.absUrl("href").substringAfterLast("/series/") }
|
||||||
|
|
||||||
|
val entries = slugs.mapNotNull {
|
||||||
|
titleCache[it]?.toSManga(baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(entries, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
|
||||||
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
addQueryParameter("perPage", perPage.toString())
|
||||||
|
addQueryParameter("searchTerm", query.trim())
|
||||||
|
filters.filterIsInstance<UrlPartFilter>().forEach {
|
||||||
|
it.addUrlParameter(this)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val data = response.parseAs<SearchResponse>()
|
||||||
|
val page = response.request.url.queryParameter("page")!!.toInt()
|
||||||
|
|
||||||
|
val entries = data.posts
|
||||||
|
.filterNot { it.isNovel }
|
||||||
|
.map { it.toSManga(baseUrl) }
|
||||||
|
|
||||||
|
val hasNextPage = data.totalCount > (page * perPage)
|
||||||
|
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
StatusFilter(),
|
||||||
|
TypeFilter(),
|
||||||
|
GenreFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val id = manga.url.substringAfterLast("#")
|
||||||
|
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
val slug = manga.url.substringBeforeLast("#")
|
||||||
|
|
||||||
|
return "$baseUrl/series/$slug"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val data = response.parseAs<Post<Manga>>()
|
||||||
|
|
||||||
|
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||||
|
|
||||||
|
// genres are only returned in search call
|
||||||
|
// and not when fetching details
|
||||||
|
return data.post.toSManga(baseUrl).apply {
|
||||||
|
genre = titleCache[data.post.slug]?.getGenres()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val data = response.parseAs<Post<ChapterListResponse>>()
|
||||||
|
|
||||||
|
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||||
|
|
||||||
|
return data.post.chapters
|
||||||
|
.filter { it.isPublic() }
|
||||||
|
.map { it.toSChapter(data.post.slug) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
return document.select("main > section > img").mapIndexed { idx, img ->
|
||||||
|
Page(idx, imageUrl = img.absUrl("src"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T =
|
||||||
|
json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val perPage = 18
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Blazescans'
|
||||||
|
extClass = '.Blazescans'
|
||||||
|
themePkg = 'mangathemesia'
|
||||||
|
baseUrl = 'https://blazescans.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.blazescans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class Blazescans : MangaThemesia("Blazescans", "https://blazescans.com", "en") {
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.rateLimit(1, 2, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Manhuagold'
|
extName = 'Manhuagold'
|
||||||
extClass = '.Manhuagold'
|
extClass = '.Manhuagold'
|
||||||
themePkg = 'mangareader'
|
themePkg = 'liliana'
|
||||||
baseUrl = 'https://manhuagold.com'
|
baseUrl = 'https://manhuagold.top'
|
||||||
overrideVersionCode = 33
|
overrideVersionCode = 34
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,233 +1,18 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.comickiba
|
package eu.kanade.tachiyomi.extension.en.comickiba
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
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.util.asJsoup
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import org.jsoup.nodes.TextNode
|
|
||||||
import org.jsoup.select.Evaluator
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
class Manhuagold : MangaReader() {
|
class Manhuagold : Liliana(
|
||||||
|
"Manhuagold",
|
||||||
|
"https://manhuagold.top",
|
||||||
|
"en",
|
||||||
|
usesPostSearch = true,
|
||||||
|
) {
|
||||||
|
// MangaReader -> Liliana
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
override val name = "Manhuagold"
|
override val client = super.client.newBuilder()
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val baseUrl = "https://manhuagold.com"
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimit(2)
|
.rateLimit(2)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) =
|
|
||||||
GET("$baseUrl/filter/$page/?sort=views&sex=All&chapter_count=0", headers)
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) =
|
|
||||||
GET("$baseUrl/filter/$page/?sort=latest-updated&sex=All&chapter_count=0", headers)
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
urlBuilder.addPathSegment("search").apply {
|
|
||||||
addQueryParameter("keyword", query)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
urlBuilder.addPathSegment("filter").apply {
|
|
||||||
filters.ifEmpty(::getFilterList).forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is Select -> {
|
|
||||||
addQueryParameter(filter.param, filter.selection)
|
|
||||||
}
|
|
||||||
is GenresFilter -> {
|
|
||||||
addQueryParameter(filter.param, filter.selection)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
urlBuilder.addPathSegment(page.toString())
|
|
||||||
urlBuilder.addPathSegment("")
|
|
||||||
|
|
||||||
return GET(urlBuilder.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) =
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
|
||||||
title = it.attr("alt")
|
|
||||||
thumbnail_url = it.imgAttr()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "ul.pagination > li.active + li"
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
|
|
||||||
override fun getFilterList() =
|
|
||||||
FilterList(
|
|
||||||
Note,
|
|
||||||
StatusFilter(),
|
|
||||||
SortFilter(),
|
|
||||||
GenresFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
|
|
||||||
val mangaTitle = root.selectFirst(Evaluator.Class("manga-name"))!!.ownText()
|
|
||||||
title = mangaTitle
|
|
||||||
description = root.run {
|
|
||||||
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
|
||||||
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
|
|
||||||
"", mangaTitle -> description
|
|
||||||
else -> "$description\n\nAlternative Title: $altTitle"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.imgAttr()
|
|
||||||
genre = root.selectFirst(Evaluator.Class("genres"))!!.children().joinToString { it.ownText() }
|
|
||||||
for (item in root.selectFirst(Evaluator.Class("anisc-info"))!!.children()) {
|
|
||||||
if (item.hasClass("item").not()) continue
|
|
||||||
when (item.selectFirst(Evaluator.Class("item-head"))!!.ownText()) {
|
|
||||||
"Authors:" -> item.parseAuthorsTo(this)
|
|
||||||
"Status:" -> status = when (item.selectFirst(Evaluator.Class("name"))!!.ownText().lowercase()) {
|
|
||||||
"ongoing" -> SManga.ONGOING
|
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
"on-hold" -> SManga.ON_HIATUS
|
|
||||||
"canceled" -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Element.parseAuthorsTo(manga: SManga) {
|
|
||||||
val authors = select(Evaluator.Tag("a"))
|
|
||||||
val text = authors.map { it.ownText().replace(",", "") }
|
|
||||||
val count = authors.size
|
|
||||||
when (count) {
|
|
||||||
0 -> return
|
|
||||||
1 -> {
|
|
||||||
manga.author = text[0]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val authorList = ArrayList<String>(count)
|
|
||||||
val artistList = ArrayList<String>(count)
|
|
||||||
for ((index, author) in authors.withIndex()) {
|
|
||||||
val textNode = author.nextSibling() as? TextNode
|
|
||||||
val list = if (textNode != null && "(Art)" in textNode.wholeText) artistList else authorList
|
|
||||||
list.add(text[index])
|
|
||||||
}
|
|
||||||
if (authorList.isEmpty().not()) manga.author = authorList.joinToString()
|
|
||||||
if (artistList.isEmpty().not()) manga.artist = artistList.joinToString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListRequest(mangaUrl: String, type: String): Request =
|
|
||||||
GET(baseUrl + mangaUrl, headers)
|
|
||||||
|
|
||||||
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override val chapterType = ""
|
|
||||||
override val volumeType = ""
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
return client.newCall(chapterListRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map(::parseChapterList)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterList(response: Response): List<SChapter> {
|
|
||||||
val document = response.use { it.asJsoup() }
|
|
||||||
|
|
||||||
return document.select(chapterListSelector())
|
|
||||||
.map(::chapterFromElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun chapterListSelector(): String = "#chapters-list > li"
|
|
||||||
|
|
||||||
private fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
||||||
element.selectFirst("a")!!.run {
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
name = selectFirst(".name")?.text() ?: text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Images
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable {
|
|
||||||
val document = client.newCall(pageListRequest(chapter)).execute().asJsoup()
|
|
||||||
|
|
||||||
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data()
|
|
||||||
val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
|
||||||
|
|
||||||
val ajaxHeaders = super.headersBuilder().apply {
|
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
|
||||||
add("Referer", baseUrl + chapter.url)
|
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val ajaxUrl = "$baseUrl/ajax/image/list/chap/$id"
|
|
||||||
client.newCall(GET(ajaxUrl, ajaxHeaders)).execute().let(::pageListParse)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.use { it.parseHtmlProperty() }
|
|
||||||
|
|
||||||
val pageList = document.select("div").map {
|
|
||||||
val index = it.attr("data-number").toInt()
|
|
||||||
val imgUrl = it.imgAttr().ifEmpty { it.selectFirst("img")!!.imgAttr() }
|
|
||||||
|
|
||||||
Page(index, "", imgUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageList
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilities
|
|
||||||
|
|
||||||
// From mangathemesia
|
|
||||||
private fun Element.imgAttr(): String = when {
|
|
||||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
|
||||||
else -> attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Response.parseHtmlProperty(): Document {
|
|
||||||
val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
|
|
||||||
return Jsoup.parseBodyFragment(html)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Drake Scans'
|
extName = 'Drake Scans'
|
||||||
extClass = '.DrakeScans'
|
extClass = '.DrakeScans'
|
||||||
themePkg = 'madara'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://drakescans.com'
|
baseUrl = 'https://drakescans.com'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 41 KiB |
|
@ -1,17 +1,12 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.drakescans
|
package eu.kanade.tachiyomi.extension.en.drakescans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class DrakeScans : Madara(
|
class DrakeScans : MangaThemesia(
|
||||||
"Drake Scans",
|
"Drake Scans",
|
||||||
"https://drakescans.com",
|
"https://drakescans.com",
|
||||||
"en",
|
"en",
|
||||||
SimpleDateFormat("dd/MM/yyyy", Locale.US),
|
|
||||||
) {
|
) {
|
||||||
|
// madara -> mangathemesia
|
||||||
override val mangaDetailsSelectorTag = ""
|
override val versionId = 2
|
||||||
|
|
||||||
override val mangaSubString = "series"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.ElarcPage'
|
extClass = '.ElarcPage'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://elarctoons.com'
|
baseUrl = 'https://elarctoons.com'
|
||||||
overrideVersionCode = 5
|
overrideVersionCode = 6
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,12 @@ class ElarcPage : MangaThemesia(
|
||||||
|
|
||||||
// Always update URL
|
// Always update URL
|
||||||
val response = chain.proceed(request)
|
val response = chain.proceed(request)
|
||||||
|
|
||||||
|
// Skip responses that do not start with "text/html"
|
||||||
|
if (response.header("content-type")?.startsWith("text/html") != true) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
val document = Jsoup.parse(
|
val document = Jsoup.parse(
|
||||||
response.peekBody(Long.MAX_VALUE).string(),
|
response.peekBody(Long.MAX_VALUE).string(),
|
||||||
request.url.toString(),
|
request.url.toString(),
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.KingOfManga'
|
extClass = '.KingOfManga'
|
||||||
themePkg = 'mangathemesia'
|
themePkg = 'mangathemesia'
|
||||||
baseUrl = 'https://king-ofmanga.com'
|
baseUrl = 'https://king-ofmanga.com'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.kingofmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||||
|
|
||||||
|
class KingOfManga : MangaThemesia("King Of Manga", "https://king-ofmanga.com", "en")
|
|
@ -0,0 +1,8 @@
|
||||||
|
ext {
|
||||||
|
extName = 'MangaTop'
|
||||||
|
extClass = '.MangaTop'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,131 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.mangatop
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
interface UriFilter {
|
||||||
|
fun addToUri(builder: HttpUrl.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
|
open class UriMultiSelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val param: String,
|
||||||
|
private val vals: Array<Pair<String, String>>,
|
||||||
|
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
||||||
|
override fun addToUri(builder: HttpUrl.Builder) {
|
||||||
|
state.filter { it.state }.forEach {
|
||||||
|
builder.addQueryParameter(param, it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeFilter : UriMultiSelectFilter(
|
||||||
|
"Type",
|
||||||
|
"types[]",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Manga", "1"),
|
||||||
|
Pair("Novel", "2"),
|
||||||
|
Pair("One Shot", "3"),
|
||||||
|
Pair("Doujinshi", "4"),
|
||||||
|
Pair("Manhwa", "5"),
|
||||||
|
Pair("Manhua", "6"),
|
||||||
|
Pair("OEL", "7"),
|
||||||
|
Pair("Light Novel", "8"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class GenreFilter : UriMultiSelectFilter(
|
||||||
|
"Genre",
|
||||||
|
"genres[]",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Action", "1"),
|
||||||
|
Pair("Adventure", "2"),
|
||||||
|
Pair("Avant Garde", "5"),
|
||||||
|
Pair("Award Winning", "46"),
|
||||||
|
Pair("Boys Love", "28"),
|
||||||
|
Pair("Comedy", "4"),
|
||||||
|
Pair("Drama", "8"),
|
||||||
|
Pair("Fantasy", "10"),
|
||||||
|
Pair("Girls Love", "26"),
|
||||||
|
Pair("Gourmet", "47"),
|
||||||
|
Pair("Horror", "14"),
|
||||||
|
Pair("Mystery", "7"),
|
||||||
|
Pair("Romance", "22"),
|
||||||
|
Pair("Sci-Fi", "24"),
|
||||||
|
Pair("Slice of Life", "36"),
|
||||||
|
Pair("Sports", "30"),
|
||||||
|
Pair("Supernatural", "37"),
|
||||||
|
Pair("Suspense", "45"),
|
||||||
|
Pair("Ecchi", "9"),
|
||||||
|
Pair("Erotica", "49"),
|
||||||
|
Pair("Hentai", "12"),
|
||||||
|
Pair("Adult Cast", "50"),
|
||||||
|
Pair("Anthropomorphic", "51"),
|
||||||
|
Pair("CGDCT", "52"),
|
||||||
|
Pair("Childcare", "53"),
|
||||||
|
Pair("Combat Sports", "54"),
|
||||||
|
Pair("Crossdressing", "44"),
|
||||||
|
Pair("Delinquents", "55"),
|
||||||
|
Pair("Detective", "39"),
|
||||||
|
Pair("Educational", "56"),
|
||||||
|
Pair("Gag Humor", "57"),
|
||||||
|
Pair("Gore", "58"),
|
||||||
|
Pair("Harem", "35"),
|
||||||
|
Pair("High Stakes Game", "59"),
|
||||||
|
Pair("Historical", "13"),
|
||||||
|
Pair("Idols (Female)", "60"),
|
||||||
|
Pair("Idols (Male)", "61"),
|
||||||
|
Pair("Isekai", "62"),
|
||||||
|
Pair("Iyashikei", "63"),
|
||||||
|
Pair("Love Polygon", "64"),
|
||||||
|
Pair("Magical Sex Shift", "65"),
|
||||||
|
Pair("Mahou Shoujo", "66"),
|
||||||
|
Pair("Martial Arts", "17"),
|
||||||
|
Pair("Mecha", "18"),
|
||||||
|
Pair("Medical", "67"),
|
||||||
|
Pair("Memoir", "68"),
|
||||||
|
Pair("Military", "38"),
|
||||||
|
Pair("Music", "19"),
|
||||||
|
Pair("Mythology", "6"),
|
||||||
|
Pair("Organized Crime", "69"),
|
||||||
|
Pair("Otaku Culture", "70"),
|
||||||
|
Pair("Parody", "20"),
|
||||||
|
Pair("Performing Arts", "71"),
|
||||||
|
Pair("Pets", "72"),
|
||||||
|
Pair("Psychological", "40"),
|
||||||
|
Pair("Racing", "3"),
|
||||||
|
Pair("Reincarnation", "73"),
|
||||||
|
Pair("Reverse Harem", "74"),
|
||||||
|
Pair("Romantic Subtext", "75"),
|
||||||
|
Pair("Samurai", "21"),
|
||||||
|
Pair("School", "23"),
|
||||||
|
Pair("Showbiz", "76"),
|
||||||
|
Pair("Space", "29"),
|
||||||
|
Pair("Strategy Game", "11"),
|
||||||
|
Pair("Super Power", "31"),
|
||||||
|
Pair("Survival", "77"),
|
||||||
|
Pair("Team Sports", "78"),
|
||||||
|
Pair("Time Travel", "79"),
|
||||||
|
Pair("Vampire", "32"),
|
||||||
|
Pair("Video Game", "80"),
|
||||||
|
Pair("Villainess", "81"),
|
||||||
|
Pair("Visual Arts", "82"),
|
||||||
|
Pair("Workplace", "48"),
|
||||||
|
Pair("Josei", "42"),
|
||||||
|
Pair("Kids", "15"),
|
||||||
|
Pair("Seinen", "41"),
|
||||||
|
Pair("Shoujo", "25"),
|
||||||
|
Pair("Shounen", "27"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class StatusFilter : UriMultiSelectFilter(
|
||||||
|
"Status",
|
||||||
|
"status[]",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Ongoing", "0"),
|
||||||
|
Pair("Completed", "1"),
|
||||||
|
),
|
||||||
|
)
|
|
@ -0,0 +1,356 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.mangatop
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
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.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MangaTop : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val name = "MangaTop"
|
||||||
|
|
||||||
|
override val baseUrl = "https://mangatop.to"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.addInterceptor(::tokenInterceptor)
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||||
|
|
||||||
|
private var storedToken: String? = null
|
||||||
|
|
||||||
|
// From Akuma
|
||||||
|
private fun tokenInterceptor(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
|
||||||
|
if (request.method == "POST" && request.header("X-CSRF-TOKEN") == null) {
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
val token = getToken()
|
||||||
|
val response = chain.proceed(
|
||||||
|
newRequest
|
||||||
|
.addHeader("X-CSRF-TOKEN", token)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.code == 419) {
|
||||||
|
response.close()
|
||||||
|
storedToken = null // reset the token
|
||||||
|
val newToken = getToken()
|
||||||
|
return chain.proceed(
|
||||||
|
newRequest
|
||||||
|
.addHeader("X-CSRF-TOKEN", newToken)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getToken(): String {
|
||||||
|
if (storedToken.isNullOrEmpty()) {
|
||||||
|
val request = GET(baseUrl, headers)
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
val document = response.asJsoup()
|
||||||
|
document.updateToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
return storedToken!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
document.updateToken()
|
||||||
|
|
||||||
|
val mangaList = document.select(popularMangaSelector())
|
||||||
|
.map(::popularMangaFromElement)
|
||||||
|
|
||||||
|
return MangasPage(mangaList, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector(): String = "aside div > article"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
|
thumbnail_url = element.selectFirst("img")!!.imgAttr()
|
||||||
|
with(element.selectFirst("a:has(h3)")!!) {
|
||||||
|
setUrlWithoutDomain(attr("abs:href"))
|
||||||
|
title = text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String? = null
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest?page=$page", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
document.updateToken()
|
||||||
|
|
||||||
|
val mangaList = document.select(latestUpdatesSelector())
|
||||||
|
.map(::latestUpdatesFromElement)
|
||||||
|
|
||||||
|
val hasNextPage = document.selectFirst(latestUpdatesNextPageSelector()) != null
|
||||||
|
|
||||||
|
return MangasPage(mangaList, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String = "div > article.manga-item"
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||||
|
popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String = "ul.pagination > li.active + li:has(a)"
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val filterList = filters.ifEmpty { getFilterList() }
|
||||||
|
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||||
|
addQueryParameter("q", query)
|
||||||
|
filterList.filterIsInstance<UriFilter>().forEach {
|
||||||
|
it.addToUri(this)
|
||||||
|
}
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage =
|
||||||
|
latestUpdatesParse(response)
|
||||||
|
|
||||||
|
override fun searchMangaSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector(): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Filters ==============================
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList = FilterList(
|
||||||
|
TypeFilter(),
|
||||||
|
GenreFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// =========================== Manga Details ============================
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||||
|
thumbnail_url = document.selectFirst("picture img")!!.imgAttr()
|
||||||
|
with(document.selectFirst(".manga-info")!!) {
|
||||||
|
title = selectFirst("h1.page-heading")!!.text()
|
||||||
|
author = selectFirst("ul > li:has(span:contains(Authors))")?.ownText()
|
||||||
|
genre = select("ul > li:has(span:contains(Genres)) a").joinToString { it.text() }
|
||||||
|
status = selectFirst(".text-info").parseStatus()
|
||||||
|
description = selectFirst("#manga-description")?.text()
|
||||||
|
?.split(".")
|
||||||
|
?.filterNot { it.contains("MangaTop") }
|
||||||
|
?.joinToString(".")
|
||||||
|
?.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||||
|
"ongoing" -> SManga.ONGOING
|
||||||
|
"completed" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Chapters ==============================
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
document.updateToken()
|
||||||
|
|
||||||
|
val mangaName = document.selectFirst("script:containsData(mangaName)")
|
||||||
|
?.data()
|
||||||
|
?.substringAfter("mangaName")
|
||||||
|
?.substringAfter("'")
|
||||||
|
?.substringBefore("'")
|
||||||
|
?: throw Exception("Failed to get form data")
|
||||||
|
|
||||||
|
val postHeaders = apiHeadersBuilder().apply {
|
||||||
|
set("Referer", response.request.url.toString())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val postBody = FormBody.Builder().apply {
|
||||||
|
add("mangaIdx", response.request.url.toString().substringAfterLast("-"))
|
||||||
|
add("mangaName", mangaName)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val postResponse = client.newCall(
|
||||||
|
POST("$baseUrl/chapter-list", postHeaders, postBody),
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
return super.chapterListParse(postResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "li"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||||
|
element.selectFirst(".text-muted")?.also {
|
||||||
|
date_upload = it.text().parseDate()
|
||||||
|
}
|
||||||
|
name = element.selectFirst("span:not(.text-muted)")!!.text()
|
||||||
|
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.parseDate(): Long {
|
||||||
|
return if (this.contains("ago")) {
|
||||||
|
this.parseRelativeDate()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
dateFormat.parse(this)!!.time
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
val relativeDate = this.split(" ").firstOrNull()
|
||||||
|
?.toIntOrNull()
|
||||||
|
?: return 0L
|
||||||
|
|
||||||
|
when {
|
||||||
|
"second" in this -> now.add(Calendar.SECOND, -relativeDate) // parse: 30 seconds ago
|
||||||
|
"minute" in this -> now.add(Calendar.MINUTE, -relativeDate) // parses: "42 minutes ago"
|
||||||
|
"hour" in this -> now.add(Calendar.HOUR, -relativeDate) // parses: "1 hour ago" and "2 hours ago"
|
||||||
|
"day" in this -> now.add(Calendar.DAY_OF_YEAR, -relativeDate) // parses: "2 days ago"
|
||||||
|
"week" in this -> now.add(Calendar.WEEK_OF_YEAR, -relativeDate) // parses: "2 weeks ago"
|
||||||
|
"month" in this -> now.add(Calendar.MONTH, -relativeDate) // parses: "2 months ago"
|
||||||
|
"year" in this -> now.add(Calendar.YEAR, -relativeDate) // parse: "2 years ago"
|
||||||
|
}
|
||||||
|
return now.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Pages ================================
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val chapterId = chapter.url.substringBeforeLast(".html")
|
||||||
|
.substringAfterLast("-")
|
||||||
|
|
||||||
|
val postHeaders = apiHeadersBuilder().apply {
|
||||||
|
set("Referer", baseUrl + chapter.url)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val postBody = FormBody.Builder().apply {
|
||||||
|
add("chapterIdx", chapterId)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return POST("$baseUrl/chapter-resources", postHeaders, postBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageListResponse(
|
||||||
|
val data: PageListDataDto,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class PageListDataDto(
|
||||||
|
val resources: List<PageDto>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class PageDto(
|
||||||
|
val name: Int,
|
||||||
|
val thumb: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
return response.parseAs<PageListResponse>().data.resources.map {
|
||||||
|
Page(it.name, imageUrl = it.thumb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page): Request {
|
||||||
|
val imgHeaders = headersBuilder().apply {
|
||||||
|
add("Accept", "image/avif,image/webp,*/*")
|
||||||
|
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(page.imageUrl!!, imgHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
private fun Document.updateToken() {
|
||||||
|
storedToken = this.selectFirst("head meta[name*=csrf-token]")
|
||||||
|
?.attr("content")
|
||||||
|
?: throw IOException("Failed to update token")
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T = use {
|
||||||
|
json.decodeFromStream(it.body.byteStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun apiHeadersBuilder() = headersBuilder().apply {
|
||||||
|
add("Accept", "*/*")
|
||||||
|
add("Host", baseUrl.toHttpUrl().host)
|
||||||
|
add("Origin", baseUrl)
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Element.imgAttr(): String = when {
|
||||||
|
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
|
else -> attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'ManhuaPlus (unoriginal)'
|
extName = 'ManhuaPlus (unoriginal)'
|
||||||
extClass = '.ManhuaPlusOrg'
|
extClass = '.ManhuaPlusOrg'
|
||||||
extVersionCode = 1
|
themePkg = 'liliana'
|
||||||
|
baseUrl = 'https://manhuaplus.org'
|
||||||
|
overrideVersionCode = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,242 +1,9 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
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.ParsedHttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class ManhuaPlusOrg : ParsedHttpSource() {
|
class ManhuaPlusOrg : Liliana(
|
||||||
|
"ManhuaPlus (Unoriginal)",
|
||||||
override val name = "ManhuaPlus (Unoriginal)"
|
"https://manhuaplus.org",
|
||||||
|
"en",
|
||||||
override val baseUrl = "https://manhuaplus.org"
|
)
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimit(1)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers)
|
|
||||||
|
|
||||||
override fun popularMangaSelector(): String = "div#main div.grid > div"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
|
||||||
thumbnail_url = element.selectFirst("img")?.imgAttr()
|
|
||||||
element.selectFirst(".text-center a")!!.run {
|
|
||||||
title = text().trim()
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span"
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/all-manga/$page/?sort=1", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
addPathSegment("search")
|
|
||||||
addQueryParameter("keyword", query)
|
|
||||||
} else {
|
|
||||||
addPathSegment("filter")
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is GenreFilter -> {
|
|
||||||
if (filter.checked.isNotEmpty()) {
|
|
||||||
addQueryParameter("genres", filter.checked.joinToString(","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is StatusFilter -> {
|
|
||||||
if (filter.selected.isNotBlank()) {
|
|
||||||
addQueryParameter("status", filter.selected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is SortFilter -> {
|
|
||||||
addQueryParameter("sort", filter.selected)
|
|
||||||
}
|
|
||||||
is ChapterCountFilter -> {
|
|
||||||
addQueryParameter("chapter_count", filter.selected)
|
|
||||||
}
|
|
||||||
is GenderFilter -> {
|
|
||||||
addQueryParameter("sex", filter.selected)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addPathSegment(page.toString())
|
|
||||||
addPathSegment("")
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
|
||||||
|
|
||||||
override fun searchMangaSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector(): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList(
|
|
||||||
Filter.Header("Ignored when using text search"),
|
|
||||||
Filter.Separator(),
|
|
||||||
GenreFilter(),
|
|
||||||
ChapterCountFilter(),
|
|
||||||
GenderFilter(),
|
|
||||||
StatusFilter(),
|
|
||||||
SortFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
|
||||||
description = document.selectFirst("div#syn-target")?.text()
|
|
||||||
thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr()
|
|
||||||
title = document.selectFirst(".a2 header h1")?.text()?.trim() ?: "N/A"
|
|
||||||
genre = document.select(".a2 div > a[rel='tag'].label").joinToString(", ") { it.text() }
|
|
||||||
|
|
||||||
document.selectFirst(".a1 > aside")?.run {
|
|
||||||
author = select("div:contains(Authors) > span a")
|
|
||||||
.joinToString(", ") { it.text().trim() }
|
|
||||||
.takeUnless { it.isBlank() || it.equals("Updating", true) }
|
|
||||||
status = selectFirst("div:contains(Status) > span")?.text().let(::parseStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String?): Int = when {
|
|
||||||
status.equals("ongoing", true) -> SManga.ONGOING
|
|
||||||
status.equals("completed", true) -> SManga.COMPLETED
|
|
||||||
status.equals("on-hold", true) -> SManga.ON_HIATUS
|
|
||||||
status.equals("canceled", true) -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "ul > li.chapter"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
|
||||||
element.selectFirst("time[datetime]")?.also {
|
|
||||||
date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L
|
|
||||||
}
|
|
||||||
element.selectFirst("a")!!.run {
|
|
||||||
text().trim().also {
|
|
||||||
name = it
|
|
||||||
chapter_number = it.substringAfter("hapter ").toFloatOrNull() ?: 0F
|
|
||||||
}
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
|
|
||||||
|
|
||||||
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data()
|
|
||||||
|
|
||||||
val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
|
||||||
|
|
||||||
val pageHeaders = headersBuilder().apply {
|
|
||||||
add("Accept", "application/json, text/javascript, *//*; q=0.01")
|
|
||||||
add("Host", baseUrl.toHttpUrl().host)
|
|
||||||
add("Referer", baseUrl + chapter.url)
|
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET("$baseUrl/ajax/image/list/chap/$id", pageHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PageListResponseDto(val html: String)
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val data = response.parseAs<PageListResponseDto>().html
|
|
||||||
return pageListParse(
|
|
||||||
Jsoup.parseBodyFragment(
|
|
||||||
data,
|
|
||||||
response.request.header("Referer")!!,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
return document.select("div.separator").map { page ->
|
|
||||||
val index = page.selectFirst("img")!!.attr("alt").substringAfterLast(" ").toInt()
|
|
||||||
val url = page.selectFirst("a")!!.attr("abs:href")
|
|
||||||
Page(index, document.location(), url)
|
|
||||||
}.sortedBy { it.index }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
val imgHeaders = headersBuilder().apply {
|
|
||||||
add("Accept", "image/avif,image/webp,*/*")
|
|
||||||
add("Host", page.imageUrl!!.toHttpUrl().host)
|
|
||||||
}.build()
|
|
||||||
return GET(page.imageUrl!!, imgHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilities
|
|
||||||
|
|
||||||
// From mangathemesia
|
|
||||||
private fun Element.imgAttr(): String = when {
|
|
||||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
|
||||||
else -> attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
|
||||||
return json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
|
|
||||||
abstract class SelectFilter(
|
|
||||||
name: String,
|
|
||||||
private val options: List<Pair<String, String>>,
|
|
||||||
) : Filter.Select<String>(
|
|
||||||
name,
|
|
||||||
options.map { it.first }.toTypedArray(),
|
|
||||||
) {
|
|
||||||
val selected get() = options[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
class CheckBoxFilter(
|
|
||||||
name: String,
|
|
||||||
val value: String,
|
|
||||||
) : Filter.CheckBox(name)
|
|
||||||
|
|
||||||
class ChapterCountFilter : SelectFilter("Chapter count", chapterCount) {
|
|
||||||
companion object {
|
|
||||||
private val chapterCount = listOf(
|
|
||||||
Pair(">= 0", "0"),
|
|
||||||
Pair(">= 10", "10"),
|
|
||||||
Pair(">= 30", "30"),
|
|
||||||
Pair(">= 50", "50"),
|
|
||||||
Pair(">= 100", "100"),
|
|
||||||
Pair(">= 200", "200"),
|
|
||||||
Pair(">= 300", "300"),
|
|
||||||
Pair(">= 400", "400"),
|
|
||||||
Pair(">= 500", "500"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenderFilter : SelectFilter("Manga Gender", gender) {
|
|
||||||
companion object {
|
|
||||||
private val gender = listOf(
|
|
||||||
Pair("All", "All"),
|
|
||||||
Pair("Boy", "Boy"),
|
|
||||||
Pair("Girl", "Girl"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusFilter : SelectFilter("Status", status) {
|
|
||||||
companion object {
|
|
||||||
private val status = listOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("Completed", "completed"),
|
|
||||||
Pair("OnGoing", "on-going"),
|
|
||||||
Pair("On-Hold", "on-hold"),
|
|
||||||
Pair("Canceled", "canceled"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SortFilter : SelectFilter("Sort", sort) {
|
|
||||||
companion object {
|
|
||||||
private val sort = listOf(
|
|
||||||
Pair("Default", "default"),
|
|
||||||
Pair("Latest Updated", "latest-updated"),
|
|
||||||
Pair("Most Viewed", "views"),
|
|
||||||
Pair("Most Viewed Month", "views_month"),
|
|
||||||
Pair("Most Viewed Week", "views_week"),
|
|
||||||
Pair("Most Viewed Day", "views_day"),
|
|
||||||
Pair("Score", "score"),
|
|
||||||
Pair("Name A-Z", "az"),
|
|
||||||
Pair("Name Z-A", "za"),
|
|
||||||
Pair("Newest", "new"),
|
|
||||||
Pair("Oldest", "old"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GenreFilter : Filter.Group<CheckBoxFilter>(
|
|
||||||
"Genre",
|
|
||||||
genres.map { CheckBoxFilter(it.first, it.second) },
|
|
||||||
) {
|
|
||||||
val checked get() = state.filter { it.state }.map { it.value }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val genres = listOf(
|
|
||||||
Pair("Action", "4"),
|
|
||||||
Pair("Adaptation", "87"),
|
|
||||||
Pair("Adult", "31"),
|
|
||||||
Pair("Adventure", "5"),
|
|
||||||
Pair("Animals", "1657"),
|
|
||||||
Pair("Cartoon", "46"),
|
|
||||||
Pair("Comedy", "14"),
|
|
||||||
Pair("Demons", "284"),
|
|
||||||
Pair("Drama", "59"),
|
|
||||||
Pair("Ecchi", "67"),
|
|
||||||
Pair("Fantasy", "6"),
|
|
||||||
Pair("Full Color", "89"),
|
|
||||||
Pair("Genderswap", "2409"),
|
|
||||||
Pair("Ghosts", "2253"),
|
|
||||||
Pair("Gore", "1182"),
|
|
||||||
Pair("Harem", "17"),
|
|
||||||
Pair("Historical", "642"),
|
|
||||||
Pair("Horror", "797"),
|
|
||||||
Pair("Isekai", "239"),
|
|
||||||
Pair("Live action", "11"),
|
|
||||||
Pair("Long Strip", "86"),
|
|
||||||
Pair("Magic", "90"),
|
|
||||||
Pair("Magical Girls", "1470"),
|
|
||||||
Pair("Manhua", "7"),
|
|
||||||
Pair("Manhwa", "70"),
|
|
||||||
Pair("Martial Arts", "8"),
|
|
||||||
Pair("Mature", "12"),
|
|
||||||
Pair("Mecha", "786"),
|
|
||||||
Pair("Medical", "1443"),
|
|
||||||
Pair("Monsters", "138"),
|
|
||||||
Pair("Mystery", "9"),
|
|
||||||
Pair("Post-Apocalyptic", "285"),
|
|
||||||
Pair("Psychological", "798"),
|
|
||||||
Pair("Reincarnation", "139"),
|
|
||||||
Pair("Romance", "987"),
|
|
||||||
Pair("School Life", "10"),
|
|
||||||
Pair("Sci-fi", "135"),
|
|
||||||
Pair("Seinen", "196"),
|
|
||||||
Pair("Shounen", "26"),
|
|
||||||
Pair("Shounen ai", "64"),
|
|
||||||
Pair("Slice of Life", "197"),
|
|
||||||
Pair("Superhero", "136"),
|
|
||||||
Pair("Supernatural", "13"),
|
|
||||||
Pair("Survival", "140"),
|
|
||||||
Pair("Thriller", "137"),
|
|
||||||
Pair("Time travel", "231"),
|
|
||||||
Pair("Tragedy", "15"),
|
|
||||||
Pair("Video Games", "283"),
|
|
||||||
Pair("Villainess", "676"),
|
|
||||||
Pair("Virtual Reality", "611"),
|
|
||||||
Pair("Web comic", "88"),
|
|
||||||
Pair("Webtoon", "18"),
|
|
||||||
Pair("Wuxia", "239"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
ext {
|
||||||
|
extName = 'Meow Meow Comics'
|
||||||
|
extClass = '.MeowMeowComics'
|
||||||
|
themePkg = 'madara'
|
||||||
|
baseUrl = 'https://meowmeowcomics.com'
|
||||||
|
overrideVersionCode = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,34 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.meowmeowcomics
|
||||||
|
|
||||||
|
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 eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class MeowMeowComics : Madara(
|
||||||
|
"Meow Meow Comics",
|
||||||
|
"https://meowmeowcomics.com",
|
||||||
|
"en",
|
||||||
|
) {
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// ============================== Chapters ==============================
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
return xhrChaptersRequest(baseUrl + manga.url.removeSuffix("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
return response.asJsoup()
|
||||||
|
.select("ul.main > li.parent,ul.main:not(:has(>li.parent))")
|
||||||
|
.sortedByDescending { it.selectFirst("a.has-child")?.text()?.toIntOrNull() ?: 0 }
|
||||||
|
.flatMap { season ->
|
||||||
|
season.select(chapterListSelector()).map(::chapterFromElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Scylla Scans'
|
extName = 'Scylla Comics'
|
||||||
extClass = '.ScyllaScans'
|
extClass = '.ScyllaComics'
|
||||||
themePkg = 'fuzzydoodle'
|
themePkg = 'fuzzydoodle'
|
||||||
overrideVersionCode = 9
|
overrideVersionCode = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -2,10 +2,13 @@ package eu.kanade.tachiyomi.extension.en.scyllascans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle
|
import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle
|
||||||
|
|
||||||
class ScyllaScans : FuzzyDoodle("Scylla Scans", "https://scyllascans.org", "en") {
|
class ScyllaComics : FuzzyDoodle("Scylla Comics", "https://scyllacomics.xyz", "en") {
|
||||||
|
|
||||||
// readerfront -> fuzzydoodle
|
// readerfront -> fuzzydoodle
|
||||||
override val versionId = 2
|
override val versionId = 2
|
||||||
|
|
||||||
|
// Scylla Scans -> Scylla Comics
|
||||||
|
override val id = 9064193520444097799
|
||||||
|
|
||||||
override val latestFromHomePage = true
|
override val latestFromHomePage = true
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'TCB Scans'
|
extName = 'TCB Scans'
|
||||||
extClass = '.TCBScans'
|
extClass = '.TCBScans'
|
||||||
extVersionCode = 6
|
extVersionCode = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -22,7 +22,7 @@ import uy.kohesive.injekt.api.get
|
||||||
class TCBScans : ParsedHttpSource() {
|
class TCBScans : ParsedHttpSource() {
|
||||||
|
|
||||||
override val name = "TCB Scans"
|
override val name = "TCB Scans"
|
||||||
override val baseUrl = "https://onepiecechapters.com"
|
override val baseUrl = "https://tcbscans.com"
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.TheBlank'
|
extClass = '.TheBlank'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://theblank.net'
|
baseUrl = 'https://theblank.net'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 1
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,4 +18,5 @@ class TheBlank : Madara(
|
||||||
|
|
||||||
override val useNewChapterEndpoint = true
|
override val useNewChapterEndpoint = true
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||||
|
override fun chapterListSelector() = "li.wp-manga-chapter:not(.vip-permission)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Last Knight Translation'
|
extName = 'ZinChanManga.com'
|
||||||
extClass = '.LKScanlation'
|
extClass = '.ZinChanMangaCom'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://lkscanlation.com'
|
baseUrl = 'https://zinchanmanga.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 0
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.8 KiB |