Move Nicomanga to fmreader multisrc theme (#627)
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
82
multisrc/overrides/fmreader/nicomanga/src/Nicomanga.kt
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.ja.nicomanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
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.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
class Nicomanga : FMReader("Nicomanga", "https://nicomanga.com", "ja") {
|
||||||
|
override val client = super.client.newBuilder()
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
// =========================== Manga Details ============================
|
||||||
|
|
||||||
|
override val infoElementSelector = ".card-body > div.row"
|
||||||
|
override val mangaDetailsSelectorGenre = "li:has(b:contains(Genre)) a.btn-danger"
|
||||||
|
|
||||||
|
// ============================== Chapters ==============================
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
val slug = urlRegex.find(manga.url)?.groupValues?.get(1) ?: throw Exception("Unable to get slug")
|
||||||
|
val headers = headersBuilder().apply {
|
||||||
|
add("Accept", "*/*")
|
||||||
|
add("Host", baseUrl.toHttpUrl().host)
|
||||||
|
set("Referer", baseUrl + manga.url)
|
||||||
|
}.build()
|
||||||
|
return GET("$baseUrl/app/manga/controllers/cont.Listchapterapi.php?slug=$slug", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, mangaTitle: String): SChapter = SChapter.create().apply {
|
||||||
|
element.select(chapterUrlSelector).first()!!.let {
|
||||||
|
setUrlWithoutDomain("$baseUrl/${it.attr("href")}")
|
||||||
|
name = it.attr("title")
|
||||||
|
}
|
||||||
|
|
||||||
|
date_upload = element.select(chapterTimeSelector)
|
||||||
|
.let { if (it.hasText()) parseRelativeDate(it.text()) else 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================== Pages ================================
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val id = chapterIdRegex.find(response.use { it.body.string() })?.groupValues?.get(1) ?: throw Exception("chapter-id not found")
|
||||||
|
val doc = client.newCall(
|
||||||
|
GET("$baseUrl/app/manga/controllers/cont.imgsList.php?cid=$id", headers),
|
||||||
|
).execute().asJsoup()
|
||||||
|
return doc.select("img.chapter-img[data-src]").mapIndexed { i, page ->
|
||||||
|
Page(i + 1, imageUrl = page.attr("data-src"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
override fun getImgAttr(element: Element?): String? {
|
||||||
|
return when {
|
||||||
|
element?.attr("style")?.contains("background-image") == true -> {
|
||||||
|
val url = thumbnailURLRegex.find(element.attr("style"))?.groupValues?.get(1)
|
||||||
|
when {
|
||||||
|
url?.startsWith("/") == true -> baseUrl + url
|
||||||
|
else -> url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.getImgAttr(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val thumbnailURLRegex = Regex("background-image:[^;]?url\\s*\\(\\s*'?([^')]+?)'?(\\)|\$)")
|
||||||
|
private val urlRegex = Regex("manga-([^/]+)\\.html\$")
|
||||||
|
private val chapterIdRegex = Regex("imgsListchap\\((\\d+)")
|
||||||
|
}
|
||||||
|
}
|
@ -76,14 +76,10 @@ abstract class FMReader(
|
|||||||
}
|
}
|
||||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||||
is GenreList -> {
|
is GenreList -> {
|
||||||
var genre = String()
|
val included = filter.state.filter { it.isIncluded() }.joinToString(",") { it.name }
|
||||||
var ungenre = String()
|
val excluded = filter.state.filter { it.isExcluded() }.joinToString(",") { it.name }
|
||||||
filter.state.forEach {
|
url.addQueryParameter("genre", included)
|
||||||
if (it.isIncluded()) genre += ",${it.name}"
|
url.addQueryParameter("ungenre", excluded)
|
||||||
if (it.isExcluded()) ungenre += ",${it.name}"
|
|
||||||
}
|
|
||||||
url.addQueryParameter("genre", genre)
|
|
||||||
url.addQueryParameter("ungenre", ungenre)
|
|
||||||
}
|
}
|
||||||
is SortBy -> {
|
is SortBy -> {
|
||||||
url.addQueryParameter(
|
url.addQueryParameter(
|
||||||
@ -161,17 +157,28 @@ abstract class FMReader(
|
|||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
// Manga Details Selector
|
||||||
|
open val infoElementSelector = "div.row"
|
||||||
|
open val mangaDetailsSelectorAuthor = "li a.btn-info"
|
||||||
|
open val mangaDetailsSelectorGenre = "li a.btn-danger"
|
||||||
|
open val mangaDetailsSelectorStatus = "li a.btn-success"
|
||||||
|
open val mangaDetailsSelectorDescription = "div.detail .content, div.row ~ div.row:has(h3:first-child) p, .summary-content p"
|
||||||
|
open val mangaDetailsSelectorThumbnail = "img.thumbnail"
|
||||||
|
|
||||||
|
open val altNameSelector = "li:contains(Other names)"
|
||||||
|
open val altName = "Alternative Name" // the alt name already contains ": " eg. ": alt name1, alt name2"
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
val infoElement = document.select("div.row").first()!!
|
val infoElement = document.select(infoElementSelector).first()!!
|
||||||
|
|
||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
author = infoElement.select("li a.btn-info").eachText().filter {
|
author = infoElement.select(mangaDetailsSelectorAuthor).eachText().filter {
|
||||||
it.equals("Updating", true).not()
|
it.equals("Updating", true).not()
|
||||||
}.joinToString().takeIf { it.isNotBlank() }
|
}.joinToString().takeIf { it.isNotBlank() }
|
||||||
genre = infoElement.select("li a.btn-danger").joinToString { it.text() }
|
genre = infoElement.select(mangaDetailsSelectorGenre).joinToString { it.text() }
|
||||||
status = parseStatus(infoElement.select("li a.btn-success").first()?.text())
|
status = parseStatus(infoElement.select(mangaDetailsSelectorStatus).first()?.text())
|
||||||
description = document.select("div.detail .content, div.row ~ div.row:has(h3:first-child) p, .summary-content p").text().trim()
|
description = document.select(mangaDetailsSelectorDescription).text().trim()
|
||||||
thumbnail_url = infoElement.select("img.thumbnail").imgAttr()
|
thumbnail_url = infoElement.select(mangaDetailsSelectorThumbnail).imgAttr()
|
||||||
|
|
||||||
// add alternative name to manga description
|
// add alternative name to manga description
|
||||||
infoElement.select(altNameSelector).firstOrNull()?.ownText()?.let {
|
infoElement.select(altNameSelector).firstOrNull()?.ownText()?.let {
|
||||||
@ -185,9 +192,6 @@ abstract class FMReader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open val altNameSelector = "li:contains(Other names)"
|
|
||||||
open val altName = "Alternative Name" // the alt name already contains ": " eg. ": alt name1, alt name2"
|
|
||||||
|
|
||||||
// languages: en, vi, tr
|
// languages: en, vi, tr
|
||||||
fun parseStatus(status: String?): Int {
|
fun parseStatus(status: String?): Int {
|
||||||
val completedWords = setOf(
|
val completedWords = setOf(
|
||||||
@ -251,7 +255,7 @@ abstract class FMReader(
|
|||||||
// gets the unit of time (day, week hour) from "1 day ago"
|
// gets the unit of time (day, week hour) from "1 day ago"
|
||||||
open val dateWordIndex = 1
|
open val dateWordIndex = 1
|
||||||
|
|
||||||
private fun parseRelativeDate(date: String): Long {
|
open fun parseRelativeDate(date: String): Long {
|
||||||
val value = date.split(' ')[dateValueIndex].toInt()
|
val value = date.split(' ')[dateValueIndex].toInt()
|
||||||
val dateWord = date.split(' ')[dateWordIndex].let {
|
val dateWord = date.split(' ')[dateWordIndex].let {
|
||||||
if (it.contains("(")) {
|
if (it.contains("(")) {
|
||||||
|
@ -14,9 +14,10 @@ class FMReaderGenerator : ThemeSourceGenerator {
|
|||||||
override val sources = listOf(
|
override val sources = listOf(
|
||||||
SingleLang("KissLove", "https://klz9.com", "ja", isNsfw = true, overrideVersionCode = 4),
|
SingleLang("KissLove", "https://klz9.com", "ja", isNsfw = true, overrideVersionCode = 4),
|
||||||
SingleLang("Manga-TR", "https://manga-tr.com", "tr", className = "MangaTR", overrideVersionCode = 3),
|
SingleLang("Manga-TR", "https://manga-tr.com", "tr", className = "MangaTR", overrideVersionCode = 3),
|
||||||
|
SingleLang("Manga1000", "https://manga1000.top", "ja"),
|
||||||
|
SingleLang("Nicomanga", "https://nicomanga.com", "ja", isNsfw = true),
|
||||||
SingleLang("Say Truyen", "https://saytruyenvip.com", "vi", overrideVersionCode = 3),
|
SingleLang("Say Truyen", "https://saytruyenvip.com", "vi", overrideVersionCode = 3),
|
||||||
SingleLang("WeLoveManga", "https://weloma.art", "ja", pkgName = "rawlh", isNsfw = true, overrideVersionCode = 5),
|
SingleLang("WeLoveManga", "https://weloma.art", "ja", pkgName = "rawlh", isNsfw = true, overrideVersionCode = 5),
|
||||||
SingleLang("Manga1000", "https://manga1000.top", "ja"),
|
|
||||||
SingleLang("WeLoveMangaOne", "https://welovemanga.one", "ja", isNsfw = true, overrideVersionCode = 1),
|
SingleLang("WeLoveMangaOne", "https://welovemanga.one", "ja", isNsfw = true, overrideVersionCode = 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest />
|
|
@ -1,7 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Nicomanga'
|
|
||||||
extClass = '.Nicomanga'
|
|
||||||
extVersionCode = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,146 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.ja.nicomanga
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
class Nicomanga : HttpSource() {
|
|
||||||
companion object {
|
|
||||||
private val thumbnailURLRegex: Regex = "background-image:[^;]url\\s*\\(\\s*'([^?']+)".toRegex()
|
|
||||||
|
|
||||||
private val statusRegex: Regex = "-([^.]+)".toRegex()
|
|
||||||
|
|
||||||
private val urlRegex: Regex = "manga-([^/]+)\\.html\$".toRegex()
|
|
||||||
|
|
||||||
private val chapterIdRegex: Regex = "imgsListchap\\((\\d+)".toRegex()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val baseUrl: String = "https://nicomanga.com"
|
|
||||||
|
|
||||||
override val lang: String = "ja"
|
|
||||||
|
|
||||||
override val name: String = "Nicomanga"
|
|
||||||
|
|
||||||
override val supportsLatest: Boolean = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
private fun mangaListParse(response: Response): MangasPage {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val hasNextPage = (
|
|
||||||
doc.select(".pagination li:last-of-type").size > 0 &&
|
|
||||||
doc.select(".pagination li:last-of-type")[0].text() == "»" &&
|
|
||||||
doc.select(".pagination li:last-of-type a.disabled").size == 0
|
|
||||||
) || doc.select(".pagination li:last-of-type a.active").size == 0
|
|
||||||
|
|
||||||
val mangas = doc.select(".row > .thumb-item-flow").map { manga ->
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(manga.selectFirst(".series-title a")!!.absUrl("href"))
|
|
||||||
title = manga.selectFirst(".series-title")?.text()!!
|
|
||||||
thumbnail_url = thumbnailURLRegex.find(manga.selectFirst(".img-in-ratio.lazyloaded")!!.attr("style"))!!.groupValues[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return MangasPage(mangas, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val url = "$baseUrl/manga-list.html".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("sort", "last_update")
|
|
||||||
.addQueryParameter("sort_type", "DESC")
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = mangaListParse(response)
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
val url = "$baseUrl/manga-list.html".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("sort", "views")
|
|
||||||
.addQueryParameter("sort_type", "DESC")
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage = mangaListParse(response)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/manga-list.html".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("artist", "")
|
|
||||||
.addQueryParameter("author", "")
|
|
||||||
.addQueryParameter("group", "")
|
|
||||||
.addQueryParameter("m_status", "")
|
|
||||||
.addQueryParameter("name", query)
|
|
||||||
.addQueryParameter("genre", "")
|
|
||||||
.addQueryParameter("ungenre", "")
|
|
||||||
.addQueryParameter("magazine", "")
|
|
||||||
.addQueryParameter("sort", "last_update")
|
|
||||||
.addQueryParameter("sort_type", "DESC")
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = mangaListParse(response)
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
author = doc.select("ul.manga-info a[href^=\"manga-author\"]").joinToString { it.text() }
|
|
||||||
genre = doc.select("ul.manga-info a[href^=\"manga-list-genre\"]").joinToString { it.text() }
|
|
||||||
val statusText = statusRegex.find(doc.select(".manga-info li:has(i.fa-spinner) a").attr("href"))?.run { groupValues[1] }
|
|
||||||
status = when (statusText) {
|
|
||||||
"on-going" -> SManga.ONGOING
|
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
|
||||||
val slug = urlRegex.find(manga.url)!!.groupValues[1]
|
|
||||||
return GET("$baseUrl/app/manga/controllers/cont.Listchapterapi.php?slug=$slug")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
val chapterList = doc.select("ul > a")
|
|
||||||
val chapterPrefix = "$baseUrl/app/manga/controllers"
|
|
||||||
|
|
||||||
val chapters = chapterList.map { chapter ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = chapter.attr("title").trim()
|
|
||||||
val url = chapter.absUrl("href").run {
|
|
||||||
takeIf { startsWith(chapterPrefix) }
|
|
||||||
?.replaceFirst(chapterPrefix, baseUrl)
|
|
||||||
?: this
|
|
||||||
}
|
|
||||||
setUrlWithoutDomain(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val id = chapterIdRegex.find(response.body.string())?.groupValues?.get(1) ?: throw Exception("chapter-id not found")
|
|
||||||
val r = client.newCall(GET("$baseUrl/app/manga/controllers/cont.imgsList.php?cid=$id", headers)).execute()
|
|
||||||
val doc = r.asJsoup()
|
|
||||||
return doc.select("img.chapter-img").mapIndexed { i, page ->
|
|
||||||
Page(i + 1, imageUrl = page.attr("data-src"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|