Compare commits

..

No commits in common. "bbb2922515d7d478cc2e0f5363e8e49a9eac921d" and "d726f9eee101a3560af53c05b34c89653feabd24" have entirely different histories.

277 changed files with 1763 additions and 2441 deletions

View File

@ -45,7 +45,7 @@ jobs:
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
- name: Set up Gradle
uses: gradle/actions/setup-gradle@6cec5d49d4d6d4bb982fbed7047db31ea6d38f11 # v3.3.0
uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0
- name: Build extensions
env:

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,90 +0,0 @@
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",
)

View File

@ -1,353 +0,0 @@
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())
}
}

View File

@ -1,8 +0,0 @@
ext {
extName = 'CosplayTele'
extClass = '.CosplayTele'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,228 +0,0 @@
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)
}
}
}

View File

@ -1,9 +1,9 @@
ext {
extName = 'SCARManga'
extClass = '.ScarManga'
extName = 'ARESNOV'
extClass = '.ARESNOV'
themePkg = 'mangathemesia'
baseUrl = 'https://scarmanga.com'
overrideVersionCode = 2
baseUrl = 'https://manhuascarlet.com'
overrideVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,41 @@
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)
}
}
}

View File

@ -1,15 +0,0 @@
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
}

View File

@ -0,0 +1,9 @@
ext {
extName = 'Hizomanga'
extClass = '.Hizomanga'
themePkg = 'madara'
baseUrl = 'https://hizomanga.me'
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.ar.hizomanga
import eu.kanade.tachiyomi.multisrc.madara.Madara
class Hizomanga : Madara("Hizomanga", "https://hizomanga.me", "ar")

View File

@ -2,8 +2,8 @@ ext {
extName = 'Manga Flame'
extClass = '.MangaFlame'
themePkg = 'mangathemesia'
baseUrl = 'https://mangaflame.org'
overrideVersionCode = 3
baseUrl = 'https://arisescans.com'
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

@ -1,14 +1,20 @@
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://mangaflame.org",
"https://arisescans.com",
"ar",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("ar")),
) {
override val id = 1501237443119573205
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.readTimeout(3, TimeUnit.MINUTES)
.build()
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.KingOfManga'
themePkg = 'mangathemesia'
baseUrl = 'https://king-ofmanga.com'
overrideVersionCode = 0
overrideVersionCode = 4
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,15 @@
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
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Anchira'
extClass = '.Anchira'
extVersionCode = 12
extVersionCode = 11
isNsfw = true
}

View File

@ -82,11 +82,8 @@ class Anchira : HttpSource(), ConfigurableSource {
url = "/g/${it.id}/${it.key}"
title = it.title
thumbnail_url = "$cdnUrl/${it.id}/${it.key}/m/${it.thumbnailIndex + 1}"
val art = it.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
.ifEmpty { null }
artist = art
artist = it.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
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
@ -243,11 +240,8 @@ class Anchira : HttpSource(), ConfigurableSource {
title = data.title
thumbnail_url =
"$cdnUrl/${data.id}/${data.key}/b/${data.thumbnailIndex + 1}"
val art = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
.ifEmpty { null }
artist = art
artist = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name }
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
@ -404,7 +398,7 @@ class Anchira : HttpSource(), ConfigurableSource {
private class SortFilter : Filter.Sort(
"Sort",
arrayOf("Title", "Pages", "Date uploaded", "Date published", "Popularity"),
arrayOf("Title", "Pages", "Date published", "Date uploaded", "Popularity"),
Selection(2, false),
)

View File

@ -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 = 0,
@SerialName("thumb_index") val thumbnailIndex: Int = 1,
val tags: List<Tag> = emptyList(),
val url: String? = null,
val pages: Int = 1,

View File

@ -1,7 +1,9 @@
ext {
extName = 'Vortex Scans'
extClass = '.VortexScans'
extVersionCode = 31
extName = 'Arven Scans'
extClass = '.ArvenScans'
themePkg = 'mangathemesia'
baseUrl = 'https://arvenscans.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,13 @@
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()
}

View File

@ -1,113 +0,0 @@
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)

View File

@ -1,101 +0,0 @@
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"),
),
)

View File

@ -1,145 +0,0 @@
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

View File

@ -1,9 +0,0 @@
ext {
extName = 'Blazescans'
extClass = '.Blazescans'
themePkg = 'mangathemesia'
baseUrl = 'https://blazescans.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,11 +0,0 @@
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()
}

View File

@ -1,9 +1,9 @@
ext {
extName = 'Manhuagold'
extClass = '.Manhuagold'
themePkg = 'liliana'
baseUrl = 'https://manhuagold.top'
overrideVersionCode = 34
themePkg = 'mangareader'
baseUrl = 'https://manhuagold.com'
overrideVersionCode = 33
isNsfw = true
}

View File

@ -1,18 +1,233 @@
package eu.kanade.tachiyomi.extension.en.comickiba
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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 : Liliana(
"Manhuagold",
"https://manhuagold.top",
"en",
usesPostSearch = true,
) {
// MangaReader -> Liliana
override val versionId = 2
class Manhuagold : MangaReader() {
override val client = super.client.newBuilder()
override val name = "Manhuagold"
override val lang = "en"
override val baseUrl = "https://manhuagold.com"
override val client = network.cloudflareClient.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)
}
}

View File

@ -1,9 +1,9 @@
ext {
extName = 'Drake Scans'
extClass = '.DrakeScans'
themePkg = 'mangathemesia'
themePkg = 'madara'
baseUrl = 'https://drakescans.com'
overrideVersionCode = 10
overrideVersionCode = 4
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,12 +1,17 @@
package eu.kanade.tachiyomi.extension.en.drakescans
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
import eu.kanade.tachiyomi.multisrc.madara.Madara
import java.text.SimpleDateFormat
import java.util.Locale
class DrakeScans : MangaThemesia(
class DrakeScans : Madara(
"Drake Scans",
"https://drakescans.com",
"en",
SimpleDateFormat("dd/MM/yyyy", Locale.US),
) {
// madara -> mangathemesia
override val versionId = 2
override val mangaDetailsSelectorTag = ""
override val mangaSubString = "series"
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.ElarcPage'
themePkg = 'mangathemesia'
baseUrl = 'https://elarctoons.com'
overrideVersionCode = 6
overrideVersionCode = 5
isNsfw = false
}

View File

@ -52,12 +52,6 @@ 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(),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,5 +0,0 @@
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")

View File

@ -1,8 +0,0 @@
ext {
extName = 'MangaTop'
extClass = '.MangaTop'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,131 +0,0 @@
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"),
),
)

View File

@ -1,356 +0,0 @@
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")
}
}

View File

@ -1,9 +1,7 @@
ext {
extName = 'ManhuaPlus (unoriginal)'
extClass = '.ManhuaPlusOrg'
themePkg = 'liliana'
baseUrl = 'https://manhuaplus.org'
overrideVersionCode = 1
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

View File

@ -1,9 +1,242 @@
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
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
class ManhuaPlusOrg : Liliana(
"ManhuaPlus (Unoriginal)",
"https://manhuaplus.org",
"en",
)
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())
}
}

View File

@ -0,0 +1,139 @@
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"),
)
}
}

View File

@ -1,9 +0,0 @@
ext {
extName = 'Meow Meow Comics'
extClass = '.MeowMeowComics'
themePkg = 'madara'
baseUrl = 'https://meowmeowcomics.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,34 +0,0 @@
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)
}
}
}

View File

@ -1,8 +1,8 @@
ext {
extName = 'Scylla Comics'
extClass = '.ScyllaComics'
extName = 'Scylla Scans'
extClass = '.ScyllaScans'
themePkg = 'fuzzydoodle'
overrideVersionCode = 11
overrideVersionCode = 9
}
apply from: "$rootDir/common.gradle"

View File

@ -2,13 +2,10 @@ package eu.kanade.tachiyomi.extension.en.scyllascans
import eu.kanade.tachiyomi.multisrc.fuzzydoodle.FuzzyDoodle
class ScyllaComics : FuzzyDoodle("Scylla Comics", "https://scyllacomics.xyz", "en") {
class ScyllaScans : FuzzyDoodle("Scylla Scans", "https://scyllascans.org", "en") {
// readerfront -> fuzzydoodle
override val versionId = 2
// Scylla Scans -> Scylla Comics
override val id = 9064193520444097799
override val latestFromHomePage = true
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'TCB Scans'
extClass = '.TCBScans'
extVersionCode = 7
extVersionCode = 6
}
apply from: "$rootDir/common.gradle"

View File

@ -22,7 +22,7 @@ import uy.kohesive.injekt.api.get
class TCBScans : ParsedHttpSource() {
override val name = "TCB Scans"
override val baseUrl = "https://tcbscans.com"
override val baseUrl = "https://onepiecechapters.com"
override val lang = "en"
override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient

View File

@ -3,7 +3,7 @@ ext {
extClass = '.TheBlank'
themePkg = 'madara'
baseUrl = 'https://theblank.net'
overrideVersionCode = 1
overrideVersionCode = 0
isNsfw = true
}

View File

@ -18,5 +18,4 @@ class TheBlank : Madara(
override val useNewChapterEndpoint = true
override val useLoadMoreRequest = LoadMoreStrategy.Always
override fun chapterListSelector() = "li.wp-manga-chapter:not(.vip-permission)"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Some files were not shown because too many files have changed in this diff Show More