[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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import eu.kanade.tachiyomi.multisrc.multichan.MultiChan
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.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 java.net.URL
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
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
|
||||
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 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 val id: Long = 5504588601186153612
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
|
||||
@ -88,10 +70,6 @@ class Henchan : ParsedHttpSource() {
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.content_row"
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaSelector() = ".content_row:not(:has(div.item:containsOwn(Тип)))"
|
||||
|
||||
private fun String.getHQThumbnail(): String {
|
||||
@ -103,33 +81,13 @@ class Henchan : ParsedHttpSource() {
|
||||
}
|
||||
|
||||
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.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 popularMangaNextPageSelector() = "#pagination > a:contains(Вперед)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = "#nextlink, ${popularMangaNextPageSelector()}"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
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()
|
||||
val manga = super.mangaDetailsParse(document)
|
||||
manga.thumbnail_url = document.select("img#cover").attr("abs:src").getHQThumbnail()
|
||||
return manga
|
||||
}
|
||||
@ -245,6 +203,7 @@ class Henchan : ParsedHttpSource() {
|
||||
}
|
||||
return GET(url, Headers.Builder().add("Accept", "image/webp,image/apng").build())
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body!!.string()
|
||||
val prefix = "fullimg\": ["
|
||||
@ -258,12 +217,6 @@ class Henchan : ParsedHttpSource() {
|
||||
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 GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
||||
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
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.multichan.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
|
||||
|
||||
class Mangachan : ParsedHttpSource() {
|
||||
class MangaChan : MultiChan("MangaChan", "https://manga-chan.me", "ru"){
|
||||
|
||||
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 {
|
||||
var pageNum = 1
|
||||
when {
|
||||
@ -111,126 +84,6 @@ class Mangachan : ParsedHttpSource() {
|
||||
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 Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
|
||||
@ -246,10 +99,6 @@ class Mangachan : ParsedHttpSource() {
|
||||
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(
|
||||
Genre("18_плюс"),
|
||||
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
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.multichan.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.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.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 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 val id: Long = 2466512768990363955
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotEmpty()) {
|
||||
@ -99,87 +76,6 @@ class Yaoichan : ParsedHttpSource() {
|
||||
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 Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
|
||||
@ -195,10 +91,6 @@ class Yaoichan : ParsedHttpSource() {
|
||||
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(
|
||||
Genre("18 плюс"),
|
||||
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"
|