[RU]MultiChan (MangaChan, HenChan, YaoiChan) (#12125)
* [RU]MultiChan (MangaChan, HenChan, YaoiChan) * id * simpleDateFormat
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
@ -1,47 +1,29 @@
|
|||||||
package eu.kanade.tachiyomi.extension.ru.henchan
|
package eu.kanade.tachiyomi.extension.ru.henchan
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import eu.kanade.tachiyomi.multisrc.multichan.MultiChan
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import java.net.URL
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.net.URL
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Henchan : ParsedHttpSource() {
|
class HenChan : MultiChan("HenChan", "https://y.hentaichan.live", "ru"){
|
||||||
|
|
||||||
override val name = "Henchan"
|
override val id: Long = 5504588601186153612
|
||||||
|
|
||||||
override val baseUrl = "https://xxxx.hentaichan.live"
|
|
||||||
|
|
||||||
override val lang = "ru"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/mostfavorites&sort=manga?offset=${20 * (page - 1)}", headers)
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/manga/new?offset=${20 * (page - 1)}", headers)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
|
||||||
@ -88,10 +70,6 @@ class Henchan : ParsedHttpSource() {
|
|||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.content_row"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".content_row:not(:has(div.item:containsOwn(Тип)))"
|
override fun searchMangaSelector() = ".content_row:not(:has(div.item:containsOwn(Тип)))"
|
||||||
|
|
||||||
private fun String.getHQThumbnail(): String {
|
private fun String.getHQThumbnail(): String {
|
||||||
@ -103,33 +81,13 @@ class Henchan : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = super.popularMangaFromElement(element)
|
||||||
manga.thumbnail_url = element.select("img").first().attr("src").getHQThumbnail()
|
manga.thumbnail_url = element.select("img").first().attr("src").getHQThumbnail()
|
||||||
manga.title = element.attr("title")
|
|
||||||
element.select("h2 > a").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
}
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
|
||||||
popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga =
|
|
||||||
popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "#pagination > a:contains(Вперед)"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "#nextlink, ${popularMangaNextPageSelector()}"
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
val manga = SManga.create()
|
val manga = super.mangaDetailsParse(document)
|
||||||
manga.title = document.select("title").text().substringBefore(" »")
|
|
||||||
manga.author = document.select(".row .item2 h2")[1].text()
|
|
||||||
manga.genre = document.select(".sidetag > a:eq(2)").joinToString { it.text() }
|
|
||||||
manga.description = document.select("#description").text().trim()
|
|
||||||
manga.thumbnail_url = document.select("img#cover").attr("abs:src").getHQThumbnail()
|
manga.thumbnail_url = document.select("img#cover").attr("abs:src").getHQThumbnail()
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
@ -245,6 +203,7 @@ class Henchan : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
return GET(url, Headers.Builder().add("Accept", "image/webp,image/apng").build())
|
return GET(url, Headers.Builder().add("Accept", "image/webp,image/apng").build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val html = response.body!!.string()
|
val html = response.body!!.string()
|
||||||
val prefix = "fullimg\": ["
|
val prefix = "fullimg\": ["
|
||||||
@ -258,12 +217,6 @@ class Henchan : ParsedHttpSource() {
|
|||||||
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
throw Exception("Not used")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
private class Genre(val id: String, @SuppressLint("DefaultLocale") name: String = id.replace('_', ' ').capitalize()) : Filter.TriState(name)
|
private class Genre(val id: String, @SuppressLint("DefaultLocale") name: String = id.replace('_', ' ').capitalize()) : Filter.TriState(name)
|
||||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
||||||
private class OrderBy : UriPartFilter(
|
private class OrderBy : UriPartFilter(
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
0
src/ru/mangachan/res/web_hi_res_512.png → multisrc/overrides/multichan/mangachan/res/web_hi_res_512.png
Executable file → Normal file
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
@ -1,42 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.extension.ru.mangachan
|
package eu.kanade.tachiyomi.extension.ru.mangachan
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.multichan.MultiChan
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Mangachan : ParsedHttpSource() {
|
class MangaChan : MultiChan("MangaChan", "https://manga-chan.me", "ru"){
|
||||||
|
|
||||||
override val id: Long = 7
|
override val id: Long = 7
|
||||||
|
|
||||||
override val name = "Mangachan"
|
|
||||||
|
|
||||||
override val baseUrl = "https://manga-chan.me"
|
|
||||||
|
|
||||||
override val lang = "ru"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
var pageNum = 1
|
var pageNum = 1
|
||||||
when {
|
when {
|
||||||
@ -111,126 +84,6 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/manga/new?offset=${20 * (page - 1)}")
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.content_row"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
val manga = SManga.create()
|
|
||||||
manga.thumbnail_url = element.select("div.manga_images img").first().attr("src")
|
|
||||||
manga.title = element.attr("title")
|
|
||||||
element.select("h2 > a").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
}
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
|
||||||
val manga = SManga.create()
|
|
||||||
manga.title = element.attr("title")
|
|
||||||
element.select("a:nth-child(1)").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
}
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a:contains(Далее)"
|
|
||||||
|
|
||||||
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
var hasNextPage = false
|
|
||||||
|
|
||||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
|
||||||
searchMangaFromElement(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
val nextSearchPage = document.select(searchMangaNextPageSelector())
|
|
||||||
if (nextSearchPage.isNotEmpty()) {
|
|
||||||
val query = document.select("input#searchinput").first().attr("value")
|
|
||||||
val pageNum = nextSearchPage.let { selector ->
|
|
||||||
val onClick = selector.attr("onclick")
|
|
||||||
onClick?.split("""\\d+""")
|
|
||||||
}
|
|
||||||
nextSearchPage.attr("href", "$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum")
|
|
||||||
hasNextPage = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val nextGenresPage = document.select(searchGenresNextPageSelector())
|
|
||||||
if (nextGenresPage.isNotEmpty()) {
|
|
||||||
hasNextPage = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(mangas, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
val infoElement = document.select("table.mangatitle").first()
|
|
||||||
val descElement = document.select("div#description").first()
|
|
||||||
val imgElement = document.select("img#cover").first()
|
|
||||||
val rawCategory = infoElement.select("tr:eq(1) > td:eq(1)").text()
|
|
||||||
val category = if (rawCategory.isNotEmpty()) {
|
|
||||||
rawCategory.lowercase()
|
|
||||||
} else {
|
|
||||||
"манга"
|
|
||||||
}
|
|
||||||
val manga = SManga.create()
|
|
||||||
manga.title = document.select("title").text().substringBefore(" »")
|
|
||||||
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
|
|
||||||
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text().split(",").plusElement(category).joinToString { it.trim() }
|
|
||||||
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
|
||||||
manga.description = descElement.textNodes().first().text().trim()
|
|
||||||
manga.thumbnail_url = imgElement.attr("src")
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(element: String): Int = when {
|
|
||||||
element.contains("перевод завершен") -> SManga.COMPLETED
|
|
||||||
element.contains("перевод продолжается") -> SManga.ONGOING
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
val chapter = SChapter.create()
|
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
chapter.date_upload = element.select("div.date").first()?.text()?.let {
|
|
||||||
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it)?.time ?: 0L
|
|
||||||
} ?: 0
|
|
||||||
return chapter
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val html = response.body!!.string()
|
|
||||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
|
||||||
val endIndex = html.indexOf(",]", beginIndex)
|
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
|
||||||
val pageUrls = trimmedHtml.split(',')
|
|
||||||
|
|
||||||
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
throw Exception("Not used")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
||||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||||
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
|
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
|
||||||
@ -246,10 +99,6 @@ class Mangachan : ParsedHttpSource() {
|
|||||||
GenreList(getGenreList())
|
GenreList(getGenreList())
|
||||||
)
|
)
|
||||||
|
|
||||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")]
|
|
||||||
* .map(el => `Genre("${el.getAttribute('href').substr(6)}")`).join(',\n')
|
|
||||||
* on https://mangachan.me/
|
|
||||||
*/
|
|
||||||
private fun getGenreList() = listOf(
|
private fun getGenreList() = listOf(
|
||||||
Genre("18_плюс"),
|
Genre("18_плюс"),
|
||||||
Genre("bdsm"),
|
Genre("bdsm"),
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@ -1,37 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.extension.ru.yaoichan
|
package eu.kanade.tachiyomi.extension.ru.yaoichan
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.multichan.MultiChan
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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 okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class Yaoichan : ParsedHttpSource() {
|
class YaoiChan : MultiChan("YaoiChan", "https://yaoi-chan.me", "ru"){
|
||||||
|
|
||||||
override val name = "Yaoichan"
|
override val id: Long = 2466512768990363955
|
||||||
|
|
||||||
override val baseUrl = "https://yaoi-chan.me"
|
|
||||||
|
|
||||||
override val lang = "ru"
|
|
||||||
|
|
||||||
override val supportsLatest = false
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder()
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
|
||||||
GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = if (query.isNotEmpty()) {
|
val url = if (query.isNotEmpty()) {
|
||||||
@ -99,87 +76,6 @@ class Yaoichan : ParsedHttpSource() {
|
|||||||
return GET(url, headers)
|
return GET(url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.content_row"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
val manga = SManga.create()
|
|
||||||
manga.thumbnail_url = element.select("div.manga_images img").first().attr("src")
|
|
||||||
manga.title = element.attr("title")
|
|
||||||
element.select("h2 > a").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
}
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used")
|
|
||||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used")
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("Not used")
|
|
||||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a:contains(Далее), ${popularMangaNextPageSelector()}"
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
val infoElement = document.select("table.mangatitle").first()
|
|
||||||
val descElement = document.select("div#description").first()
|
|
||||||
val imgElement = document.select("img#cover").first()
|
|
||||||
val rawCategory = infoElement.select("tr:eq(1) > td:eq(1)").text()
|
|
||||||
val category = if (rawCategory.isNotEmpty()) {
|
|
||||||
rawCategory.lowercase()
|
|
||||||
} else {
|
|
||||||
"манга"
|
|
||||||
}
|
|
||||||
val manga = SManga.create()
|
|
||||||
manga.title = document.select("title").text().substringBefore(" »")
|
|
||||||
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
|
|
||||||
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text().split(",").plusElement(category).joinToString { it.trim() }
|
|
||||||
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
|
||||||
manga.description = descElement.textNodes().first().text().trim()
|
|
||||||
manga.thumbnail_url = imgElement.attr("src")
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(element: String): Int = when {
|
|
||||||
element.contains("перевод завершен") -> SManga.COMPLETED
|
|
||||||
element.contains("перевод продолжается") -> SManga.ONGOING
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
val chapter = SChapter.create()
|
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
chapter.date_upload = element.select("div.date").first()?.text()?.let {
|
|
||||||
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it)?.time ?: 0L
|
|
||||||
} ?: 0
|
|
||||||
return chapter
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val html = response.body!!.string()
|
|
||||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
|
||||||
val endIndex = html.indexOf(",]", beginIndex)
|
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
|
||||||
val pageUrls = trimmedHtml.split(',')
|
|
||||||
|
|
||||||
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
throw Exception("Not used")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
||||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||||
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
|
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
|
||||||
@ -195,10 +91,6 @@ class Yaoichan : ParsedHttpSource() {
|
|||||||
GenreList(getGenreList())
|
GenreList(getGenreList())
|
||||||
)
|
)
|
||||||
|
|
||||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")]
|
|
||||||
* .map(el => `Genre("${el.getAttribute('href').substr(6)}")`).join(',\n')
|
|
||||||
* on https://yaoi-chan.me/catalog
|
|
||||||
*/
|
|
||||||
private fun getGenreList() = listOf(
|
private fun getGenreList() = listOf(
|
||||||
Genre("18 плюс"),
|
Genre("18 плюс"),
|
||||||
Genre("bdsm"),
|
Genre("bdsm"),
|
@ -0,0 +1,26 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.multichan
|
||||||
|
|
||||||
|
import generator.ThemeSourceData.SingleLang
|
||||||
|
import generator.ThemeSourceGenerator
|
||||||
|
|
||||||
|
class ChanGenerator: ThemeSourceGenerator {
|
||||||
|
|
||||||
|
override val themePkg = "multichan"
|
||||||
|
|
||||||
|
override val themeClass = "MultiChan"
|
||||||
|
|
||||||
|
override val baseVersionCode: Int = 1
|
||||||
|
|
||||||
|
override val sources = listOf(
|
||||||
|
SingleLang("MangaChan", "https://manga-chan.me", "ru", overrideVersionCode = 14),
|
||||||
|
SingleLang("HenChan", "https://y.hentaichan.live", "ru",isNsfw = true, overrideVersionCode = 35),
|
||||||
|
SingleLang("YaoiChan", "https://yaoi-chan.me", "ru",isNsfw = true, overrideVersionCode = 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
ChanGenerator().createAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.multichan
|
||||||
|
|
||||||
|
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 okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
abstract class MultiChan(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
final override val lang: String
|
||||||
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.client.newBuilder()
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
|
GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/manga/new?offset=${20 * (page - 1)}")
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.content_row"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.thumbnail_url = element.select("img").first().attr("src")
|
||||||
|
manga.title = element.attr("title")
|
||||||
|
element.select("h2 > a").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||||
|
popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "a:contains(Далее)"
|
||||||
|
|
||||||
|
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
var hasNextPage = false
|
||||||
|
|
||||||
|
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextSearchPage = document.select(searchMangaNextPageSelector())
|
||||||
|
if (nextSearchPage.isNotEmpty()) {
|
||||||
|
val query = document.select("input#searchinput").first().attr("value")
|
||||||
|
val pageNum = nextSearchPage.let { selector ->
|
||||||
|
val onClick = selector.attr("onclick")
|
||||||
|
onClick?.split("""\\d+""")
|
||||||
|
}
|
||||||
|
nextSearchPage.attr("href", "$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum")
|
||||||
|
hasNextPage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextGenresPage = document.select(searchGenresNextPageSelector())
|
||||||
|
if (nextGenresPage.isNotEmpty()) {
|
||||||
|
hasNextPage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val infoElement = document.select("#info_wrap tr,#info_wrap > div")
|
||||||
|
val descElement = document.select("div#description").first()
|
||||||
|
val rawCategory = infoElement.select(":contains(Тип) a").text().lowercase()
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.title = document.select("title").text().substringBefore(" »")
|
||||||
|
manga.author = infoElement.select(":contains(Автор)").text()
|
||||||
|
manga.genre = rawCategory + ", " + document.select(".sidetags ul a:last-child").joinToString { it.text() }
|
||||||
|
manga.status = parseStatus(infoElement.select(":contains(Загружено)").text())
|
||||||
|
manga.description = descElement.textNodes().first().text().trim()
|
||||||
|
manga.thumbnail_url = document.select("img#cover").first().attr("src")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(element: String): Int = when {
|
||||||
|
element.contains("перевод завершен") -> SManga.COMPLETED
|
||||||
|
element.contains("перевод продолжается") -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = simpleDateFormat.parse(element.select("div.date").first().text())?.time ?: 0L
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val html = response.body!!.string()
|
||||||
|
val beginIndex = html.indexOf("fullimg\":[") + 10
|
||||||
|
val endIndex = html.indexOf(",]", beginIndex)
|
||||||
|
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
||||||
|
val pageUrls = trimmedHtml.split(',')
|
||||||
|
|
||||||
|
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
throw Exception("Not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.US) }
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
|
||||||
apply plugin: 'kotlinx-serialization'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
extName = 'Henchan'
|
|
||||||
pkgNameSuffix = 'ru.henchan'
|
|
||||||
extClass = '.Henchan'
|
|
||||||
extVersionCode = 35
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -1,11 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
extName = 'Mangachan'
|
|
||||||
pkgNameSuffix = 'ru.mangachan'
|
|
||||||
extClass = '.Mangachan'
|
|
||||||
extVersionCode = 14
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -1,12 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
extName = 'Yaoichan'
|
|
||||||
pkgNameSuffix = 'ru.yaoichan'
|
|
||||||
extClass = '.Yaoichan'
|
|
||||||
extVersionCode = 4
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|