New multisrc theme: Liliana (#2413)
* new multisrc theme: liliana * dont specify type * suggestions * add raw1001
5
lib-multisrc/liliana/build.gradle.kts
Normal file
@ -0,0 +1,5 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -0,0 +1,90 @@
|
||||
package eu.kanade.tachiyomi.multisrc.liliana
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
interface UrlPartFilter {
|
||||
fun addUrlParameter(url: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
private val urlParameter: String,
|
||||
) : UrlPartFilter, Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
override fun addUrlParameter(url: HttpUrl.Builder) {
|
||||
url.addQueryParameter(urlParameter, options[state].second)
|
||||
}
|
||||
}
|
||||
|
||||
class TriStateFilter(name: String, val id: String) : Filter.TriState(name)
|
||||
|
||||
abstract class TriStateGroupFilter(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
private val includeUrlParameter: String,
|
||||
private val excludeUrlParameter: String,
|
||||
) : UrlPartFilter, Filter.Group<TriStateFilter>(
|
||||
name,
|
||||
options.map { TriStateFilter(it.first, it.second) },
|
||||
) {
|
||||
override fun addUrlParameter(url: HttpUrl.Builder) {
|
||||
url.addQueryParameter(
|
||||
includeUrlParameter,
|
||||
state.filter { it.isIncluded() }.joinToString(",") { it.id },
|
||||
)
|
||||
url.addQueryParameter(
|
||||
excludeUrlParameter,
|
||||
state.filter { it.isExcluded() }.joinToString(",") { it.id },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class GenreFilter(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
) : TriStateGroupFilter(
|
||||
name,
|
||||
options,
|
||||
"genres",
|
||||
"notGenres",
|
||||
)
|
||||
|
||||
class ChapterCountFilter(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
) : SelectFilter(
|
||||
name,
|
||||
options,
|
||||
"chapter_count",
|
||||
)
|
||||
|
||||
class StatusFilter(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
) : SelectFilter(
|
||||
name,
|
||||
options,
|
||||
"status",
|
||||
)
|
||||
|
||||
class GenderFilter(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
) : SelectFilter(
|
||||
name,
|
||||
options,
|
||||
"sex",
|
||||
)
|
||||
|
||||
class SortFilter(
|
||||
name: String,
|
||||
options: List<Pair<String, String>>,
|
||||
) : SelectFilter(
|
||||
name,
|
||||
options,
|
||||
"sort",
|
||||
)
|
@ -0,0 +1,353 @@
|
||||
package eu.kanade.tachiyomi.multisrc.liliana
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.Exception
|
||||
|
||||
abstract class Liliana(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
final override val lang: String,
|
||||
private val usesPostSearch: Boolean = false,
|
||||
) : ParsedHttpSource() {
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers)
|
||||
|
||||
override fun popularMangaSelector(): String = "div#main div.grid > div"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")?.imgAttr()
|
||||
with(element.selectFirst(".text-center a")!!) {
|
||||
title = text()
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/all-manga/$page/?sort=last_update&status=0", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||
popularMangaParse(response)
|
||||
|
||||
override fun latestUpdatesSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.isNotBlank() && usesPostSearch) {
|
||||
val formBody = FormBody.Builder()
|
||||
.add("search", query)
|
||||
.build()
|
||||
|
||||
val formHeaders = headersBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
add("Origin", baseUrl)
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
return POST("$baseUrl/ajax/search", formHeaders, formBody)
|
||||
}
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (query.isNotBlank()) {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("keyword", query)
|
||||
} else {
|
||||
addPathSegment("filter")
|
||||
filters.filterIsInstance<UrlPartFilter>().forEach {
|
||||
it.addUrlParameter(this)
|
||||
}
|
||||
}
|
||||
addPathSegment(page.toString())
|
||||
addPathSegment("")
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (response.request.method == "GET") {
|
||||
return popularMangaParse(response)
|
||||
}
|
||||
|
||||
val mangaList = response.parseAs<SearchResponseDto>().list.map { manga ->
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(manga.url)
|
||||
title = manga.name
|
||||
thumbnail_url = baseUrl + manga.cover
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangaList, false)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class SearchResponseDto(
|
||||
val list: List<MangaDto>,
|
||||
) {
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
val cover: String,
|
||||
val name: String,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaNextPageSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Filters ==============================
|
||||
|
||||
protected var genreName = ""
|
||||
protected var genreData = listOf<Pair<String, String>>()
|
||||
protected var chapterCountName = ""
|
||||
protected var chapterCountData = listOf<Pair<String, String>>()
|
||||
protected var statusName = ""
|
||||
protected var statusData = listOf<Pair<String, String>>()
|
||||
protected var genderName = ""
|
||||
protected var genderData = listOf<Pair<String, String>>()
|
||||
protected var sortName = ""
|
||||
protected var sortData = listOf<Pair<String, String>>()
|
||||
private var fetchFilterAttempts = 0
|
||||
|
||||
protected suspend fun fetchFilters() {
|
||||
if (
|
||||
fetchFilterAttempts < 3 &&
|
||||
arrayOf(genreData, chapterCountData, statusData, genderData, sortData).any { it.isEmpty() }
|
||||
) {
|
||||
try {
|
||||
val doc = client.newCall(filtersRequest())
|
||||
.await()
|
||||
.asJsoup()
|
||||
|
||||
parseFilters(doc)
|
||||
} catch (e: Exception) {
|
||||
Log.e("$name: Filters", e.stackTraceToString())
|
||||
}
|
||||
fetchFilterAttempts++
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun filtersRequest() = GET("$baseUrl/filter", headers)
|
||||
|
||||
protected open fun parseFilters(document: Document) {
|
||||
genreName = document.selectFirst("div.advanced-genres > h3")?.text() ?: ""
|
||||
genreData = document.select("div.advanced-genres > div > .advance-item").map {
|
||||
it.text() to it.selectFirst("span")!!.attr("data-genre")
|
||||
}
|
||||
|
||||
chapterCountName = document.getSelectName("select-count")
|
||||
chapterCountData = document.getSelectData("select-count")
|
||||
|
||||
statusName = document.getSelectName("select-status")
|
||||
statusData = document.getSelectData("select-status")
|
||||
|
||||
genderName = document.getSelectName("select-gender")
|
||||
genderData = document.getSelectData("select-gender")
|
||||
|
||||
sortName = document.getSelectName("select-sort")
|
||||
sortData = document.getSelectData("select-sort")
|
||||
}
|
||||
|
||||
private fun Document.getSelectName(selectorClass: String): String {
|
||||
return this.selectFirst(".select-div > label.$selectorClass")?.text() ?: ""
|
||||
}
|
||||
|
||||
private fun Document.getSelectData(selectorId: String): List<Pair<String, String>> {
|
||||
return this.select("#$selectorId > option").map {
|
||||
it.text() to it.attr("value")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
launchIO { fetchFilters() }
|
||||
|
||||
val filters = mutableListOf<Filter<*>>()
|
||||
|
||||
if (genreData.isNotEmpty()) {
|
||||
filters.add(GenreFilter(genreName, genreData))
|
||||
}
|
||||
if (chapterCountData.isNotEmpty()) {
|
||||
filters.add(ChapterCountFilter(chapterCountName, chapterCountData))
|
||||
}
|
||||
if (statusData.isNotEmpty()) {
|
||||
filters.add(StatusFilter(statusName, statusData))
|
||||
}
|
||||
if (genderData.isNotEmpty()) {
|
||||
filters.add(GenderFilter(genderName, genderData))
|
||||
}
|
||||
if (sortData.isNotEmpty()) {
|
||||
filters.add(SortFilter(sortName, sortData))
|
||||
}
|
||||
if (filters.size < 5) {
|
||||
filters.add(0, Filter.Header("Press 'reset' to load more filters"))
|
||||
} else {
|
||||
filters.add(0, Filter.Header("NOTE: Ignored if using text search!"))
|
||||
filters.add(1, Filter.Separator())
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
protected fun launchIO(block: suspend () -> Unit) = scope.launch { block() }
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
description = document.selectFirst("div#syn-target")?.text()
|
||||
thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr()
|
||||
title = document.selectFirst(".a2 header h1")!!.text()
|
||||
genre = document.select(".a2 div > a[rel='tag'].label").joinToString { it.text() }
|
||||
author = document.selectFirst("div.y6x11p i.fas.fa-user + span.dt")?.text()?.takeUnless {
|
||||
it.equals("updating", true)
|
||||
}
|
||||
status = document.selectFirst("div.y6x11p i.fas.fa-rss + span.dt").parseStatus()
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"ongoing", "đang tiến hành", "進行中" -> SManga.ONGOING
|
||||
"completed", "hoàn thành", "完了" -> SManga.COMPLETED
|
||||
"on-hold", "tạm ngưng", "保留" -> SManga.ON_HIATUS
|
||||
"canceled", "đã huỷ", "キャンセル" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun chapterListSelector() = "ul > li.chapter"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
element.selectFirst("time[datetime]")?.also {
|
||||
date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L
|
||||
}
|
||||
with(element.selectFirst("a")!!) {
|
||||
name = text()
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
|
||||
@Serializable
|
||||
class PageListResponseDto(
|
||||
val status: Boolean = false,
|
||||
val msg: String? = null,
|
||||
val html: String,
|
||||
)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")?.data()
|
||||
?: throw Exception("Failed to get chapter id")
|
||||
|
||||
val chapterId = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
||||
|
||||
val pageHeaders = headersBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, *//*; q=0.01")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
set("Referer", response.request.url.toString())
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
val ajaxResponse = client.newCall(
|
||||
GET("$baseUrl/ajax/image/list/chap/$chapterId", pageHeaders),
|
||||
).execute()
|
||||
|
||||
val data = ajaxResponse.parseAs<PageListResponseDto>()
|
||||
|
||||
if (!data.status) {
|
||||
throw Exception(data.msg)
|
||||
}
|
||||
|
||||
return pageListParse(
|
||||
Jsoup.parseBodyFragment(
|
||||
data.html,
|
||||
response.request.url.toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div.separator").mapIndexed { i, page ->
|
||||
val url = page.selectFirst("a")!!.attr("abs:href")
|
||||
Page(i, document.location(), url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeaders = headersBuilder().apply {
|
||||
add("Accept", "image/avif,image/webp,*/*")
|
||||
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||
}.build()
|
||||
return GET(page.imageUrl!!, imgHeaders)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
// From mangathemesia
|
||||
private fun Element.imgAttr(): String = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
ext {
|
||||
extName = 'Manhuagold'
|
||||
extClass = '.Manhuagold'
|
||||
themePkg = 'mangareader'
|
||||
baseUrl = 'https://manhuagold.com'
|
||||
overrideVersionCode = 33
|
||||
themePkg = 'liliana'
|
||||
baseUrl = 'https://manhuagold.top'
|
||||
overrideVersionCode = 34
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,233 +1,18 @@
|
||||
package eu.kanade.tachiyomi.extension.en.comickiba
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
|
||||
class Manhuagold : MangaReader() {
|
||||
class Manhuagold : Liliana(
|
||||
"Manhuagold",
|
||||
"https://manhuagold.top",
|
||||
"en",
|
||||
usesPostSearch = true,
|
||||
) {
|
||||
// MangaReader -> Liliana
|
||||
override val versionId = 2
|
||||
|
||||
override val name = "Manhuagold"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val baseUrl = "https://manhuagold.com"
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$baseUrl/filter/$page/?sort=views&sex=All&chapter_count=0", headers)
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/filter/$page/?sort=latest-updated&sex=All&chapter_count=0", headers)
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
if (query.isNotBlank()) {
|
||||
urlBuilder.addPathSegment("search").apply {
|
||||
addQueryParameter("keyword", query)
|
||||
}
|
||||
} else {
|
||||
urlBuilder.addPathSegment("filter").apply {
|
||||
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||
when (filter) {
|
||||
is Select -> {
|
||||
addQueryParameter(filter.param, filter.selection)
|
||||
}
|
||||
is GenresFilter -> {
|
||||
addQueryParameter(filter.param, filter.selection)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
urlBuilder.addPathSegment(page.toString())
|
||||
urlBuilder.addPathSegment("")
|
||||
|
||||
return GET(urlBuilder.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) =
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
||||
title = it.attr("alt")
|
||||
thumbnail_url = it.imgAttr()
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "ul.pagination > li.active + li"
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() =
|
||||
FilterList(
|
||||
Note,
|
||||
StatusFilter(),
|
||||
SortFilter(),
|
||||
GenresFilter(),
|
||||
)
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
|
||||
val mangaTitle = root.selectFirst(Evaluator.Class("manga-name"))!!.ownText()
|
||||
title = mangaTitle
|
||||
description = root.run {
|
||||
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
||||
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
|
||||
"", mangaTitle -> description
|
||||
else -> "$description\n\nAlternative Title: $altTitle"
|
||||
}
|
||||
}
|
||||
thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.imgAttr()
|
||||
genre = root.selectFirst(Evaluator.Class("genres"))!!.children().joinToString { it.ownText() }
|
||||
for (item in root.selectFirst(Evaluator.Class("anisc-info"))!!.children()) {
|
||||
if (item.hasClass("item").not()) continue
|
||||
when (item.selectFirst(Evaluator.Class("item-head"))!!.ownText()) {
|
||||
"Authors:" -> item.parseAuthorsTo(this)
|
||||
"Status:" -> status = when (item.selectFirst(Evaluator.Class("name"))!!.ownText().lowercase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
"on-hold" -> SManga.ON_HIATUS
|
||||
"canceled" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.parseAuthorsTo(manga: SManga) {
|
||||
val authors = select(Evaluator.Tag("a"))
|
||||
val text = authors.map { it.ownText().replace(",", "") }
|
||||
val count = authors.size
|
||||
when (count) {
|
||||
0 -> return
|
||||
1 -> {
|
||||
manga.author = text[0]
|
||||
return
|
||||
}
|
||||
}
|
||||
val authorList = ArrayList<String>(count)
|
||||
val artistList = ArrayList<String>(count)
|
||||
for ((index, author) in authors.withIndex()) {
|
||||
val textNode = author.nextSibling() as? TextNode
|
||||
val list = if (textNode != null && "(Art)" in textNode.wholeText) artistList else authorList
|
||||
list.add(text[index])
|
||||
}
|
||||
if (authorList.isEmpty().not()) manga.author = authorList.joinToString()
|
||||
if (artistList.isEmpty().not()) manga.artist = artistList.joinToString()
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListRequest(mangaUrl: String, type: String): Request =
|
||||
GET(baseUrl + mangaUrl, headers)
|
||||
|
||||
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override val chapterType = ""
|
||||
override val volumeType = ""
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map(::parseChapterList)
|
||||
}
|
||||
|
||||
private fun parseChapterList(response: Response): List<SChapter> {
|
||||
val document = response.use { it.asJsoup() }
|
||||
|
||||
return document.select(chapterListSelector())
|
||||
.map(::chapterFromElement)
|
||||
}
|
||||
|
||||
private fun chapterListSelector(): String = "#chapters-list > li"
|
||||
|
||||
private fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
element.selectFirst("a")!!.run {
|
||||
setUrlWithoutDomain(attr("href"))
|
||||
name = selectFirst(".name")?.text() ?: text()
|
||||
}
|
||||
}
|
||||
|
||||
// Images
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable {
|
||||
val document = client.newCall(pageListRequest(chapter)).execute().asJsoup()
|
||||
|
||||
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data()
|
||||
val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
||||
|
||||
val ajaxHeaders = super.headersBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||
add("Referer", baseUrl + chapter.url)
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
val ajaxUrl = "$baseUrl/ajax/image/list/chap/$id"
|
||||
client.newCall(GET(ajaxUrl, ajaxHeaders)).execute().let(::pageListParse)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.use { it.parseHtmlProperty() }
|
||||
|
||||
val pageList = document.select("div").map {
|
||||
val index = it.attr("data-number").toInt()
|
||||
val imgUrl = it.imgAttr().ifEmpty { it.selectFirst("img")!!.imgAttr() }
|
||||
|
||||
Page(index, "", imgUrl)
|
||||
}
|
||||
|
||||
return pageList
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
// From mangathemesia
|
||||
private fun Element.imgAttr(): String = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
|
||||
private fun Response.parseHtmlProperty(): Document {
|
||||
val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
|
||||
return Jsoup.parseBodyFragment(html)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
ext {
|
||||
extName = 'ManhuaPlus (unoriginal)'
|
||||
extClass = '.ManhuaPlusOrg'
|
||||
extVersionCode = 1
|
||||
themePkg = 'liliana'
|
||||
baseUrl = 'https://manhuaplus.org'
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,242 +1,9 @@
|
||||
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.multisrc.liliana.Liliana
|
||||
|
||||
class ManhuaPlusOrg : ParsedHttpSource() {
|
||||
|
||||
override val name = "ManhuaPlus (Unoriginal)"
|
||||
|
||||
override val baseUrl = "https://manhuaplus.org"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(1)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers)
|
||||
|
||||
override fun popularMangaSelector(): String = "div#main div.grid > div"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")?.imgAttr()
|
||||
element.selectFirst(".text-center a")!!.run {
|
||||
title = text().trim()
|
||||
setUrlWithoutDomain(attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span"
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/all-manga/$page/?sort=1", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun latestUpdatesSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (query.isNotBlank()) {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("keyword", query)
|
||||
} else {
|
||||
addPathSegment("filter")
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> {
|
||||
if (filter.checked.isNotEmpty()) {
|
||||
addQueryParameter("genres", filter.checked.joinToString(","))
|
||||
}
|
||||
}
|
||||
is StatusFilter -> {
|
||||
if (filter.selected.isNotBlank()) {
|
||||
addQueryParameter("status", filter.selected)
|
||||
}
|
||||
}
|
||||
is SortFilter -> {
|
||||
addQueryParameter("sort", filter.selected)
|
||||
}
|
||||
is ChapterCountFilter -> {
|
||||
addQueryParameter("chapter_count", filter.selected)
|
||||
}
|
||||
is GenderFilter -> {
|
||||
addQueryParameter("sex", filter.selected)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPathSegment(page.toString())
|
||||
addPathSegment("")
|
||||
}
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaNextPageSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("Ignored when using text search"),
|
||||
Filter.Separator(),
|
||||
GenreFilter(),
|
||||
ChapterCountFilter(),
|
||||
GenderFilter(),
|
||||
StatusFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
description = document.selectFirst("div#syn-target")?.text()
|
||||
thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr()
|
||||
title = document.selectFirst(".a2 header h1")?.text()?.trim() ?: "N/A"
|
||||
genre = document.select(".a2 div > a[rel='tag'].label").joinToString(", ") { it.text() }
|
||||
|
||||
document.selectFirst(".a1 > aside")?.run {
|
||||
author = select("div:contains(Authors) > span a")
|
||||
.joinToString(", ") { it.text().trim() }
|
||||
.takeUnless { it.isBlank() || it.equals("Updating", true) }
|
||||
status = selectFirst("div:contains(Status) > span")?.text().let(::parseStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?): Int = when {
|
||||
status.equals("ongoing", true) -> SManga.ONGOING
|
||||
status.equals("completed", true) -> SManga.COMPLETED
|
||||
status.equals("on-hold", true) -> SManga.ON_HIATUS
|
||||
status.equals("canceled", true) -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector() = "ul > li.chapter"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
element.selectFirst("time[datetime]")?.also {
|
||||
date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L
|
||||
}
|
||||
element.selectFirst("a")!!.run {
|
||||
text().trim().also {
|
||||
name = it
|
||||
chapter_number = it.substringAfter("hapter ").toFloatOrNull() ?: 0F
|
||||
}
|
||||
setUrlWithoutDomain(attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
|
||||
|
||||
val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data()
|
||||
|
||||
val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";")
|
||||
|
||||
val pageHeaders = headersBuilder().apply {
|
||||
add("Accept", "application/json, text/javascript, *//*; q=0.01")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
add("Referer", baseUrl + chapter.url)
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
return GET("$baseUrl/ajax/image/list/chap/$id", pageHeaders)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PageListResponseDto(val html: String)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val data = response.parseAs<PageListResponseDto>().html
|
||||
return pageListParse(
|
||||
Jsoup.parseBodyFragment(
|
||||
data,
|
||||
response.request.header("Referer")!!,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div.separator").map { page ->
|
||||
val index = page.selectFirst("img")!!.attr("alt").substringAfterLast(" ").toInt()
|
||||
val url = page.selectFirst("a")!!.attr("abs:href")
|
||||
Page(index, document.location(), url)
|
||||
}.sortedBy { it.index }
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeaders = headersBuilder().apply {
|
||||
add("Accept", "image/avif,image/webp,*/*")
|
||||
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||
}.build()
|
||||
return GET(page.imageUrl!!, imgHeaders)
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
// From mangathemesia
|
||||
private fun Element.imgAttr(): String = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
}
|
||||
class ManhuaPlusOrg : Liliana(
|
||||
"ManhuaPlus (Unoriginal)",
|
||||
"https://manhuaplus.org",
|
||||
"en",
|
||||
)
|
||||
|
@ -1,139 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.manhuaplusorg
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
val selected get() = options[state].second
|
||||
}
|
||||
|
||||
class CheckBoxFilter(
|
||||
name: String,
|
||||
val value: String,
|
||||
) : Filter.CheckBox(name)
|
||||
|
||||
class ChapterCountFilter : SelectFilter("Chapter count", chapterCount) {
|
||||
companion object {
|
||||
private val chapterCount = listOf(
|
||||
Pair(">= 0", "0"),
|
||||
Pair(">= 10", "10"),
|
||||
Pair(">= 30", "30"),
|
||||
Pair(">= 50", "50"),
|
||||
Pair(">= 100", "100"),
|
||||
Pair(">= 200", "200"),
|
||||
Pair(">= 300", "300"),
|
||||
Pair(">= 400", "400"),
|
||||
Pair(">= 500", "500"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class GenderFilter : SelectFilter("Manga Gender", gender) {
|
||||
companion object {
|
||||
private val gender = listOf(
|
||||
Pair("All", "All"),
|
||||
Pair("Boy", "Boy"),
|
||||
Pair("Girl", "Girl"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class StatusFilter : SelectFilter("Status", status) {
|
||||
companion object {
|
||||
private val status = listOf(
|
||||
Pair("All", ""),
|
||||
Pair("Completed", "completed"),
|
||||
Pair("OnGoing", "on-going"),
|
||||
Pair("On-Hold", "on-hold"),
|
||||
Pair("Canceled", "canceled"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : SelectFilter("Sort", sort) {
|
||||
companion object {
|
||||
private val sort = listOf(
|
||||
Pair("Default", "default"),
|
||||
Pair("Latest Updated", "latest-updated"),
|
||||
Pair("Most Viewed", "views"),
|
||||
Pair("Most Viewed Month", "views_month"),
|
||||
Pair("Most Viewed Week", "views_week"),
|
||||
Pair("Most Viewed Day", "views_day"),
|
||||
Pair("Score", "score"),
|
||||
Pair("Name A-Z", "az"),
|
||||
Pair("Name Z-A", "za"),
|
||||
Pair("Newest", "new"),
|
||||
Pair("Oldest", "old"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class GenreFilter : Filter.Group<CheckBoxFilter>(
|
||||
"Genre",
|
||||
genres.map { CheckBoxFilter(it.first, it.second) },
|
||||
) {
|
||||
val checked get() = state.filter { it.state }.map { it.value }
|
||||
|
||||
companion object {
|
||||
private val genres = listOf(
|
||||
Pair("Action", "4"),
|
||||
Pair("Adaptation", "87"),
|
||||
Pair("Adult", "31"),
|
||||
Pair("Adventure", "5"),
|
||||
Pair("Animals", "1657"),
|
||||
Pair("Cartoon", "46"),
|
||||
Pair("Comedy", "14"),
|
||||
Pair("Demons", "284"),
|
||||
Pair("Drama", "59"),
|
||||
Pair("Ecchi", "67"),
|
||||
Pair("Fantasy", "6"),
|
||||
Pair("Full Color", "89"),
|
||||
Pair("Genderswap", "2409"),
|
||||
Pair("Ghosts", "2253"),
|
||||
Pair("Gore", "1182"),
|
||||
Pair("Harem", "17"),
|
||||
Pair("Historical", "642"),
|
||||
Pair("Horror", "797"),
|
||||
Pair("Isekai", "239"),
|
||||
Pair("Live action", "11"),
|
||||
Pair("Long Strip", "86"),
|
||||
Pair("Magic", "90"),
|
||||
Pair("Magical Girls", "1470"),
|
||||
Pair("Manhua", "7"),
|
||||
Pair("Manhwa", "70"),
|
||||
Pair("Martial Arts", "8"),
|
||||
Pair("Mature", "12"),
|
||||
Pair("Mecha", "786"),
|
||||
Pair("Medical", "1443"),
|
||||
Pair("Monsters", "138"),
|
||||
Pair("Mystery", "9"),
|
||||
Pair("Post-Apocalyptic", "285"),
|
||||
Pair("Psychological", "798"),
|
||||
Pair("Reincarnation", "139"),
|
||||
Pair("Romance", "987"),
|
||||
Pair("School Life", "10"),
|
||||
Pair("Sci-fi", "135"),
|
||||
Pair("Seinen", "196"),
|
||||
Pair("Shounen", "26"),
|
||||
Pair("Shounen ai", "64"),
|
||||
Pair("Slice of Life", "197"),
|
||||
Pair("Superhero", "136"),
|
||||
Pair("Supernatural", "13"),
|
||||
Pair("Survival", "140"),
|
||||
Pair("Thriller", "137"),
|
||||
Pair("Time travel", "231"),
|
||||
Pair("Tragedy", "15"),
|
||||
Pair("Video Games", "283"),
|
||||
Pair("Villainess", "676"),
|
||||
Pair("Virtual Reality", "611"),
|
||||
Pair("Web comic", "88"),
|
||||
Pair("Webtoon", "18"),
|
||||
Pair("Wuxia", "239"),
|
||||
)
|
||||
}
|
||||
}
|
9
src/ja/mangakoma/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
ext {
|
||||
extName = 'Manga Koma'
|
||||
extClass = '.MangaKoma'
|
||||
themePkg = 'liliana'
|
||||
baseUrl = 'https://mangakoma01.net'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/ja/mangakoma/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/ja/mangakoma/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/ja/mangakoma/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/ja/mangakoma/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
src/ja/mangakoma/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
@ -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 }
|
||||
}
|
||||
}
|
9
src/ja/raw1001/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
ext {
|
||||
extName = 'Raw1001'
|
||||
extClass = '.Raw1001'
|
||||
themePkg = 'liliana'
|
||||
baseUrl = 'https://raw1001.net'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/ja/raw1001/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/ja/raw1001/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/ja/raw1001/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/ja/raw1001/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/ja/raw1001/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
@ -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")
|
@ -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"
|
||||
|
@ -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")
|
||||
|