Add back manhuascan (#849)
* add back manhuascan * bump version to 8 * stuff
This commit is contained in:
parent
a200ab1081
commit
a18a5e527c
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'ManhuaScan'
|
||||
extClass = '.ManhuaScan'
|
||||
extVersionCode = 8
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,333 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manhuascan
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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 eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Calendar
|
||||
|
||||
class ManhuaScan : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val name = "ManhuaScan"
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
|
||||
override val baseUrl = getMirror()
|
||||
|
||||
override val client by lazy {
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/popular${page.getPage()}", headers)
|
||||
|
||||
override fun popularMangaSelector(): String = ".manga-list > .book-item"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
thumbnail_url = element.selectFirst(".thumb img")?.imgAttr()
|
||||
element.selectFirst(".title a")!!.run {
|
||||
title = text()
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = ".paginator > .active + a"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/latest${page.getPage()}", headers)
|
||||
|
||||
override fun latestUpdatesSelector(): String =
|
||||
popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String =
|
||||
popularMangaNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genre = filters.firstInstanceOrNull<GenreFilter>()
|
||||
val genreInclusion = filters.firstInstanceOrNull<GenreInclusionFilter>()
|
||||
val status = filters.firstInstanceOrNull<StatusFilter>()
|
||||
val orderBy = filters.firstInstanceOrNull<OrderByFilter>()
|
||||
val author = filters.firstInstanceOrNull<AuthorFilter>()
|
||||
|
||||
val url = "$baseUrl/search${page.getPage()}".toHttpUrl().newBuilder().apply {
|
||||
genre?.included?.forEach {
|
||||
addEncodedQueryParameter("include[]", it)
|
||||
}
|
||||
genre?.excluded?.forEach {
|
||||
addEncodedQueryParameter("exclude[]", it)
|
||||
}
|
||||
addQueryParameter("include_mode", genreInclusion?.toUriPart())
|
||||
addQueryParameter("bookmark", "off")
|
||||
addQueryParameter("status", status?.toUriPart())
|
||||
addQueryParameter("sort", orderBy?.toUriPart())
|
||||
if (query.isNotEmpty()) {
|
||||
addQueryParameter("q", query)
|
||||
}
|
||||
if (author?.state?.isNotEmpty() == true) {
|
||||
addQueryParameter("author", author.state)
|
||||
}
|
||||
}
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector(): String =
|
||||
popularMangaNextPageSelector()
|
||||
|
||||
// =============================== Filters ==============================
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
GenreFilter(),
|
||||
GenreInclusionFilter(),
|
||||
Filter.Separator(),
|
||||
StatusFilter(),
|
||||
OrderByFilter(),
|
||||
AuthorFilter(),
|
||||
)
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
var alternativeName = ""
|
||||
|
||||
document.selectFirst(".book-info")?.run {
|
||||
genre = select(".meta p:has(strong:contains(Genres)) a").joinToString(", ") { it.text().removeSuffix(" ,") }
|
||||
author = select(".meta p:has(strong:contains(Authors)) a").joinToString(", ") { it.text() }
|
||||
thumbnail_url = selectFirst("#cover img")?.imgAttr()
|
||||
status = selectFirst(".meta p:has(strong:contains(Status)) a").parseStatus()
|
||||
title = selectFirst("h1")!!.text()
|
||||
selectFirst("h2")?.also {
|
||||
alternativeName = it.text()
|
||||
}
|
||||
}
|
||||
|
||||
description = buildString {
|
||||
document.selectFirst(".summary > p:not([style]):not(:empty)")?.let {
|
||||
append(it.text())
|
||||
if (alternativeName.isNotEmpty()) {
|
||||
append("\n\n")
|
||||
}
|
||||
}
|
||||
if (alternativeName.isNotEmpty()) {
|
||||
append("Alternative name(s): $alternativeName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = with(this?.text()) {
|
||||
return when {
|
||||
equals("ongoing", true) -> SManga.ONGOING
|
||||
equals("completed", true) -> SManga.COMPLETED
|
||||
equals("on-hold", true) -> SManga.ON_HIATUS
|
||||
equals("canceled", true) -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val id = manga.url.substringAfter("manga/").substringBefore("-")
|
||||
|
||||
val chapterHeaders = headersBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
set("Referer", baseUrl + manga.url)
|
||||
}.build()
|
||||
|
||||
val url = "$baseUrl/service/backend/chaplist/?manga_id=$id&manga_name=${manga.title}"
|
||||
|
||||
return GET(url, chapterHeaders)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
element.selectFirst("time")?.also {
|
||||
date_upload = it.text().parseRelativeDate()
|
||||
}
|
||||
name = element.selectFirst("strong")!!.text()
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||
}
|
||||
|
||||
// From OppaiStream
|
||||
private fun String.parseRelativeDate(): Long {
|
||||
val now = Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
||||
var parsedDate = 0L
|
||||
val relativeDate = this.split(" ").firstOrNull()
|
||||
?.replace("one", "1")
|
||||
?.replace("a", "1")
|
||||
?.toIntOrNull()
|
||||
?: return 0L
|
||||
|
||||
when {
|
||||
// parse: 30 seconds ago
|
||||
"second" in this -> {
|
||||
parsedDate = now.apply { add(Calendar.SECOND, -relativeDate) }.timeInMillis
|
||||
}
|
||||
// parses: "42 minutes ago"
|
||||
"minute" in this -> {
|
||||
parsedDate = now.apply { add(Calendar.MINUTE, -relativeDate) }.timeInMillis
|
||||
}
|
||||
// parses: "1 hour ago" and "2 hours ago"
|
||||
"hour" in this -> {
|
||||
parsedDate = now.apply { add(Calendar.HOUR, -relativeDate) }.timeInMillis
|
||||
}
|
||||
// parses: "2 days ago"
|
||||
"day" in this -> {
|
||||
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -relativeDate) }.timeInMillis
|
||||
}
|
||||
// parses: "2 weeks ago"
|
||||
"week" in this -> {
|
||||
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -relativeDate) }.timeInMillis
|
||||
}
|
||||
// parses: "2 months ago"
|
||||
"month" in this -> {
|
||||
parsedDate = now.apply { add(Calendar.MONTH, -relativeDate) }.timeInMillis
|
||||
}
|
||||
// parse: "2 years ago"
|
||||
"year" in this -> {
|
||||
parsedDate = now.apply { add(Calendar.YEAR, -relativeDate) }.timeInMillis
|
||||
}
|
||||
}
|
||||
return parsedDate
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val scriptData = document.selectFirst("script:containsData(chapterId)")?.data()
|
||||
?: throw Exception("Unable to find script data")
|
||||
val chapterId = CHAPTER_ID_REGEX.find(scriptData)?.groupValues?.get(1)
|
||||
?: throw Exception("Unable to retrieve chapterId")
|
||||
|
||||
val pagesHeaders = headersBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
set("Referer", document.location())
|
||||
}.build()
|
||||
val pagesUrl = "$baseUrl/service/backend/chapterServer/?server_id=$server&chapter_id=$chapterId"
|
||||
|
||||
val pagesDocument = client.newCall(
|
||||
GET(pagesUrl, pagesHeaders),
|
||||
).execute().asJsoup()
|
||||
|
||||
return pagesDocument.select("div").map { page ->
|
||||
val url = page.imgAttr()
|
||||
val index = page.id().substringAfterLast("-").toInt()
|
||||
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", "*/*")
|
||||
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||
set("Referer", page.url)
|
||||
}.build()
|
||||
|
||||
return GET(page.imageUrl!!, imgHeaders)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun Int.getPage(): String = if (this == 1) "" else "?page=$this"
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val CHAPTER_ID_REGEX = Regex("""chapterId\s*=\s*(\d+)""")
|
||||
|
||||
private const val MIRROR_PREF_KEY = "pref_mirror"
|
||||
private const val MIRROR_PREF_TITLE = "Select Mirror (Requires Restart)"
|
||||
private val MIRROR_PREF_ENTRIES = arrayOf("manhuascan.com", "manhuascan.io", "mangajinx.com")
|
||||
private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
|
||||
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES.first()
|
||||
|
||||
private const val SERVER_PREF_KEY = "pref_server"
|
||||
private const val SERVER_PREF_TITLE = "Image Server"
|
||||
private val SERVER_PREF_ENTRIES = arrayOf("Server 1", "Server 2")
|
||||
private val SERVER_PREF_ENTRY_VALUES = SERVER_PREF_ENTRIES.map { it.substringAfter(" ") }.toTypedArray()
|
||||
private val SERVER_PREF_DEFAULT_VALUE = SERVER_PREF_ENTRY_VALUES.first()
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = MIRROR_PREF_KEY
|
||||
title = MIRROR_PREF_TITLE
|
||||
entries = MIRROR_PREF_ENTRIES
|
||||
entryValues = MIRROR_PREF_ENTRY_VALUES
|
||||
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
|
||||
summary = "%s"
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = SERVER_PREF_KEY
|
||||
title = SERVER_PREF_TITLE
|
||||
entries = SERVER_PREF_ENTRIES
|
||||
entryValues = SERVER_PREF_ENTRY_VALUES
|
||||
setDefaultValue(SERVER_PREF_DEFAULT_VALUE)
|
||||
summary = "%s"
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private fun getMirror(): String =
|
||||
preferences.getString(MIRROR_PREF_KEY, MIRROR_PREF_DEFAULT_VALUE)!!
|
||||
|
||||
private val server
|
||||
get() = preferences.getString(SERVER_PREF_KEY, SERVER_PREF_DEFAULT_VALUE)!!
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package eu.kanade.tachiyomi.extension.en.manhuascan
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
inline fun <reified T> List<*>.firstInstanceOrNull() = firstOrNull { it is T } as? T
|
||||
|
||||
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
class Genre(val id: String, name: String) : Filter.TriState(name)
|
||||
|
||||
class GenreFilter : Filter.Group<Genre>(
|
||||
"Genres",
|
||||
listOf(
|
||||
Genre("action", "Action"),
|
||||
Genre("adaptation", "Adaptation"),
|
||||
Genre("adult", "Adult"),
|
||||
Genre("adventure", "Adventure"),
|
||||
Genre("animal", "Animal"),
|
||||
Genre("anthology", "Anthology"),
|
||||
Genre("cartoon", "Cartoon"),
|
||||
Genre("comedy", "Comedy"),
|
||||
Genre("comic", "Comic"),
|
||||
Genre("cooking", "Cooking"),
|
||||
Genre("demons", "Demons"),
|
||||
Genre("doujinshi", "Doujinshi"),
|
||||
Genre("drama", "Drama"),
|
||||
Genre("ecchi", "Ecchi"),
|
||||
Genre("fantasy", "Fantasy"),
|
||||
Genre("full-color", "Full Color"),
|
||||
Genre("game", "Game"),
|
||||
Genre("gender-bender", "Gender bender"),
|
||||
Genre("ghosts", "Ghosts"),
|
||||
Genre("harem", "Harem"),
|
||||
Genre("historical", "Historical"),
|
||||
Genre("horror", "Horror"),
|
||||
Genre("isekai", "Isekai"),
|
||||
Genre("josei", "Josei"),
|
||||
Genre("long-strip", "Long strip"),
|
||||
Genre("mafia", "Mafia"),
|
||||
Genre("magic", "Magic"),
|
||||
Genre("manga", "Manga"),
|
||||
Genre("manhua", "Manhua"),
|
||||
Genre("manhwa", "Manhwa"),
|
||||
Genre("martial-arts", "Martial arts"),
|
||||
Genre("mature", "Mature"),
|
||||
Genre("mecha", "Mecha"),
|
||||
Genre("medical", "Medical"),
|
||||
Genre("military", "Military"),
|
||||
Genre("monster", "Monster"),
|
||||
Genre("monster-girls", "Monster girls"),
|
||||
Genre("monsters", "Monsters"),
|
||||
Genre("music", "Music"),
|
||||
Genre("mystery", "Mystery"),
|
||||
Genre("office", "Office"),
|
||||
Genre("office-workers", "Office workers"),
|
||||
Genre("one-shot", "One shot"),
|
||||
Genre("police", "Police"),
|
||||
Genre("psychological", "Psychological"),
|
||||
Genre("reincarnation", "Reincarnation"),
|
||||
Genre("romance", "Romance"),
|
||||
Genre("school-life", "School life"),
|
||||
Genre("sci-fi", "Sci fi"),
|
||||
Genre("science-fiction", "Science fiction"),
|
||||
Genre("seinen", "Seinen"),
|
||||
Genre("shoujo", "Shoujo"),
|
||||
Genre("shoujo-ai", "Shoujo ai"),
|
||||
Genre("shounen", "Shounen"),
|
||||
Genre("shounen-ai", "Shounen ai"),
|
||||
Genre("slice-of-life", "Slice of life"),
|
||||
Genre("smut", "Smut"),
|
||||
Genre("soft-yaoi", "Soft Yaoi"),
|
||||
Genre("sports", "Sports"),
|
||||
Genre("super-power", "Super Power"),
|
||||
Genre("superhero", "Superhero"),
|
||||
Genre("supernatural", "Supernatural"),
|
||||
Genre("thriller", "Thriller"),
|
||||
Genre("time-travel", "Time travel"),
|
||||
Genre("tragedy", "Tragedy"),
|
||||
Genre("vampire", "Vampire"),
|
||||
Genre("vampires", "Vampires"),
|
||||
Genre("video-games", "Video games"),
|
||||
Genre("villainess", "Villainess"),
|
||||
Genre("web-comic", "Web comic"),
|
||||
Genre("webtoons", "Webtoons"),
|
||||
Genre("yaoi", "Yaoi"),
|
||||
Genre("yuri", "Yuri"),
|
||||
Genre("zombies", "Zombies"),
|
||||
),
|
||||
) {
|
||||
val included: List<String>?
|
||||
get() = state.filter { it.isIncluded() }.map { it.id }.takeUnless { it.isEmpty() }
|
||||
|
||||
val excluded: List<String>?
|
||||
get() = state.filter { it.isExcluded() }.map { it.id }.takeUnless { it.isEmpty() }
|
||||
}
|
||||
|
||||
class GenreInclusionFilter : UriPartFilter(
|
||||
"Genre Inclusion Mode",
|
||||
arrayOf(
|
||||
Pair("AND (All Selected Genres)", "and"),
|
||||
Pair("OR (Any Selected Genres)", "or"),
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : UriPartFilter(
|
||||
"Status",
|
||||
arrayOf(
|
||||
Pair("All", "all"),
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Completed", "completed"),
|
||||
),
|
||||
)
|
||||
|
||||
class OrderByFilter : UriPartFilter(
|
||||
"Order By",
|
||||
arrayOf(
|
||||
Pair("Views", "views"),
|
||||
Pair("Updated", "updated_at"),
|
||||
Pair("Created", "created_at"),
|
||||
Pair("Name A-Z", "name"),
|
||||
Pair("Number of Chapters", "total_chapters"),
|
||||
Pair("Rating", "rating"),
|
||||
),
|
||||
)
|
||||
|
||||
class AuthorFilter : Filter.Text("Author name")
|
Loading…
Reference in New Issue