Fmreader split (#5800)
* split fmreader * convert Manhwa18Net to factory * remove the extra source * add back Manhwa18 * fix building
This commit is contained in:
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
@ -1,13 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'FMReader (multiple aggregators)'
|
||||
pkgNameSuffix = 'all.fmreader'
|
||||
extClass = '.FMReaderFactory'
|
||||
extVersionCode = 28
|
||||
libVersion = '1.2'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 72 KiB |
@ -1,458 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.fmreader
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import java.nio.charset.Charset
|
||||
import java.util.Calendar
|
||||
|
||||
/**
|
||||
* For sites based on the Flat-Manga CMS
|
||||
*/
|
||||
abstract class FMReader(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64) Gecko/20100101 Firefox/77.0")
|
||||
add("Referer", baseUrl)
|
||||
}
|
||||
|
||||
protected fun Elements.imgAttr(): String? = getImgAttr(this.firstOrNull())
|
||||
|
||||
private fun Element.imgAttr(): String? = getImgAttr(this)
|
||||
|
||||
open fun getImgAttr(element: Element?): String? {
|
||||
return when {
|
||||
element == null -> null
|
||||
element.hasAttr("data-original") -> element.attr("abs:data-original")
|
||||
element.hasAttr("data-src") -> element.attr("abs:data-src")
|
||||
element.hasAttr("data-bg") -> element.attr("abs:data-bg")
|
||||
else -> element.attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
open val requestPath = "manga-list.html"
|
||||
|
||||
open val popularSort = "sort=views"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/$requestPath?listType=pagination&page=$page&$popularSort&sort_type=DESC", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/$requestPath?")!!.newBuilder()
|
||||
.addQueryParameter("name", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> {
|
||||
val status = arrayOf("", "1", "2")[filter.state]
|
||||
url.addQueryParameter("m_status", status)
|
||||
}
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is GenreList -> {
|
||||
var genre = String()
|
||||
var ungenre = String()
|
||||
filter.state.forEach {
|
||||
if (it.isIncluded()) genre += ",${it.name}"
|
||||
if (it.isExcluded()) ungenre += ",${it.name}"
|
||||
}
|
||||
url.addQueryParameter("genre", genre)
|
||||
url.addQueryParameter("ungenre", ungenre)
|
||||
}
|
||||
is SortBy -> {
|
||||
url.addQueryParameter(
|
||||
"sort",
|
||||
when (filter.state?.index) {
|
||||
0 -> "name"
|
||||
1 -> "views"
|
||||
else -> "last_update"
|
||||
}
|
||||
)
|
||||
if (filter.state?.ascending == true)
|
||||
url.addQueryParameter("sort_type", "ASC")
|
||||
}
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/$requestPath?listType=pagination&page=$page&sort=last_update&sort_type=DESC", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { popularMangaFromElement(it) }
|
||||
|
||||
// check if there's a next page
|
||||
val hasNextPage = (document.select(popularMangaNextPageSelector())?.first()?.text() ?: "").let {
|
||||
if (it.contains(Regex("""\w*\s\d*\s\w*\s\d*"""))) {
|
||||
it.split(" ").let { pageOf -> pageOf[1] != pageOf[3] } // current page not last page
|
||||
} else {
|
||||
it.isNotEmpty() // standard next page check
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun popularMangaSelector() = "div.media, .thumb-item-flow"
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
open val headerSelector = "h3 a, .series-title a"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select("$headerSelector").let {
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
title = it.text()
|
||||
}
|
||||
thumbnail_url = element.select("img, .thumb-wrapper .img-in-ratio").imgAttr()
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
/**
|
||||
* can select one of 2 different types of elements
|
||||
* one is an element with text "page x of y", must be the first element if it's part of a collection
|
||||
* the other choice is the standard "next page" element (but most FMReader sources don't have this one)
|
||||
*/
|
||||
override fun popularMangaNextPageSelector() = "div.col-lg-9 button.btn-info, .pagination a:contains(»):not(.disabled)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.row").first()
|
||||
|
||||
return SManga.create().apply {
|
||||
author = infoElement.select("li a.btn-info").text()
|
||||
genre = infoElement.select("li a.btn-danger").joinToString { it.text() }
|
||||
status = parseStatus(infoElement.select("li a.btn-success").first()?.text())
|
||||
description = document.select("div.detail .content, div.row ~ div.row:has(h3:first-child) p, .summary-content p").text().trim()
|
||||
thumbnail_url = infoElement.select("img.thumbnail").imgAttr()
|
||||
}
|
||||
}
|
||||
|
||||
// languages: en, vi, tr
|
||||
fun parseStatus(status: String?): Int {
|
||||
val completedWords = setOf("completed", "complete", "incomplete", "đã hoàn thành", "tamamlandı", "hoàn thành")
|
||||
val ongoingWords = setOf("ongoing", "on going", "updating", "chưa hoàn thành", "đang cập nhật", "devam ediyor", "Đang tiến hành")
|
||||
return when {
|
||||
status == null -> SManga.UNKNOWN
|
||||
completedWords.any { it.equals(status, ignoreCase = true) } -> SManga.COMPLETED
|
||||
ongoingWords.any { it.equals(status, ignoreCase = true) } -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val mangaTitle = document.select(".manga-info h1, .manga-info h3").text()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, mangaTitle) }.distinctBy { it.url }
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return chapterFromElement(element, "")
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div#list-chapters p, table.table tr, .list-chapters > a"
|
||||
|
||||
open val chapterUrlSelector = "a"
|
||||
|
||||
open val chapterTimeSelector = "time, .chapter-time"
|
||||
|
||||
open val chapterNameAttrSelector = "title"
|
||||
|
||||
open fun chapterFromElement(element: Element, mangaTitle: String = ""): SChapter {
|
||||
return SChapter.create().apply {
|
||||
if (chapterUrlSelector != "") {
|
||||
element.select(chapterUrlSelector).first().let {
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
name = it.text().substringAfter("$mangaTitle ")
|
||||
}
|
||||
} else {
|
||||
element.let {
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
name = element.attr(chapterNameAttrSelector).substringAfter("$mangaTitle ")
|
||||
}
|
||||
}
|
||||
date_upload = element.select(chapterTimeSelector).let { if (it.hasText()) parseChapterDate(it.text()) else 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// gets the number from "1 day ago"
|
||||
open val dateValueIndex = 0
|
||||
|
||||
// gets the unit of time (day, week hour) from "1 day ago"
|
||||
open val dateWordIndex = 1
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
val value = date.split(' ')[dateValueIndex].toInt()
|
||||
val dateWord = date.split(' ')[dateWordIndex].let {
|
||||
if (it.contains("(")) {
|
||||
it.substringBefore("(")
|
||||
} else {
|
||||
it.substringBefore("s")
|
||||
}
|
||||
}
|
||||
|
||||
// languages: en, vi, es, tr
|
||||
return when (dateWord) {
|
||||
"min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"day", "ngày", "día", "gün" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"year", "năm", "año", "yıl" -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val pageListImageSelector = "img.chapter-img"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(pageListImageSelector).mapIndexed { i, img ->
|
||||
Page(i, document.location(), img.imgAttr())
|
||||
}
|
||||
}
|
||||
|
||||
protected fun base64PageListParse(document: Document): List<Page> {
|
||||
fun Element.decoded(): String {
|
||||
val attr =
|
||||
when {
|
||||
this.hasAttr("data-original") -> "data-original"
|
||||
this.hasAttr("data-src") -> "data-src"
|
||||
this.hasAttr("data-srcset") -> "data-srcset"
|
||||
this.hasAttr("data-aload") -> "data-aload"
|
||||
else -> "src"
|
||||
}
|
||||
return if (!this.attr(attr).contains(".")) {
|
||||
Base64.decode(this.attr(attr), Base64.DEFAULT).toString(Charset.defaultCharset())
|
||||
} else {
|
||||
this.attr("abs:$attr")
|
||||
}
|
||||
}
|
||||
|
||||
return document.select(pageListImageSelector).mapIndexed { i, img ->
|
||||
Page(i, document.location(), img.decoded())
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Status : Filter.Select<String>("Status", arrayOf("Any", "Completed", "Ongoing"))
|
||||
class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
||||
class Genre(name: String, val id: String = name.replace(' ', '+')) : Filter.TriState(name)
|
||||
private class SortBy : Filter.Sort("Sorted By", arrayOf("A-Z", "Most vỉews", "Last updated"), Selection(1, false))
|
||||
|
||||
// TODO: Country (leftover from original LHTranslation)
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Group", "group"),
|
||||
Status(),
|
||||
SortBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("div.panel-body a")].map((el,i) => `Genre("${el.innerText.trim()}")`).join(',\n')
|
||||
// on https://lhtranslation.net/search
|
||||
open fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("18+"),
|
||||
Genre("Adult"),
|
||||
Genre("Anime"),
|
||||
Genre("Comedy"),
|
||||
Genre("Comic"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Live action"),
|
||||
Genre("Manhua"),
|
||||
Genre("Manhwa"),
|
||||
Genre("Martial Art"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("One shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shojou Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Adventure"),
|
||||
Genre("Yaoi")
|
||||
)
|
||||
|
||||
// from manhwa18.com/search, removed a few that didn't return results/wouldn't be terribly useful
|
||||
fun getAdultGenreList() = listOf(
|
||||
Genre("18"),
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Anime"),
|
||||
Genre("Comedy"),
|
||||
Genre("Comic"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Live action"),
|
||||
Genre("Magic"),
|
||||
Genre("Manhua"),
|
||||
Genre("Manhwa"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("Oneshot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of life"),
|
||||
Genre("Smut"),
|
||||
Genre("Soft Yaoi"),
|
||||
Genre("Soft Yuri"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("VnComic"),
|
||||
Genre("Webtoon")
|
||||
)
|
||||
|
||||
// taken from readcomiconline.org/search
|
||||
fun getComicsGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adventure"),
|
||||
Genre("Anthology"),
|
||||
Genre("Anthropomorphic"),
|
||||
Genre("Biography"),
|
||||
Genre("Children"),
|
||||
Genre("Comedy"),
|
||||
Genre("Crime"),
|
||||
Genre("Drama"),
|
||||
Genre("Family"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Fighting"),
|
||||
Genre("GraphicNovels"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("LeadingLadies"),
|
||||
Genre("LGBTQ"),
|
||||
Genre("Literature"),
|
||||
Genre("Manga"),
|
||||
Genre("MartialArts"),
|
||||
Genre("Mature"),
|
||||
Genre("Military"),
|
||||
Genre("Mystery"),
|
||||
Genre("Mythology"),
|
||||
Genre("Personal"),
|
||||
Genre("Political"),
|
||||
Genre("Post-Apocalyptic"),
|
||||
Genre("Psychological"),
|
||||
Genre("Pulp"),
|
||||
Genre("Religious"),
|
||||
Genre("Robots"),
|
||||
Genre("Romance"),
|
||||
Genre("Schoollife"),
|
||||
Genre("Sci-Fi"),
|
||||
Genre("Sliceoflife"),
|
||||
Genre("Sport"),
|
||||
Genre("Spy"),
|
||||
Genre("Superhero"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Suspense"),
|
||||
Genre("Thriller"),
|
||||
Genre("Vampires"),
|
||||
Genre("VideoGames"),
|
||||
Genre("War"),
|
||||
Genre("Western"),
|
||||
Genre("Zombies")
|
||||
)
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.fmreader
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
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.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
class FMReaderFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
LHTranslation(),
|
||||
KissLove(),
|
||||
HanaScan(),
|
||||
RawLH(),
|
||||
Manhwa18(),
|
||||
EighteenLHPlus(),
|
||||
MangaTR(),
|
||||
Manhwa18Net(),
|
||||
Manhwa18NetRaw(),
|
||||
SayTruyen(),
|
||||
EpikManga(),
|
||||
ManhuaScan(),
|
||||
ManhwaSmut(),
|
||||
HeroScan()
|
||||
)
|
||||
}
|
||||
|
||||
/** For future sources: when testing and popularMangaRequest() returns a Jsoup error instead of results
|
||||
* most likely the fix is to override popularMangaNextPageSelector() */
|
||||
|
||||
class LHTranslation : FMReader("LHTranslation", "https://lhtranslation.net", "en")
|
||||
|
||||
class KissLove : FMReader("KissLove", "https://kissaway.net", "ja") {
|
||||
override fun pageListParse(document: Document): List<Page> = base64PageListParse(document)
|
||||
}
|
||||
|
||||
class HanaScan : FMReader("HanaScan (RawQQ)", "https://hanascan.com", "ja") {
|
||||
override fun popularMangaNextPageSelector() = "div.col-md-8 button"
|
||||
// Referer needs to be chapter URL
|
||||
override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headersBuilder().set("Referer", page.url).build())
|
||||
}
|
||||
|
||||
class HeroScan : FMReader("HeroScan", "https://heroscan.com", "en") {
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val originalRequest = chain.request()
|
||||
chain.proceed(originalRequest).let { response ->
|
||||
if (response.code() == 403 && originalRequest.url().host().contains("b-cdn")) {
|
||||
response.close()
|
||||
chain.proceed(originalRequest.newBuilder().removeHeader("Referer").addHeader("Referer", "https://isekaiscan.com").build())
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
class RawLH : FMReader("RawLH", "https://lovehug.net", "ja") {
|
||||
override val chapterUrlSelector = ""
|
||||
override fun pageListParse(document: Document): List<Page> = base64PageListParse(document)
|
||||
// Referer needs to be chapter URL
|
||||
override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headersBuilder().set("Referer", page.url).build())
|
||||
}
|
||||
|
||||
class Manhwa18 : FMReader("Manhwa18", "https://manhwa18.com", "en") {
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return if (page.imageUrl!!.contains("manhwa18")) {
|
||||
super.imageRequest(page)
|
||||
} else {
|
||||
GET(page.imageUrl!!, headers.newBuilder().removeAll("Referer").build())
|
||||
}
|
||||
}
|
||||
override fun getGenreList() = getAdultGenreList()
|
||||
}
|
||||
|
||||
class EighteenLHPlus : FMReader("18LHPlus", "https://18lhplus.com", "en") {
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val originalRequest = chain.request()
|
||||
chain.proceed(originalRequest).let { response ->
|
||||
if (response.code() == 403 && originalRequest.url().host().contains("mkklcdn")) {
|
||||
response.close()
|
||||
chain.proceed(originalRequest.newBuilder().removeHeader("Referer").addHeader("Referer", "https://manganelo.com").build())
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
override fun popularMangaNextPageSelector() = "div.col-lg-8 div.btn-group:first-of-type"
|
||||
override fun getGenreList() = getAdultGenreList()
|
||||
}
|
||||
|
||||
class MangaTR : FMReader("Manga-TR", "https://manga-tr.com", "tr") {
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
}
|
||||
override fun popularMangaNextPageSelector() = "div.btn-group:not(div.btn-block) button.btn-info"
|
||||
// TODO: genre search possible but a bit of a pain
|
||||
override fun getFilterList() = FilterList()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/arama.html?icerik=$query", headers)
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val mangas = mutableListOf<SManga>()
|
||||
|
||||
response.asJsoup().select("div.row a[data-toggle]")
|
||||
.filterNot { it.siblingElements().text().contains("Novel") }
|
||||
.map { mangas.add(searchMangaFromElement(it)) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
manga.setUrlWithoutDomain(element.attr("abs:href"))
|
||||
manga.title = element.text()
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
val infoElement = document.select("div#tab1").first()
|
||||
|
||||
manga.author = infoElement.select("table + table tr + tr td a").first()?.text()
|
||||
manga.artist = infoElement.select("table + table tr + tr td + td a").first()?.text()
|
||||
manga.genre = infoElement.select("div#tab1 table + table tr + tr td + td + td").text()
|
||||
manga.status = parseStatus(infoElement.select("div#tab1 table tr + tr td a").first().text())
|
||||
manga.description = infoElement.select("div.well").text().trim()
|
||||
manga.thumbnail_url = document.select("img.thumbnail").attr("abs:src")
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "tr.table-bordered"
|
||||
override val chapterUrlSelector = "td[align=left] > a"
|
||||
override val chapterTimeSelector = "td[align=right]"
|
||||
private val chapterListHeaders = headers.newBuilder().add("X-Requested-With", "XMLHttpRequest").build()
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val requestUrl = "$baseUrl/cek/fetch_pages_manga.php?manga_cek=${manga.url.substringAfter("manga-").substringBefore(".")}"
|
||||
return client.newCall(GET(requestUrl, chapterListHeaders))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response, requestUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun chapterListParse(response: Response, requestUrl: String): List<SChapter> {
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
var document = response.asJsoup()
|
||||
var moreChapters = true
|
||||
var nextPage = 2
|
||||
|
||||
// chapters are paginated
|
||||
while (moreChapters) {
|
||||
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
||||
if (document.select("a[data-page=$nextPage]").isNotEmpty()) {
|
||||
val body = FormBody.Builder()
|
||||
.add("page", nextPage.toString())
|
||||
.build()
|
||||
document = client.newCall(POST(requestUrl, chapterListHeaders, body)).execute().asJsoup()
|
||||
nextPage++
|
||||
} else {
|
||||
moreChapters = false
|
||||
}
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl/${chapter.url.substringAfter("cek/")}", headers)
|
||||
}
|
||||
|
||||
class Manhwa18Net : FMReader("Manhwa18.net", "https://manhwa18.net", "en") {
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/$requestPath?listType=pagination&page=$page&sort=views&sort_type=DESC&ungenre=raw", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/$requestPath?listType=pagination&page=$page&sort=last_update&sort_type=DESC&ungenre=raw", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val noRawsUrl = super.searchMangaRequest(page, query, filters).url().newBuilder().addQueryParameter("ungenre", "raw").toString()
|
||||
return GET(noRawsUrl, headers)
|
||||
}
|
||||
|
||||
override fun getGenreList() = getAdultGenreList()
|
||||
}
|
||||
|
||||
class Manhwa18NetRaw : FMReader("Manhwa18.net Raw", "https://manhwa18.net", "ko") {
|
||||
override val requestPath = "manga-list-genre-raw.html"
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val onlyRawsUrl = super.searchMangaRequest(page, query, filters).url().newBuilder().addQueryParameter("genre", "raw").toString()
|
||||
return GET(onlyRawsUrl, headers)
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(super.getFilterList().filterNot { it == GenreList(getGenreList()) })
|
||||
}
|
||||
|
||||
class SayTruyen : FMReader("Say Truyen", "https://saytruyen.com", "vi") {
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val info = document.select("div.row").first()
|
||||
return SManga.create().apply {
|
||||
author = info.select("div.row li:has(b:contains(Tác giả)) small").text()
|
||||
genre = info.select("div.row li:has(b:contains(Thể loại)) small a").joinToString { it.text() }
|
||||
status = parseStatus(info.select("div.row li:has(b:contains(Tình trạng)) a").text())
|
||||
description = document.select("div.description").text()
|
||||
thumbnail_url = info.select("img.thumbnail").attr("abs:src")
|
||||
}
|
||||
}
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return response.asJsoup().let { document ->
|
||||
document.select(chapterListSelector()).map {
|
||||
chapterFromElement(it).apply {
|
||||
scanlator = document.select("div.row li:has(b:contains(Nhóm dịch)) small").text()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun pageListParse(document: Document): List<Page> = super.pageListParse(document).onEach { it.imageUrl!!.trim() }
|
||||
}
|
||||
|
||||
class EpikManga : FMReader("Epik Manga", "https://www.epikmanga.com", "tr") {
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/seri-listesi?sorting=views&sorting-type=DESC&Sayfa=$page", headers)
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/seri-listesi?sorting=lastUpdate&sorting-type=DESC&Sayfa=$page", headers)
|
||||
override fun popularMangaNextPageSelector() = "ul.pagination li.active + li:not(.disabled)"
|
||||
|
||||
override val headerSelector = "h4 a"
|
||||
|
||||
// search wasn't working on source's website
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response, query)
|
||||
}
|
||||
}
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/seri-listesi?type=text", headers)
|
||||
private fun searchMangaParse(response: Response, query: String): MangasPage {
|
||||
val mangas = response.asJsoup().select("div.char.col-lg-4 a")
|
||||
.filter { it.text().contains(query, ignoreCase = true) }
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.text()
|
||||
}
|
||||
}
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.col-md-9 div.row").first()
|
||||
|
||||
return SManga.create().apply {
|
||||
status = parseStatus(infoElement.select("h4:contains(Durum:)").firstOrNull()?.ownText())
|
||||
author = infoElement.select("h4:contains(Yazar:)").firstOrNull()?.ownText()
|
||||
artist = infoElement.select("h4:contains(Çizer:)").firstOrNull()?.ownText()
|
||||
genre = infoElement.select("h4:contains(Türler:) a").joinToString { it.text() }
|
||||
thumbnail_url = infoElement.select("img.thumbnail").imgAttr()
|
||||
description = document.select("div.col-md-12 p").text()
|
||||
}
|
||||
}
|
||||
override fun chapterListSelector() = "table.table tbody tr"
|
||||
override fun getFilterList(): FilterList = FilterList()
|
||||
}
|
||||
|
||||
class ManhuaScan : FMReader("ManhuaScan", "https://manhuascan.com", "en")
|
||||
|
||||
class ManhwaSmut : FMReader("ManhwaSmut", "https://manhwasmut.com", "en") {
|
||||
private val noReferer = headersBuilder().removeAll("Referer").build()
|
||||
override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, if (page.imageUrl!!.contains("toonily")) noReferer else headers)
|
||||
}
|
Reference in New Issue
Block a user