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
|
||||
|
||||
- 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
|
||||
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 {
|
||||
extName = 'ARESNOV'
|
||||
extClass = '.ARESNOV'
|
||||
extName = 'SCARManga'
|
||||
extClass = '.ScarManga'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://manhuascarlet.com'
|
||||
overrideVersionCode = 1
|
||||
baseUrl = 'https://scarmanga.com'
|
||||
overrideVersionCode = 2
|
||||
}
|
||||
|
||||
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'
|
||||
extClass = '.MangaFlame'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://arisescans.com'
|
||||
overrideVersionCode = 2
|
||||
baseUrl = 'https://mangaflame.org'
|
||||
overrideVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.mangaflame
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import okhttp3.OkHttpClient
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MangaFlame : MangaThemesia(
|
||||
"Manga Flame",
|
||||
"https://arisescans.com",
|
||||
"https://mangaflame.org",
|
||||
"ar",
|
||||
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
|
||||
) {
|
||||
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 {
|
||||
extName = 'Anchira'
|
||||
extClass = '.Anchira'
|
||||
extVersionCode = 11
|
||||
extVersionCode = 12
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -82,8 +82,11 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
url = "/g/${it.id}/${it.key}"
|
||||
title = it.title
|
||||
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 }
|
||||
.ifEmpty { art }
|
||||
genre = prepareTags(it.tags, preferences.useTagGrouping)
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
status = SManga.COMPLETED
|
||||
|
@ -240,8 +243,11 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
title = data.title
|
||||
thumbnail_url =
|
||||
"$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 }
|
||||
.ifEmpty { art }
|
||||
genre = prepareTags(data.tags, preferences.useTagGrouping)
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
status = SManga.COMPLETED
|
||||
|
@ -398,7 +404,7 @@ class Anchira : HttpSource(), ConfigurableSource {
|
|||
|
||||
private class SortFilter : Filter.Sort(
|
||||
"Sort",
|
||||
arrayOf("Title", "Pages", "Date published", "Date uploaded", "Popularity"),
|
||||
arrayOf("Title", "Pages", "Date uploaded", "Date published", "Popularity"),
|
||||
Selection(2, false),
|
||||
)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ data class Entry(
|
|||
val key: String,
|
||||
@SerialName("published_at") val publishedAt: Long = 0L,
|
||||
val title: String,
|
||||
@SerialName("thumb_index") val thumbnailIndex: Int = 1,
|
||||
@SerialName("thumb_index") val thumbnailIndex: Int = 0,
|
||||
val tags: List<Tag> = emptyList(),
|
||||
val url: String? = null,
|
||||
val pages: Int = 1,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
ext {
|
||||
extName = 'Arven Scans'
|
||||
extClass = '.ArvenScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://arvenscans.com'
|
||||
overrideVersionCode = 0
|
||||
extName = 'Vortex Scans'
|
||||
extClass = '.VortexScans'
|
||||
extVersionCode = 31
|
||||
}
|
||||
|
||||
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 {
|
||||
extName = 'Manhuagold'
|
||||
extClass = '.Manhuagold'
|
||||
themePkg = 'mangareader'
|
||||
baseUrl = 'https://manhuagold.com'
|
||||
overrideVersionCode = 33
|
||||
themePkg = 'liliana'
|
||||
baseUrl = 'https://manhuagold.top'
|
||||
overrideVersionCode = 34
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,233 +1,18 @@
|
|||
package eu.kanade.tachiyomi.extension.en.comickiba
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||
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 lang = "en"
|
||||
|
||||
override val baseUrl = "https://manhuagold.com"
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
.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 {
|
||||
extName = 'Drake Scans'
|
||||
extClass = '.DrakeScans'
|
||||
themePkg = 'madara'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://drakescans.com'
|
||||
overrideVersionCode = 4
|
||||
overrideVersionCode = 10
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
|
||||
class DrakeScans : Madara(
|
||||
class DrakeScans : MangaThemesia(
|
||||
"Drake Scans",
|
||||
"https://drakescans.com",
|
||||
"en",
|
||||
SimpleDateFormat("dd/MM/yyyy", Locale.US),
|
||||
) {
|
||||
|
||||
override val mangaDetailsSelectorTag = ""
|
||||
|
||||
override val mangaSubString = "series"
|
||||
// madara -> mangathemesia
|
||||
override val versionId = 2
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.ElarcPage'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://elarctoons.com'
|
||||
overrideVersionCode = 5
|
||||
overrideVersionCode = 6
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,12 @@ class ElarcPage : MangaThemesia(
|
|||
|
||||
// Always update URL
|
||||
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(
|
||||
response.peekBody(Long.MAX_VALUE).string(),
|
||||
request.url.toString(),
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.KingOfManga'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://king-ofmanga.com'
|
||||
overrideVersionCode = 4
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
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 {
|
||||
extName = 'ManhuaPlus (unoriginal)'
|
||||
extClass = '.ManhuaPlusOrg'
|
||||
extVersionCode = 1
|
||||
themePkg = 'liliana'
|
||||
baseUrl = 'https://manhuaplus.org'
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,242 +1,9 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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
|
||||
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||
|
||||
class ManhuaPlusOrg : ParsedHttpSource() {
|
||||
|
||||
override val name = "ManhuaPlus (Unoriginal)"
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
class ManhuaPlusOrg : Liliana(
|
||||
"ManhuaPlus (Unoriginal)",
|
||||
"https://manhuaplus.org",
|
||||
"en",
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
extName = 'Scylla Scans'
|
||||
extClass = '.ScyllaScans'
|
||||
extName = 'Scylla Comics'
|
||||
extClass = '.ScyllaComics'
|
||||
themePkg = 'fuzzydoodle'
|
||||
overrideVersionCode = 9
|
||||
overrideVersionCode = 11
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -2,10 +2,13 @@ package eu.kanade.tachiyomi.extension.en.scyllascans
|
|||
|
||||
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
|
||||
override val versionId = 2
|
||||
|
||||
// Scylla Scans -> Scylla Comics
|
||||
override val id = 9064193520444097799
|
||||
|
||||
override val latestFromHomePage = true
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'TCB Scans'
|
||||
extClass = '.TCBScans'
|
||||
extVersionCode = 6
|
||||
extVersionCode = 7
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -22,7 +22,7 @@ import uy.kohesive.injekt.api.get
|
|||
class TCBScans : ParsedHttpSource() {
|
||||
|
||||
override val name = "TCB Scans"
|
||||
override val baseUrl = "https://onepiecechapters.com"
|
||||
override val baseUrl = "https://tcbscans.com"
|
||||
override val lang = "en"
|
||||
override val supportsLatest = false
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.TheBlank'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://theblank.net'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -18,4 +18,5 @@ class TheBlank : Madara(
|
|||
|
||||
override val useNewChapterEndpoint = true
|
||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||
override fun chapterListSelector() = "li.wp-manga-chapter:not(.vip-permission)"
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
ext {
|
||||
extName = 'Last Knight Translation'
|
||||
extClass = '.LKScanlation'
|
||||
extName = 'ZinChanManga.com'
|
||||
extClass = '.ZinChanMangaCom'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://lkscanlation.com'
|
||||
baseUrl = 'https://zinchanmanga.com'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.8 KiB |