New multisrc theme: Liliana (#2413)

* new multisrc theme: liliana

* dont specify type

* suggestions

* add raw1001
This commit is contained in:
Secozzi 2024-04-15 10:45:09 +00:00 committed by GitHub
parent 1aca886008
commit c15cfece54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 513 additions and 982 deletions

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

@ -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,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 = 'Manga Koma'
extClass = '.MangaKoma'
themePkg = 'liliana'
baseUrl = 'https://mangakoma01.net'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.ja.mangakoma
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
import eu.kanade.tachiyomi.source.model.Page
import org.jsoup.nodes.Document
class MangaKoma : Liliana("Manga Koma", "https://mangakoma01.net", "ja") {
override fun pageListParse(document: Document): List<Page> {
return document.select("div.separator[data-index]").map { page ->
val index = page.attr("data-index").toInt()
val url = page.selectFirst("a")!!.attr("abs:href")
Page(index, document.location(), url)
}.sortedBy { it.index }
}
}

View File

@ -0,0 +1,9 @@
ext {
extName = 'Raw1001'
extClass = '.Raw1001'
themePkg = 'liliana'
baseUrl = 'https://raw1001.net'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.ja.raw1001
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
class Raw1001 : Liliana("Raw1001", "https://raw1001.net", "ja")

View File

@ -1,7 +1,9 @@
ext {
extName = 'DocTruyen5s'
extClass = '.DocTruyen5s'
extVersionCode = 2
themePkg = 'liliana'
baseUrl = 'https://manga.io.vn'
overrideVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

@ -1,377 +1,5 @@
package eu.kanade.tachiyomi.extension.vi.doctruyen5s
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter
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.source.online.ParsedHttpSource
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl
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 eu.kanade.tachiyomi.multisrc.liliana.Liliana
class DocTruyen5s : ParsedHttpSource() {
override val name = "DocTruyen5s"
override val lang = "vi"
override val baseUrl = "https://manga.io.vn"
override val supportsLatest = true
override val client = network.cloudflareClient
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter/$page/?sort=views_day&chapter_count=0&sex=All", headers)
override fun popularMangaSelector() = "div.Blog section div.grid > div"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
val anchor = element.selectFirst("div.text-center a")!!
setUrlWithoutDomain(anchor.attr("abs:href"))
title = anchor.text()
thumbnail_url = element.selectFirst("img")?.attr("abs:data-src")
}
override fun popularMangaNextPageSelector() = "span.pagecurrent:not(:last-child)"
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter/$page/?sort=latest-updated&chapter_count=0&sex=All", headers)
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = if (query.isNotBlank()) {
"$baseUrl/search/$page/".toHttpUrl().newBuilder().apply {
addQueryParameter("keyword", query)
}.build()
} else {
val builder = "$baseUrl/filter/$page/".toHttpUrl().newBuilder()
(if (filters.isEmpty()) getFilterList() else filters).filterIsInstance<UriFilter>()
.forEach { it.addToUri(builder) }
builder.build()
}
return GET(url, headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("article header h1")!!.text()
author = document.selectFirst("div.y6x11p i.fas.fa-user + span.dt")?.text()
description = document.selectFirst("div#syn-target")?.text()
genre = document.select("a.label[rel=tag]").joinToString { it.text() }
status = when (document.selectFirst("div.y6x11p i.fas.fa-rss + span.dt")?.text()) {
"Đang tiến hành" -> SManga.ONGOING
"Hoàn thành" -> SManga.COMPLETED
"Tạm ngưng" -> SManga.ON_HIATUS
"Đã huỷ" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
thumbnail_url = document.selectFirst("figure img")?.attr("abs:src")
}
override fun chapterListSelector() = "li.chapter"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
val anchor = element.selectFirst("a")!!
setUrlWithoutDomain(anchor.attr("abs:href"))
name = anchor.text()
date_upload = element
.selectFirst("time")
?.attr("datetime")
?.toLongOrNull()
?.times(1000L) ?: 0L
}
private val mangaIdRegex = Regex("""const MANGA_ID = (\d+);""")
private val chapterIdRegex = Regex("""const CHAPTER_ID = (\d+);""")
@Serializable
data class PageAjaxResponse(
val status: Boolean = false,
val msg: String? = null,
val html: String,
)
override fun pageListRequest(chapter: SChapter): Request {
val html = client.newCall(GET("$baseUrl${chapter.url}")).execute().body.string()
val chapterId = chapterIdRegex.find(html)?.groupValues?.get(1)
?: throw Exception("Không tìm thấy ID của chương truyện.")
val mangaId = mangaIdRegex.find(html)?.groupValues?.get(1)
if (mangaId != null) {
countViews(mangaId, chapterId)
}
return POST("https://manga.io.vn/ajax/image/list/chap/$chapterId", headers)
}
override fun pageListParse(response: Response): List<Page> {
val data = json.decodeFromString<PageAjaxResponse>(response.body.string())
if (!data.status) {
throw Exception(data.msg)
}
return pageListParse(Jsoup.parse(data.html))
}
override fun pageListParse(document: Document) =
document.select("a.readImg img").mapIndexed { i, it ->
Page(i, imageUrl = it.attr("abs:src"))
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
private fun countViews(mangaId: String, chapterId: String) {
val body = FormBody.Builder()
.add("manga", mangaId)
.add("chapter", chapterId)
.build()
val request = POST(
"$baseUrl/ajax/manga/view",
headers,
body,
)
runCatching { client.newCall(request).execute().close() }
}
override fun getFilterList() = FilterList(
Filter.Header("Không dùng chung với tìm kiếm bằng tên"),
ChapterCountFilter(),
StatusFilter(),
GenderFilter(),
OrderByFilter(),
GenreList(getGenresList()),
)
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val query: String,
private val vals: Array<Pair<String, String>>,
state: Int = 0,
) : UriFilter, Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(query, vals[state].second)
}
}
class ChapterCountFilter : UriPartFilter(
"Số chương",
"chapter_count",
arrayOf(
">= 0" to "0",
">= 10" to "10",
">= 30" to "30",
">= 50" to "50",
">= 100" to "100",
">= 200" to "200",
">= 300" to "300",
">= 400" to "400",
">= 500" to "500",
),
)
class GenderFilter : UriPartFilter(
"Giới tính",
"sex",
arrayOf(
"Tất cả" to "All",
"Con trai" to "Boy",
"Con gái" to "Girl",
),
)
class StatusFilter : UriPartFilter(
"Trạng thái",
"status",
arrayOf(
"Tất cả" to "",
"Hoàn thành" to "completed",
"Đang tiến hành" to "on-going",
"Tạm ngưng" to "on-hold",
"Đã huỷ" to "canceled",
),
)
class OrderByFilter : UriPartFilter(
"Sắp xếp",
"sort",
arrayOf(
"Mặc định" to "default",
"Mới cập nhật" to "latest-updated",
"Xem nhiều" to "views",
"Xem nhiều nhất tháng" to "views_month",
"Xem nhiều nhất tuần" to "views_week",
"Xem nhiều nhất hôm nay" to "views_day",
"Đánh giá cao" to "score",
"Từ A-Z" to "az",
"Từ Z-A" to "za",
"Số chương nhiều nhất" to "chapters",
"Mới nhất" to "new",
"Cũ nhất" to "old",
),
5,
)
class Genre(name: String, val id: String) : Filter.TriState(name)
class GenreList(state: List<Genre>) : UriFilter, Filter.Group<Genre>("Thể loại", state) {
override fun addToUri(builder: HttpUrl.Builder) {
val genres = mutableListOf<String>()
val genresEx = mutableListOf<String>()
state.forEach {
when (it.state) {
TriState.STATE_INCLUDE -> genres.add(it.id)
TriState.STATE_EXCLUDE -> genresEx.add(it.id)
else -> {}
}
}
if (genres.size > 0) {
builder.addQueryParameter("genres", genres.joinToString(","))
}
if (genresEx.size > 0) {
builder.addQueryParameter("notGenres", genresEx.joinToString(","))
}
}
}
/*
Get the list by navigating to https://manga.io.vn/filter/1 and paste in the code below
```
copy([...document.querySelectorAll("div.advanced-genres div.advance-item")].map((e) => {
const genreId = e.querySelector("span").dataset.genre;
const genreName = e.querySelector("label").textContent;
return `Genre("${genreName}", "${genreId}"),`
}).join("\n"))
```
*/
private fun getGenresList() = listOf(
Genre("16+", "788"),
Genre("Action", "129"),
Genre("Adult", "837"),
Genre("Adventure", "810"),
Genre("Bi Kịch", "393"),
Genre("Cải Biên Tiểu Thuyết", "771"),
Genre("Chuyển sinh", "287"),
Genre("Chuyển Thể", "803"),
Genre("Cổ Đại", "809"),
Genre("Cổ Trang", "340"),
Genre("Comedy", "131"),
Genre("Comic", "828"),
Genre("Cooking", "834"),
Genre("Doujinshi", "201"),
Genre("Drama", "149"),
Genre("Ecchi", "300"),
Genre("Fantasy", "132"),
Genre("Full màu", "189"),
Genre("Game", "38"),
Genre("Gender Bender", "133"),
Genre("gender_bender", "832"),
Genre("Girls Love", "815"),
Genre("Hài Hước", "791"),
Genre("Hào Môn", "779"),
Genre("Harem", "187"),
Genre("Hiện đại", "285"),
Genre("Historical", "836"),
Genre("Hoạt Hình", "497"),
Genre("Horror", "191"),
Genre("Huyền Huyễn", "475"),
Genre("Isekai", "811"),
Genre("Josei", "395"),
Genre("Lịch Sử", "561"),
Genre("Ma Mị", "764"),
Genre("Magic", "160"),
Genre("Main Mạnh", "763"),
Genre("Manga", "151"),
Genre("Manh Bảo", "807"),
Genre("Mạnh Mẽ", "818"),
Genre("Manhua", "153"),
Genre("Manhwa", "193"),
Genre("Martial Arts", "614"),
Genre("Mystery", "155"),
Genre("Ngôn Tình", "156"),
Genre("Ngọt Sủng", "799"),
Genre("Nữ Cường", "819"),
Genre("Oneshot", "65"),
Genre("Phép Thuật", "808"),
Genre("Phiêu Lưu", "478"),
Genre("Psychological", "180"),
Genre("Quái Vật", "758"),
Genre("Romance", "756"),
Genre("School Life", "31"),
Genre("school_life", "833"),
Genre("Sci-Fi", "812"),
Genre("Seinen", "172"),
Genre("Shoujo", "68"),
Genre("Shoujo Ai", "136"),
Genre("Shounen", "140"),
Genre("Shounen Ai", "203"),
Genre("Showbiz", "436"),
Genre("siêu nhiên", "765"),
Genre("Slice Of Life", "8"),
Genre("Sports", "167"),
Genre("Sư Tôn", "794"),
Genre("Sủng", "820"),
Genre("Sủng Nịch", "806"),
Genre("Supernatural", "150"),
Genre("Tận Thế", "759"),
Genre("Thú Thê", "800"),
Genre("Tiên Hiệp", "773"),
Genre("Tình cảm", "814"),
Genre("Tragedy", "822"),
Genre("Tranh Sủng", "805"),
Genre("Trap (Crossdressing)", "147"),
Genre("Trinh Thám", "336"),
Genre("Trọng Sinh", "398"),
Genre("Trùng Sinh", "392"),
Genre("Truy Thê", "780"),
Genre("Truyện Màu", "154"),
Genre("Truyện Nam", "761"),
Genre("Truyện Nữ", "776"),
Genre("Tu Tiên", "477"),
Genre("Viễn Tưởng", "438"),
Genre("VNComic", "787"),
Genre("Vườn Trường", "813"),
Genre("Webtoon", "198"),
Genre("Xuyên Không", "157"),
Genre("Yaoi", "593"),
Genre("Yuri", "137"),
)
}
class DocTruyen5s : Liliana("DocTruyen5s", "https://manga.io.vn", "vi")