Compare commits

...

48 Commits

Author SHA1 Message Date
WarmSeeker6 bbb2922515 HentaiVN: Change URL (#2457)
CI / Prepare job (push) Successful in 5s Details
CI / Build individual modules (push) Successful in 3m2s Details
CI / Publish repo (push) Successful in 41s Details
2024-04-18 14:46:00 +01:00
lamaxama 597f1ad8d1 Elarc Toon: Skip responses that do not start with "text/html" (#2462)
* Elarc Toon: Skip responses that do not start with "text/html"

* Update build.gradle

* format
2024-04-18 14:46:00 +01:00
Chopper 4c13971c15 Manhastro: Fix date format (#2460)
Fix date format
2024-04-18 14:46:00 +01:00
Chopper 64659b0f11 Add OnePieceTeca (#2449)
* Add OnePieceTeca

* Remove override client

* Remove unused import
2024-04-18 14:46:00 +01:00
BrutuZ c411229164 Anchira: Non-empty Author field (#2448)
Don't pass empty string for Author
* Fix swapped sort filter
* When the cover offset is undefined, assume first page instead of second
2024-04-18 14:46:00 +01:00
kana-shii d86505e788 add Coven Scans (#2392)
* add Coven Scans

* fix coven scan

* add coven scans icon
2024-04-18 14:46:00 +01:00
bapeey 9aecf7e174 MangaOni: Fix bad base64 (#2439)
fix bad base64
2024-04-18 14:46:00 +01:00
bapeey 9385d11c43 Remove dead sources (#2434)
* Remove Mangadoor

Id: 5598298311107274167

* Remove Last Knight Translation

Id: 4608749082635217570

* Remove TuMangaOnline.site

Id: 8850093899291265785

* Remove Unitoon Oficial

Id: 2422714322713320966

* Remove Shayami

Id: 3984924321110067740

* Remove Eromiau

Id: 7532305797477636173

* Remove Cocorip

Id: 2811607756759454878

* Revert "Remove Cocorip"

This reverts commit c1cf4f5dbd342f3bf808acf3b09a5ea1f4303f59.

* Reapply "Remove Cocorip"

smh i forgot why i remove it

* Revert "Reapply "Remove Cocorip"" XD

This reverts commit 2ad2046289076fa2f126c8e6fc46c82bddb5a9b7.

* Remove MangaPT

ID: 4501201462814418979
2024-04-18 14:46:00 +01:00
bapeey dcf3bde0a4 Add RyujinManga (#2423) 2024-04-18 14:46:00 +01:00
bapeey 26ddebcc20 ManhuaOnline: Add logo and change mangaSubString (#2418)
Add logo and change mangaSubString
2024-04-18 14:46:00 +01:00
Secozzi f0d8933cf8 New multisrc theme: Liliana (#2413)
* new multisrc theme: liliana

* dont specify type

* suggestions

* add raw1001
2024-04-18 14:46:00 +01:00
AwkwardPeak7 9238b633a2 remove HizoManga (#2412)
only contains novels
2024-04-18 14:46:00 +01:00
AwkwardPeak7 96ff217f86 KingOfManga: move to en (#2394)
* KingOfManga: ar -> en

* rename
2024-04-18 14:46:00 +01:00
AwkwardPeak7 7423444c40 Update Actions Dependencies (#2410)
Update Dependencies
2024-04-18 14:45:51 +01:00
Chopper c2a996152e Remove PortugaMangas (#2405) 2024-04-18 14:44:14 +01:00
AwkwardPeak7 7475e54eaa Mode Scanlator: move to HeanCMS (#2393) 2024-04-18 14:44:14 +01:00
AwkwardPeak7 71d2a50a96 add Vortex Scans (#2350)
* add VortexScans

* remove arven

* Revert "remove arven"

This reverts commit 4c67556a6cc71ff9e9f38b26d870f437d01daca6.

* in place update and actual popular

* use next cached thumbnails
2024-04-18 14:44:14 +01:00
bapeey ada1d19b34 MNS: Deobfuscate script (#2409)
* yep

* Update src/es/mangasnosekai/build.gradle
2024-04-18 14:44:14 +01:00
Vetle Ledaal 398e59a3e3 Add ZinChanManga.com (#2384) 2024-04-18 14:44:14 +01:00
AwkwardPeak7 955c098d8e Drake: move to MangaThemesia (#2370) 2024-04-18 14:44:14 +01:00
Vetle Ledaal a009e6b4d1 ARESNOV -> SCARManga: update domain (#2385)
* SCARManga: update domain, icons

* keep id

* remove redundant overrides
2024-04-18 14:44:14 +01:00
Chaos Pjeles 6da99b2e55 Add cosplaytele.com (#2356)
* implement cosplaytele.com

* Update src/all/cosplaytele/build.gradle

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* resolve suggestions

* resolve 2

* Update src/all/cosplaytele/src/eu/kanade/tachiyomi/extension/all/cosplaytele/CosplayTele.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Update src/all/cosplaytele/src/eu/kanade/tachiyomi/extension/all/cosplaytele/CosplayTele.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Update src/all/cosplaytele/src/eu/kanade/tachiyomi/extension/all/cosplaytele/CosplayTele.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Update src/all/cosplaytele/src/eu/kanade/tachiyomi/extension/all/cosplaytele/CosplayTele.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Update src/all/cosplaytele/src/eu/kanade/tachiyomi/extension/all/cosplaytele/CosplayTele.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* fix query

* fix query 2

* update src/all/cosplaytele/src/eu/kanade/tachiyomi/extension/all/cosplaytele/CosplayTele.kt

* Update src/all/cosplaytele/src/eu/kanade/tachiyomi/extension/all/cosplaytele/CosplayTele.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-04-18 14:44:14 +01:00
Hasan 53b99172d4 Add Zenith Scans (#2379)
Co-authored-by: hasanturkylmz <hasanturkylmz@outlook.com>
2024-04-18 14:44:14 +01:00
bapeey 1ad2cfa0e1 TempleScan(es): Fix pages not found (#2382)
fix pages
2024-04-18 14:44:14 +01:00
bapeey 440abc28d8 MNS: Fetch regex and selectors from repository (#2380)
* Use values from repository
2024-04-18 14:44:14 +01:00
bapeey 7c27192ab2 Remove RagnarokScan (#2377)
remove
2024-04-18 14:44:14 +01:00
bapeey 5a118fec87 InariManga: Update domain (#2376)
update domain
2024-04-18 14:44:14 +01:00
kana-shii 2a81be31c1 add XXX Yaoi (#2355)
* add XXX Yaoi

* Update XXXYaoi.kt
2024-04-18 14:44:14 +01:00
bapeey 15b3fc9866 MNS: Fix chapters not found again (#2375)
insane
2024-04-18 14:44:14 +01:00
AwkwardPeak7 8ffd960733 preserve id
oops
2024-04-18 14:44:14 +01:00
AwkwardPeak7 0d3409399b Scylla Comics: update domain (#2364) 2024-04-18 14:44:14 +01:00
Chopper e79e16f7d9 Add MangaTerra (#2300)
* Add MangaTerra

* Cleanup

* Add search

* Cleanup

* Remove override unneeed

* Remove override fetchSearchManga

* Fix search query

* Add fetch manga by slug

* Add method modifies

* Cleanup

* Add srcAttr

* Move filters to file

* Improved pageListParse

* Cleanup

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

* Removes the client instance from the loop

* Fix search by slug

* Fix page index

* Remove custom client

* Fix redirect last page by default and fix last page

* Remove parser type

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Maintains the positions of the method declarations of the ParsedHttpSource class

* Move noRedirectClient to class properties

* Add warning when GenreFiltrs is empty

* Cleanup

* Remove spaces

* Use cloudflareClient

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-04-18 14:44:14 +01:00
bapeey 40619db2b2 Temple Scan (es): change theme (#2363)
* change theme

* Bump versionId
2024-04-18 14:44:14 +01:00
Chopper be7c034bd2 Add MaidSecret (#2354)
* Add MaidSecret

* Add isNsfw

* Add icons

* Set useNewChapterEndpoint to true
2024-04-18 14:44:14 +01:00
bapeey 9d43ba0711 MNS: FIx no chapters found (#2360)
wtf
2024-04-18 14:44:14 +01:00
Norsze 46aa7fd5cf Add Merlin Scans (#2357) 2024-04-18 14:44:14 +01:00
Secozzi 490eab456b add blazescans (#2344)
* add blazescans

* gradle

* add units
2024-04-18 14:44:14 +01:00
Secozzi 485447d7b2 add mangatop (#2336)
* add mangatop

* update token when possible
2024-04-18 14:44:14 +01:00
bapeey fb22115b58 MNS: Fix mangaId (#2353)
fix
2024-04-18 14:44:14 +01:00
Norsze 130a22e847 Add Shijie Scans (#2338) 2024-04-18 14:44:14 +01:00
Vetle Ledaal 13f8712813 Manga Flame: update domain (reverts #1931) (#2335)
* Manga Flame: update domain (reverts #1931)

* remove unused import
2024-04-18 14:44:14 +01:00
Vetle Ledaal ce0bdea748 CBHentai: update domain (#2333) 2024-04-18 14:44:14 +01:00
Norsze 36fc99d48b Fix TCB scans (#2320) 2024-04-18 14:44:14 +01:00
Secozzi 9c5d99e898 add meowmeowcomics (#2329)
* add meowmeowcomics

* fallback
2024-04-18 14:44:14 +01:00
haruki-takeshi 3d0c1ef64e Update NetTruyen Domain (#2328)
* Update NetTruyen.kt

* Update build.gradle
2024-04-18 14:44:14 +01:00
bapeey 885d951788 MNS: Change chapters action (#2326)
Fix
2024-04-18 14:44:14 +01:00
AwkwardPeak7 165d83f01b TheBlank: filter out vip chapters (#2318)
filter out vip chapters
2024-04-18 14:44:13 +01:00
AwkwardPeak7 bb0db200aa add Manhuakey (#2317)
* add ManhuaKey (th)

* newline

* remove log
2024-04-18 14:44:13 +01:00
277 changed files with 2441 additions and 1763 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@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0
uses: gradle/actions/setup-gradle@6cec5d49d4d6d4bb982fbed7047db31ea6d38f11 # v3.3.0
- name: Build extensions
env:

View File

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

View File

@ -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",
)

View File

@ -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())
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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)
}
}
}

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -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)
}
}
}

View File

@ -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
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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")

View File

@ -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"

View File

@ -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()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -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
}

View File

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

View File

@ -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),
)

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

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -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()
}

View File

@ -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)

View File

@ -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"),
),
)

View File

@ -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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -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()
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -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
}

View File

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

View File

@ -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(),

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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")

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -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"),
),
)

View File

@ -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")
}
}

View File

@ -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"

View File

@ -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",
)

View File

@ -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"),
)
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -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)
}
}
}

View File

@ -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"

View File

@ -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
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'TCB Scans'
extClass = '.TCBScans'
extVersionCode = 6
extVersionCode = 7
}
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://onepiecechapters.com"
override val baseUrl = "https://tcbscans.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 = 0
overrideVersionCode = 1
isNsfw = true
}

View File

@ -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)"
}

View File

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

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