Initial commit
2
src/ru/acomics/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/ru/acomics/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'AComics'
|
||||
pkgNameSuffix = 'ru.acomics'
|
||||
extClass = '.AComics'
|
||||
extVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/acomics/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/ru/acomics/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/ru/acomics/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/ru/acomics/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/ru/acomics/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/ru/acomics/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
@@ -0,0 +1,198 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.acomics
|
||||
|
||||
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.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.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.net.URLEncoder
|
||||
|
||||
class AComics : ParsedHttpSource() {
|
||||
|
||||
override val name = "AComics"
|
||||
|
||||
override val baseUrl = "https://acomics.ru"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
private val cookiesHeader by lazy {
|
||||
val cookies = mutableMapOf<String, String>()
|
||||
cookies["ageRestrict"] = "17"
|
||||
buildCookies(cookies)
|
||||
}
|
||||
|
||||
private fun buildCookies(cookies: Map<String, String>) =
|
||||
cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||
}
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val newReq = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.addHeader("Cookie", cookiesHeader)
|
||||
.build()
|
||||
|
||||
chain.proceed(newReq)
|
||||
}.build()
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/comics?categories=&ratings[]=1&ratings[]=2&ratings[]=3&ratings[]=4&ratings[]=5ratings[]=6&&type=0&updatable=0&subscribe=0&issue_count=2&sort=subscr_count&skip=${10 * (page - 1)}", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/comics?categories=&ratings[]=1&ratings[]=2&ratings[]=3&ratings[]=4&ratings[]=5ratings[]=6&&type=0&updatable=0&subscribe=0&issue_count=2&sort=last_update&skip=${10 * (page - 1)}", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url: String = if (query.isNotEmpty()) {
|
||||
"$baseUrl/search?keyword=$query"
|
||||
} else {
|
||||
val categories = mutableListOf<Int>()
|
||||
var status = "0"
|
||||
val rating = mutableListOf<Int>()
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is GenreList -> {
|
||||
filter.state.forEach {
|
||||
if (it.state) {
|
||||
categories.add(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Status -> {
|
||||
if (filter.state == 1) {
|
||||
status = "no"
|
||||
}
|
||||
if (filter.state == 2) {
|
||||
status = "yes"
|
||||
}
|
||||
}
|
||||
is RatingList -> {
|
||||
filter.state.forEach {
|
||||
if (it.state) {
|
||||
rating.add(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
"$baseUrl/comics?categories=${categories.joinToString(",")}&${rating.joinToString { "ratings[]=$it" }}&type=0&updatable=$status&subscribe=0&issue_count=2&sort=subscr_count&skip=${10 * (page - 1)}"
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "table.list-loadable > tbody > tr"
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = baseUrl + element.select("a > img").first()!!.attr("src")
|
||||
element.select("div.title > a").first()!!.let {
|
||||
manga.setUrlWithoutDomain(it.attr("href") + "/about")
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "span.button:not(:has(a)) + span.button > a"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select(".about-summary").first()!!
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select(".about-summary > p:contains(Автор)").text().split(":")[1]
|
||||
manga.genre = infoElement.select("a.button").joinToString { it.text() }
|
||||
manga.description = infoElement.ownText()
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val res = mutableListOf<SChapter>()
|
||||
val count = response.asJsoup()
|
||||
.select(".about-summary > p:contains(Количество выпусков:)")
|
||||
.text()
|
||||
.split("Количество выпусков: ")[1].toInt()
|
||||
|
||||
for (index in count downTo 1) {
|
||||
val chapter = SChapter.create()
|
||||
chapter.chapter_number = index.toFloat()
|
||||
chapter.name = index.toString()
|
||||
val url = response.request.url.toString().split("/about")[0].split(baseUrl)[1]
|
||||
chapter.url = "$url/$index"
|
||||
res.add(chapter)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): Nothing = throw Exception("Not used")
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = throw Exception("Not used")
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val imageElement = document.select("img#mainImage").first()!!
|
||||
return listOf(Page(0, imageUrl = baseUrl + imageElement.attr("src")))
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Категории", genres)
|
||||
private class Genre(name: String, val id: Int) : Filter.CheckBox(name)
|
||||
private class Rating(name: String, val id: Int) : Filter.CheckBox(name, state = true)
|
||||
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Завершенный", "Продолжающийся"))
|
||||
|
||||
private class RatingList : Filter.Group<Rating>(
|
||||
"Возрастная категория",
|
||||
listOf(
|
||||
Rating("???", 1),
|
||||
Rating("0+", 2),
|
||||
Rating("6+", 3),
|
||||
Rating("12+", 4),
|
||||
Rating("16+", 5),
|
||||
Rating("18+", 6),
|
||||
),
|
||||
)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Status(),
|
||||
RatingList(),
|
||||
GenreList(getGenreList()),
|
||||
)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Животные", 1),
|
||||
Genre("Драма", 2),
|
||||
Genre("Фэнтези", 3),
|
||||
Genre("Игры", 4),
|
||||
Genre("Юмор", 5),
|
||||
Genre("Журнал", 6),
|
||||
Genre("Паранормальное", 7),
|
||||
Genre("Конец света", 8),
|
||||
Genre("Романтика", 9),
|
||||
Genre("Фантастика", 10),
|
||||
Genre("Бытовое", 11),
|
||||
Genre("Стимпанк", 12),
|
||||
Genre("Супергерои", 13),
|
||||
)
|
||||
}
|
||||
2
src/ru/comx/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/ru/comx/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Com-X'
|
||||
pkgNameSuffix = 'ru.comx'
|
||||
extClass = '.ComX'
|
||||
extVersionCode = 27
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/comx/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/ru/comx/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 948 B |
BIN
src/ru/comx/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/ru/comx/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/ru/comx/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/ru/comx/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
573
src/ru/comx/src/eu/kanade/tachiyomi/extension/ru/comx/ComX.kt
Normal file
@@ -0,0 +1,573 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.comx
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
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.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 kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ComX : ParsedHttpSource() {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val name = "Com-X"
|
||||
|
||||
override val baseUrl = "https://com-x.life"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
private val cookieManager by lazy { CookieManager.getInstance() }
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.rateLimit(3)
|
||||
.cookieJar(
|
||||
object : CookieJar {
|
||||
// Syncs okhttp with WebView cookies, allowing logged-in users do logged-in stuff
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
for (cookie in cookies) {
|
||||
cookieManager.setCookie(url.toString(), cookie.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
|
||||
val cookiesString = cookieManager.getCookie(url.toString())
|
||||
|
||||
if (cookiesString != null && cookiesString.isNotEmpty()) {
|
||||
val cookieHeaders = cookiesString.split("; ").toList()
|
||||
val cookies = mutableListOf<Cookie>()
|
||||
for (header in cookieHeaders) {
|
||||
cookies.add(Cookie.parse(url, header)!!)
|
||||
}
|
||||
// Adds age verification cookies to access mature comics
|
||||
return if (url.toString().contains("/reader/")) {
|
||||
cookies.apply {
|
||||
add(
|
||||
Cookie.Builder()
|
||||
.domain(baseUrl.substringAfter("//"))
|
||||
.path("/")
|
||||
.name("adult")
|
||||
.value(
|
||||
url.toString().substringAfter("/reader/")
|
||||
.substringBefore("/"),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
cookies
|
||||
}
|
||||
} else {
|
||||
return mutableListOf()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.addInterceptor { chain ->
|
||||
val originalRequest = chain.request()
|
||||
val response = chain.proceed(originalRequest)
|
||||
if (response.code == 404 && response.asJsoup().toString().contains("Protected by Batman")) {
|
||||
throw IOException("Antibot, попробуйте пройти капчу в WebView")
|
||||
}
|
||||
response
|
||||
}
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Tachiyomi")
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList())
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
return MangasPage(mangas, mangas.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.short"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = baseUrl + element.select("img").first()!!.attr("data-src")
|
||||
element.select(".readed__title a").first()!!.let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
// Russian's titles prevails. +Site bad search English titles.
|
||||
manga.title = it.text().replace(" / ", " | ").split(" | ").last().trim()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): Nothing? = null
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
return MangasPage(mangas, mangas.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = "ul#content-load li.latest"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = baseUrl + element.select("img").first()!!.attr("src").replace("mini/mini", "mini/mid")
|
||||
element.select("a.latest__title").first()!!.let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
// Russian's titles prevails. +Site bad search English titles.
|
||||
manga.title = it.text().replace(" / ", " | ").split(" | ").last().trim()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
override fun latestUpdatesNextPageSelector(): Nothing? = null
|
||||
|
||||
// Search
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.isNotEmpty()) {
|
||||
return POST(
|
||||
"$baseUrl/index.php?do=search",
|
||||
body = FormBody.Builder()
|
||||
.add("do", "search")
|
||||
.add("subaction", "search")
|
||||
.add("story", query)
|
||||
.add("search_start", page.toString())
|
||||
.build(),
|
||||
headers = headers,
|
||||
)
|
||||
}
|
||||
val mutableGenre = mutableListOf<String>()
|
||||
val mutableType = mutableListOf<String>()
|
||||
val mutableAge = mutableListOf<String>()
|
||||
var orderBy = "rating"
|
||||
var ascEnd = "desc"
|
||||
val sectionPub = mutableListOf<String>()
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
orderBy = arrayOf("date", "rating", "news_read", "comm_num", "title")[filter.state!!.index]
|
||||
ascEnd = if (filter.state!!.ascending) "asc" else "desc"
|
||||
}
|
||||
is AgeList -> filter.state.forEach { age ->
|
||||
if (age.state) {
|
||||
mutableAge += age.id
|
||||
}
|
||||
}
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
if (genre.state) {
|
||||
mutableGenre += genre.id
|
||||
}
|
||||
}
|
||||
is TypeList -> filter.state.forEach { type ->
|
||||
if (type.state) {
|
||||
mutableType += type.id
|
||||
}
|
||||
}
|
||||
is PubList -> filter.state.forEach { publisher ->
|
||||
if (publisher.state) {
|
||||
sectionPub += publisher.id
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
val pageParameter = if (page > 1) "page/$page/" else ""
|
||||
return POST(
|
||||
"$baseUrl/ComicList/p.cat=${sectionPub.joinToString(",")}/g=${mutableGenre.joinToString(",")}/t=${mutableType.joinToString(",")}/adult=${mutableAge.joinToString(",")}/$pageParameter",
|
||||
body = FormBody.Builder()
|
||||
.add("dlenewssortby", orderBy)
|
||||
.add("dledirection", ascEnd)
|
||||
.add("set_new_sort", "dle_sort_xfilter")
|
||||
.add("set_direction_sort", "dle_direction_xfilter")
|
||||
.build(),
|
||||
headers = headers,
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
override fun searchMangaNextPageSelector(): Nothing? = null
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.page__grid").first()!!
|
||||
|
||||
val ratingValue = infoElement.select(".page__activity-votes").textNodes().first().text().trim().toFloat() * 2
|
||||
val ratingVotes = infoElement.select(".page__activity-votes span > span").first()!!.text().trim()
|
||||
val ratingStar = when {
|
||||
ratingValue > 9.5 -> "★★★★★"
|
||||
ratingValue > 8.5 -> "★★★★✬"
|
||||
ratingValue > 7.5 -> "★★★★☆"
|
||||
ratingValue > 6.5 -> "★★★✬☆"
|
||||
ratingValue > 5.5 -> "★★★☆☆"
|
||||
ratingValue > 4.5 -> "★★✬☆☆"
|
||||
ratingValue > 3.5 -> "★★☆☆☆"
|
||||
ratingValue > 2.5 -> "★✬☆☆☆"
|
||||
ratingValue > 1.5 -> "★☆☆☆☆"
|
||||
ratingValue > 0.5 -> "✬☆☆☆☆"
|
||||
else -> "☆☆☆☆☆"
|
||||
}
|
||||
val rawCategory = document.select(".speedbar a").last()!!.text().trim()
|
||||
val category = when (rawCategory.lowercase()) {
|
||||
"manga" -> "Манга"
|
||||
"manhwa" -> "Манхва"
|
||||
"manhua" -> "Маньхуа"
|
||||
else -> "Комикс"
|
||||
}
|
||||
val rawAgeStop = if (document.toString().contains("ВНИМАНИЕ! 18+")) "18+" else ""
|
||||
val manga = SManga.create()
|
||||
manga.title = infoElement.select(".page__header h1").text().trim()
|
||||
manga.author = infoElement.select(".page__list li:contains(Издатель)").text()
|
||||
manga.genre = category + ", " + rawAgeStop + ", " + infoElement.select(".page__tags a").joinToString { it.text() }
|
||||
manga.status = parseStatus(infoElement.select(".page__list li:contains(Статус)").text())
|
||||
|
||||
manga.description = infoElement.select(".page__title-original").text().trim() + "\n" +
|
||||
if (document.select(".page__list li:contains(Тип выпуска)").text().contains("!!! События в комиксах - ХРОНОЛОГИЯ !!!")) { "Cобытие в комиксах - ХРОНОЛОГИЯ\n" } else { "" } +
|
||||
ratingStar + " " + ratingValue + " (голосов: " + ratingVotes + ")\n" +
|
||||
infoElement.select(".page__text ").first()?.html()?.let { Jsoup.parse(it) }
|
||||
?.select("body:not(:has(p)),p,br")
|
||||
?.prepend("\\n")?.text()?.replace("\\n", "\n")?.replace("\n ", "\n")
|
||||
.orEmpty()
|
||||
|
||||
val src = infoElement.select(".img-wide img").let {
|
||||
it.attr("data-src").ifEmpty { it.attr("src") }
|
||||
}
|
||||
|
||||
if (src.contains("://")) {
|
||||
manga.thumbnail_url = src
|
||||
} else {
|
||||
manga.thumbnail_url = baseUrl + src
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("Продолжается") ||
|
||||
element.contains(" из ") ||
|
||||
element.contains("Онгоинг") -> SManga.ONGOING
|
||||
element.contains("Заверш") ||
|
||||
element.contains("Лимитка") ||
|
||||
element.contains("Ван шот") ||
|
||||
element.contains("Графический роман") -> SManga.COMPLETED
|
||||
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override fun chapterListSelector() = throw NotImplementedError("Unused")
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val dataStr = document
|
||||
.toString()
|
||||
.substringAfter("window.__DATA__ = ")
|
||||
.substringBefore("</script>")
|
||||
.substringBeforeLast(";")
|
||||
|
||||
val data = json.decodeFromString<JsonObject>(dataStr)
|
||||
val chaptersList = data["chapters"]?.jsonArray
|
||||
val isEvent = document.select(".page__list li:contains(Тип выпуска)").text()
|
||||
.contains("!!! События в комиксах - ХРОНОЛОГИЯ !!!")
|
||||
|
||||
val chapters: List<SChapter>? = chaptersList?.map {
|
||||
val chapter = SChapter.create()
|
||||
// Usually "title" is main chapter name info, "title_en" is additional chapter name info.
|
||||
// I decided to keep them both because who knows where they decided to put useful info today.
|
||||
// Except when they are the same.
|
||||
chapter.name = if (it.jsonObject["title"]!!.jsonPrimitive.content == it.jsonObject["title_en"]!!.jsonPrimitive.content) {
|
||||
it.jsonObject["title"]!!.jsonPrimitive.content
|
||||
} else {
|
||||
(it.jsonObject["title"]!!.jsonPrimitive.content + " " + it.jsonObject["title_en"]!!.jsonPrimitive.content).trim()
|
||||
}
|
||||
chapter.date_upload = simpleDateFormat.parse(it.jsonObject["date"]!!.jsonPrimitive.content)?.time ?: 0L
|
||||
chapter.chapter_number = it.jsonObject["posi"]!!.jsonPrimitive.float
|
||||
// when it is Event add reading order numbers as prefix
|
||||
if (isEvent) {
|
||||
chapter.name = chapter.chapter_number.toInt().toString() + " " + chapter.name
|
||||
}
|
||||
chapter.setUrlWithoutDomain("/readcomix/" + data["news_id"] + "/" + it.jsonObject["id"]!!.jsonPrimitive.content + ".html")
|
||||
chapter
|
||||
}
|
||||
return chapters ?: emptyList()
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter =
|
||||
throw NotImplementedError("Unused")
|
||||
|
||||
// Pages
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body.string()
|
||||
|
||||
// Comics 18+
|
||||
if (html.contains("adult__header")) {
|
||||
throw Exception("Комикс 18+ (что-то сломалось)")
|
||||
}
|
||||
|
||||
val baseImgUrl = "https://img.com-x.life/comix/"
|
||||
|
||||
val beginTag = "\"images\":["
|
||||
val beginIndex = html.indexOf(beginTag)
|
||||
val endIndex = html.indexOf("]", beginIndex)
|
||||
|
||||
val urls: List<String> = html.substring(beginIndex + beginTag.length, endIndex)
|
||||
.split(',').map {
|
||||
val img = it.replace("\\", "").replace("\"", "")
|
||||
baseImgUrl + img
|
||||
}
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
for (i in urls.indices) {
|
||||
pages.add(Page(i, "", urls[i]))
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
if (response.code == 404 && response.asJsoup().toString().contains("Выпуск был удален по требованию правообладателя")) {
|
||||
throw Exception("Лицензировано. Возможно может помочь авторизация через WebView")
|
||||
} else {
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
.map { response ->
|
||||
pageListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
// Filters
|
||||
private class OrderBy : Filter.Sort(
|
||||
"Сортировать по",
|
||||
arrayOf("Дате", "Популярности", "Посещаемости", "Комментариям", "Алфавиту"),
|
||||
Selection(1, false),
|
||||
)
|
||||
|
||||
private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
|
||||
|
||||
private class PubList(publishers: List<CheckFilter>) : Filter.Group<CheckFilter>("Разделы", publishers)
|
||||
private class GenreList(genres: List<CheckFilter>) : Filter.Group<CheckFilter>("Жанры", genres)
|
||||
private class TypeList(types: List<CheckFilter>) : Filter.Group<CheckFilter>("Тип выпуска", types)
|
||||
private class AgeList(ages: List<CheckFilter>) : Filter.Group<CheckFilter>("Возрастное ограничение", ages)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
OrderBy(),
|
||||
PubList(getPubList()),
|
||||
GenreList(getGenreList()),
|
||||
TypeList(getTypeList()),
|
||||
AgeList(getAgeList()),
|
||||
)
|
||||
|
||||
private fun getAgeList() = listOf(
|
||||
CheckFilter("Для всех", "1"),
|
||||
CheckFilter("18+", "2"),
|
||||
)
|
||||
|
||||
private fun getTypeList() = listOf(
|
||||
CheckFilter("События в комиксах", "1"),
|
||||
CheckFilter("Аннуалы", "3"),
|
||||
CheckFilter("Артбук", "4"),
|
||||
CheckFilter("Ван-шот", "7"),
|
||||
CheckFilter("Гайд", "15"),
|
||||
CheckFilter("Графический Роман", "17"),
|
||||
CheckFilter("Комикс Стрип", "19"),
|
||||
CheckFilter("Лимитка", "21"),
|
||||
CheckFilter("Макси-серия", "25"),
|
||||
CheckFilter("Мини-серия", "27"),
|
||||
CheckFilter("Онгоинг", "28"),
|
||||
CheckFilter("Рассказ", "29"),
|
||||
CheckFilter("Роман", "30"),
|
||||
CheckFilter("Сборник", "31"),
|
||||
CheckFilter("Серия", "32"),
|
||||
CheckFilter("Спешл", "35"),
|
||||
CheckFilter("Энциклопедия", "36"),
|
||||
)
|
||||
|
||||
private fun getPubList() = listOf(
|
||||
CheckFilter("Манга", "3"),
|
||||
CheckFilter("Маньхуа", "45"),
|
||||
CheckFilter("Манхва", "44"),
|
||||
CheckFilter("Разные комиксы", "18"),
|
||||
CheckFilter("Aftershock", "50"),
|
||||
CheckFilter("Avatar Press", "11"),
|
||||
CheckFilter("Boom! Studios", "12"),
|
||||
CheckFilter("Dark Horse", "7"),
|
||||
CheckFilter("DC Comics", "14"),
|
||||
CheckFilter("Dynamite Entertainment", "10"),
|
||||
CheckFilter("Icon Comics", "16"),
|
||||
CheckFilter("IDW Publishing", "6"),
|
||||
CheckFilter("Image", "4"),
|
||||
CheckFilter("Marvel", "2"),
|
||||
CheckFilter("Oni Press", "13"),
|
||||
CheckFilter("Top Cow", "9"),
|
||||
CheckFilter("Valiant", "15"),
|
||||
CheckFilter("Vertigo", "8"),
|
||||
CheckFilter("Wildstorm", "5"),
|
||||
CheckFilter("Zenescope", "51"),
|
||||
)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
CheckFilter("Автобиографическая новелла", "9"),
|
||||
CheckFilter("Альтернативная история", "10"),
|
||||
CheckFilter("Антиутопия", "11"),
|
||||
CheckFilter("Апокалипсис", "12"),
|
||||
CheckFilter("Артбук", "14"),
|
||||
CheckFilter("Афрофутуризм", "15"),
|
||||
CheckFilter("Биография", "16"),
|
||||
CheckFilter("Боевик", "17"),
|
||||
CheckFilter("Боевые искусства", "18"),
|
||||
CheckFilter("Вампиры", "19"),
|
||||
CheckFilter("Вестерн", "20"),
|
||||
CheckFilter("Военный", "21"),
|
||||
CheckFilter("Гарем", "22"),
|
||||
CheckFilter("Гендерная интрига", "23"),
|
||||
CheckFilter("Героическое фэнтези", "24"),
|
||||
CheckFilter("Детектив", "25"),
|
||||
CheckFilter("Детский", "26"),
|
||||
CheckFilter("Дзёсэй", "27"),
|
||||
CheckFilter("Документальный", "28"),
|
||||
CheckFilter("Драма", "29"),
|
||||
CheckFilter("Единоборства", "30"),
|
||||
CheckFilter("Жизнь", "31"),
|
||||
CheckFilter("Зомби", "32"),
|
||||
CheckFilter("Игра", "33"),
|
||||
CheckFilter("Игры", "124"),
|
||||
CheckFilter("Исекай", "35"),
|
||||
CheckFilter("Исэкай", "38"),
|
||||
CheckFilter("Исторический", "36"),
|
||||
CheckFilter("История", "37"),
|
||||
CheckFilter("Квест", "39"),
|
||||
CheckFilter("Киберпанк", "40"),
|
||||
CheckFilter("Кодомо", "41"),
|
||||
CheckFilter("Комедия", "42"),
|
||||
CheckFilter("Комелия", "43"),
|
||||
CheckFilter("Космоопера", "44"),
|
||||
CheckFilter("Космос", "45"),
|
||||
CheckFilter("Криминал", "46"),
|
||||
CheckFilter("Криптоистория", "47"),
|
||||
CheckFilter("ЛГБТ", "48"),
|
||||
CheckFilter("Магия", "49"),
|
||||
CheckFilter("Мелодрама", "50"),
|
||||
CheckFilter("Меха", "51"),
|
||||
CheckFilter("Мистика", "52"),
|
||||
CheckFilter("Музыка", "54"),
|
||||
CheckFilter("Научная фантастика", "55"),
|
||||
CheckFilter("Неотвратимость", "56"),
|
||||
CheckFilter("Нуар", "57"),
|
||||
CheckFilter("Омегаверс", "58"),
|
||||
CheckFilter("Паника", "59"),
|
||||
CheckFilter("Пародия", "60"),
|
||||
CheckFilter("Пираты", "61"),
|
||||
CheckFilter("Повседневность", "62"),
|
||||
CheckFilter("Политика", "63"),
|
||||
CheckFilter("Постапокалиптика", "64"),
|
||||
CheckFilter("Предатель среди нас", "65"),
|
||||
CheckFilter("Приключения", "67"),
|
||||
CheckFilter("Приступления", "69"),
|
||||
CheckFilter("Психические отклонения", "70"),
|
||||
CheckFilter("Психоделика", "71"),
|
||||
CheckFilter("Психология", "73"),
|
||||
CheckFilter("Путешествия во времени", "74"),
|
||||
CheckFilter("Религия", "75"),
|
||||
CheckFilter("Романтика", "76"),
|
||||
CheckFilter("Самурайский боевик", "77"),
|
||||
CheckFilter("Сверхъестественное", "78"),
|
||||
CheckFilter("Сейнен", "79"),
|
||||
CheckFilter("Симбиоты", "80"),
|
||||
CheckFilter("Сказка", "81"),
|
||||
CheckFilter("Слэшер", "82"),
|
||||
CheckFilter("Смерть", "83"),
|
||||
CheckFilter("Спорт", "84"),
|
||||
CheckFilter("Стимпанк", "85"),
|
||||
CheckFilter("Супергероика", "87"),
|
||||
CheckFilter("Сэйнэн", "90"),
|
||||
CheckFilter("Сянься", "91"),
|
||||
CheckFilter("Сёдзё", "93"),
|
||||
CheckFilter("Сёдзё-ай", "94"),
|
||||
CheckFilter("Сёнэн", "96"),
|
||||
CheckFilter("Сёнэн-ай", "97"),
|
||||
CheckFilter("Трагедия", "98"),
|
||||
CheckFilter("Тревога", "99"),
|
||||
CheckFilter("Триллер", "100"),
|
||||
CheckFilter("Тюремная драма", "101"),
|
||||
CheckFilter("Ужасы", "102"),
|
||||
CheckFilter("Фантасмагория", "103"),
|
||||
CheckFilter("Фантасмогория", "104"),
|
||||
CheckFilter("Фантастика", "105"),
|
||||
CheckFilter("Фэнтези", "106"),
|
||||
CheckFilter("Хоррор", "109"),
|
||||
CheckFilter("Черный юмор", "110"),
|
||||
CheckFilter("Школа", "111"),
|
||||
CheckFilter("Школьная жизнь", "123"),
|
||||
CheckFilter("Шпионский", "113"),
|
||||
CheckFilter("Экшен", "122"),
|
||||
CheckFilter("Экшн", "115"),
|
||||
CheckFilter("Эротика", "116"),
|
||||
CheckFilter("Этти", "117"),
|
||||
CheckFilter("Юмор", "118"),
|
||||
CheckFilter("Юри", "119"),
|
||||
CheckFilter("Яой", "120"),
|
||||
CheckFilter("Ёнкома", "121"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val simpleDateFormat by lazy { SimpleDateFormat("dd.MM.yyyy", Locale.US) }
|
||||
}
|
||||
}
|
||||
28
src/ru/desu/AndroidManifest.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ru.desu.DesuActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- DesuActivity sites can be added here. -->
|
||||
<data
|
||||
android:host="desu.me"
|
||||
android:pathPattern="/manga/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="desu.win"
|
||||
android:pathPattern="/manga/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
12
src/ru/desu/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Desu'
|
||||
pkgNameSuffix = 'ru.desu'
|
||||
extClass = '.Desu'
|
||||
extVersionCode = 23
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/desu/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/ru/desu/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/ru/desu/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/ru/desu/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/ru/desu/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
src/ru/desu/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
405
src/ru/desu/src/eu/kanade/tachiyomi/extension/ru/desu/Desu.kt
Normal file
@@ -0,0 +1,405 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.desu
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.widget.Toast
|
||||
import androidx.preference.ListPreference
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.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 kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.floatOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Desu : ConfigurableSource, HttpSource() {
|
||||
override val name = "Desu"
|
||||
|
||||
override val id: Long = 6684416167758830305
|
||||
|
||||
override val baseUrl = "https://desu.win"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Tachiyomi")
|
||||
add("Referer", baseUrl)
|
||||
}
|
||||
|
||||
override val client: OkHttpClient =
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 3)
|
||||
.build()
|
||||
|
||||
private fun mangaPageFromJSON(jsonStr: String, next: Boolean): MangasPage {
|
||||
val mangaList = json.parseToJsonElement(jsonStr).jsonArray
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
mangaFromJSON(it.jsonObject, false)
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangaList, next)
|
||||
}
|
||||
|
||||
private fun SManga.mangaFromJSON(obj: JsonObject, chapter: Boolean) {
|
||||
val id = obj["id"]!!.jsonPrimitive.int
|
||||
|
||||
url = "/$id"
|
||||
title = if (isEng.equals("rus")) {
|
||||
obj["russian"]!!.jsonPrimitive.content
|
||||
.split(" / ")
|
||||
.first()
|
||||
} else {
|
||||
obj["name"]!!.jsonPrimitive.content
|
||||
.split(" / ")
|
||||
.first()
|
||||
}
|
||||
thumbnail_url = obj["image"]!!.jsonObject["original"]!!.jsonPrimitive.content
|
||||
|
||||
val ratingValue = obj["score"]!!.jsonPrimitive.floatOrNull ?: 0F
|
||||
val ratingStar = when {
|
||||
ratingValue > 9.5 -> "★★★★★"
|
||||
ratingValue > 8.5 -> "★★★★✬"
|
||||
ratingValue > 7.5 -> "★★★★☆"
|
||||
ratingValue > 6.5 -> "★★★✬☆"
|
||||
ratingValue > 5.5 -> "★★★☆☆"
|
||||
ratingValue > 4.5 -> "★★✬☆☆"
|
||||
ratingValue > 3.5 -> "★★☆☆☆"
|
||||
ratingValue > 2.5 -> "★✬☆☆☆"
|
||||
ratingValue > 1.5 -> "★☆☆☆☆"
|
||||
ratingValue > 0.5 -> "✬☆☆☆☆"
|
||||
else -> "☆☆☆☆☆"
|
||||
}
|
||||
|
||||
val rawAgeStop = when (obj["adult"]!!.jsonPrimitive.int) {
|
||||
1 -> "18+"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val category = when (obj["kind"]!!.jsonPrimitive.content) {
|
||||
"manga" -> "Манга"
|
||||
"manhwa" -> "Манхва"
|
||||
"manhua" -> "Маньхуа"
|
||||
"comics" -> "Комикс"
|
||||
"one_shot" -> "Ваншот"
|
||||
else -> "Манга"
|
||||
}
|
||||
|
||||
var altName = ""
|
||||
|
||||
if (obj["synonyms"]?.jsonPrimitive?.content.orEmpty().isNotEmpty() && obj["synonyms"]!!.jsonPrimitive.contentOrNull != null) {
|
||||
altName = "Альтернативные названия:\n" +
|
||||
obj["synonyms"]!!.jsonPrimitive.content
|
||||
.replace("/", " / ") +
|
||||
"\n\n"
|
||||
}
|
||||
|
||||
description = if (isEng.equals("rus")) {
|
||||
obj["name"]!!.jsonPrimitive.content
|
||||
.split(" / ")
|
||||
.first()
|
||||
} else {
|
||||
obj["russian"]!!.jsonPrimitive.content
|
||||
.split(" / ")
|
||||
.first()
|
||||
} + "\n" +
|
||||
ratingStar + " " + ratingValue +
|
||||
" (голосов: " +
|
||||
obj["score_users"]!!.jsonPrimitive.int +
|
||||
")\n" + altName +
|
||||
obj["description"]!!.jsonPrimitive.content
|
||||
|
||||
genre = if (chapter) {
|
||||
"$category, $rawAgeStop, " +
|
||||
obj["genres"]!!.jsonArray
|
||||
.map { it.jsonObject["russian"]!!.jsonPrimitive.content }
|
||||
.joinToString()
|
||||
} else {
|
||||
category + ", " + rawAgeStop + ", " + obj["genres"]!!.jsonPrimitive.content
|
||||
}
|
||||
|
||||
status = when (obj["trans_status"]!!.jsonPrimitive.content) {
|
||||
"continued" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> when (obj["status"]!!.jsonPrimitive.content) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"released" -> SManga.COMPLETED
|
||||
// "copyright" -> SManga.LICENSED Hides available chapters!
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$baseUrl$API_URL/?limit=50&order=popular&page=$page")
|
||||
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl$API_URL/?limit=50&order=updated&page=$page")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
var url = "$baseUrl$API_URL/?limit=20&page=$page"
|
||||
val types = mutableListOf<Type>()
|
||||
val genres = mutableListOf<Genre>()
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy -> url += "&order=" + arrayOf("popular", "updated", "name")[filter.state]
|
||||
is TypeList -> filter.state.forEach { type -> if (type.state) types.add(type) }
|
||||
is GenreList -> filter.state.forEach { genre -> if (genre.state) genres.add(genre) }
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (types.isNotEmpty()) {
|
||||
url += "&kinds=" + types.joinToString(",") { it.id }
|
||||
}
|
||||
if (genres.isNotEmpty()) {
|
||||
url += "&genres=" + genres.joinToString(",") { it.id }
|
||||
}
|
||||
if (query.isNotEmpty()) {
|
||||
url += "&search=$query"
|
||||
}
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val res = json.parseToJsonElement(response.body.string()).jsonObject
|
||||
val obj = res["response"]!!.jsonArray
|
||||
val nav = res["pageNavParams"]!!.jsonObject
|
||||
val count = nav["count"]!!.jsonPrimitive.int
|
||||
val limit = nav["limit"]!!.jsonPrimitive.int
|
||||
val page = nav["page"]!!.jsonPrimitive.int
|
||||
|
||||
return mangaPageFromJSON(obj.toString(), count > page * limit)
|
||||
}
|
||||
|
||||
private fun titleDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + API_URL + manga.url + "/", headers)
|
||||
}
|
||||
|
||||
// Workaround to allow "Open in browser" use the real URL.
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(titleDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + "/manga" + manga.url, headers)
|
||||
}
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val obj = json.parseToJsonElement(response.body.string())
|
||||
.jsonObject["response"]!!
|
||||
.jsonObject
|
||||
|
||||
mangaFromJSON(obj, true)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val obj = json.parseToJsonElement(response.body.string())
|
||||
.jsonObject["response"]!!
|
||||
.jsonObject
|
||||
|
||||
val cid = obj["id"]!!.jsonPrimitive.int
|
||||
val objChapter = obj["chapters"]!!
|
||||
return objChapter.jsonObject["list"]!!.jsonArray.filterNot { it.jsonObject["vol"]!!.jsonPrimitive.floatOrNull!! == objChapter.jsonObject["last"]!!.jsonObject["vol"]!!.jsonPrimitive.float && it.jsonObject["ch"]!!.jsonPrimitive.floatOrNull!! > objChapter.jsonObject["last"]!!.jsonObject["ch"]!!.jsonPrimitive.float }.map {
|
||||
val chapterObj = it.jsonObject
|
||||
val ch = chapterObj["ch"]!!.jsonPrimitive.content
|
||||
val vol = chapterObj["vol"]!!.jsonPrimitive.content
|
||||
val fullNumStr = "$vol. Глава $ch"
|
||||
val title = chapterObj["title"]!!.jsonPrimitive.contentOrNull ?: ""
|
||||
|
||||
SChapter.create().apply {
|
||||
name = "$fullNumStr $title"
|
||||
// #apiChapter - JSON API url to automatically delete when chapter is opened in browser
|
||||
url = "/manga/$cid/vol$vol/ch$ch/rus" + "#apiChapter/$cid/chapter/${chapterObj["id"]!!.jsonPrimitive.int}"
|
||||
chapter_number = ch.toFloatOrNull() ?: -1f
|
||||
date_upload = chapterObj["date"]!!.jsonPrimitive.long * 1000L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = titleDetailsRequest(manga)
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(baseUrl + API_URL + chapter.url.substringAfterLast("#apiChapter"), headers)
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return baseUrl + chapter.url.substringBeforeLast("#apiChapter")
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val obj = json.parseToJsonElement(response.body.string())
|
||||
.jsonObject["response"]!!
|
||||
.jsonObject
|
||||
|
||||
return obj["pages"]!!.jsonObject["list"]!!.jsonArray
|
||||
.mapIndexed { i, jsonEl ->
|
||||
Page(i, "", jsonEl.jsonObject["img"]!!.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException("This method should not be called!")
|
||||
|
||||
private fun searchMangaByIdRequest(id: String): Request {
|
||||
return GET("$baseUrl$API_URL/$id", headers)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
|
||||
val realQuery = query.removePrefix(PREFIX_SLUG_SEARCH)
|
||||
client.newCall(searchMangaByIdRequest(realQuery))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val details = mangaDetailsParse(response)
|
||||
details.url = "/$realQuery"
|
||||
MangasPage(listOf(details), false)
|
||||
}
|
||||
} else {
|
||||
client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class OrderBy : Filter.Select<String>(
|
||||
"Сортировка",
|
||||
arrayOf("Популярность", "Дата", "Имя"),
|
||||
)
|
||||
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Жанр", genres)
|
||||
|
||||
private class TypeList(types: List<Type>) : Filter.Group<Type>("Тип", types)
|
||||
|
||||
private class Type(name: String, val id: String) : Filter.CheckBox(name)
|
||||
|
||||
private class Genre(name: String, val id: String) : Filter.CheckBox(name)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
OrderBy(),
|
||||
TypeList(getTypeList()),
|
||||
GenreList(getGenreList()),
|
||||
)
|
||||
|
||||
private fun getTypeList() = listOf(
|
||||
Type("Манга", "manga"),
|
||||
Type("Манхва", "manhwa"),
|
||||
Type("Маньхуа", "manhua"),
|
||||
Type("Ваншот", "one_shot"),
|
||||
Type("Комикс", "comics"),
|
||||
)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Безумие", "Dementia"),
|
||||
Genre("Боевые искусства", "Martial Arts"),
|
||||
Genre("Вампиры", "Vampire"),
|
||||
Genre("Военное", "Military"),
|
||||
Genre("Гарем", "Harem"),
|
||||
Genre("Демоны", "Demons"),
|
||||
Genre("Детектив", "Mystery"),
|
||||
Genre("Детское", "Kids"),
|
||||
Genre("Дзёсей", "Josei"),
|
||||
Genre("Додзинси", "Doujinshi"),
|
||||
Genre("Драма", "Drama"),
|
||||
Genre("Игры", "Game"),
|
||||
Genre("Исторический", "Historical"),
|
||||
Genre("Комедия", "Comedy"),
|
||||
Genre("Космос", "Space"),
|
||||
Genre("Магия", "Magic"),
|
||||
Genre("Машины", "Cars"),
|
||||
Genre("Меха", "Mecha"),
|
||||
Genre("Музыка", "Music"),
|
||||
Genre("Пародия", "Parody"),
|
||||
Genre("Повседневность", "Slice of Life"),
|
||||
Genre("Полиция", "Police"),
|
||||
Genre("Приключения", "Adventure"),
|
||||
Genre("Психологическое", "Psychological"),
|
||||
Genre("Романтика", "Romance"),
|
||||
Genre("Самураи", "Samurai"),
|
||||
Genre("Сверхъестественное", "Supernatural"),
|
||||
Genre("Сёдзе", "Shoujo"),
|
||||
Genre("Сёдзе Ай", "Shoujo Ai"),
|
||||
Genre("Сейнен", "Seinen"),
|
||||
Genre("Сёнен", "Shounen"),
|
||||
Genre("Сёнен Ай", "Shounen Ai"),
|
||||
Genre("Смена пола", "Gender Bender"),
|
||||
Genre("Спорт", "Sports"),
|
||||
Genre("Супер сила", "Super Power"),
|
||||
Genre("Триллер", "Thriller"),
|
||||
Genre("Ужасы", "Horror"),
|
||||
Genre("Фантастика", "Sci-Fi"),
|
||||
Genre("Фэнтези", "Fantasy"),
|
||||
Genre("Хентай", "Hentai"),
|
||||
Genre("Школа", "School"),
|
||||
Genre("Экшен", "Action"),
|
||||
Genre("Этти", "Ecchi"),
|
||||
Genre("Юри", "Yuri"),
|
||||
Genre("Яой", "Yaoi"),
|
||||
)
|
||||
|
||||
private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng")
|
||||
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||
val titleLanguagePref = ListPreference(screen.context).apply {
|
||||
key = LANGUAGE_PREF
|
||||
title = "Выбор языка на обложке"
|
||||
entries = arrayOf("Английский", "Русский")
|
||||
entryValues = arrayOf("eng", "rus")
|
||||
summary = "%s"
|
||||
setDefaultValue("eng")
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val warning = "Если язык обложки не изменился очистите базу данных в приложении (Настройки -> Дополнительно -> Очистить базу данных)"
|
||||
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
screen.addPreference(titleLanguagePref)
|
||||
}
|
||||
companion object {
|
||||
const val PREFIX_SLUG_SEARCH = "slug:"
|
||||
|
||||
private const val LANGUAGE_PREF = "DesuTitleLanguage"
|
||||
|
||||
private const val API_URL = "/manga/api"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.desu
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
/**
|
||||
* Springboard that accepts https://desu.me/manga/xxx intents and redirects them to
|
||||
* the main tachiyomi process. The idea is to not install the intent filter unless
|
||||
* you have this extension installed, but still let the main tachiyomi app control
|
||||
* things.
|
||||
*/
|
||||
class DesuActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
val titleid = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${Desu.PREFIX_SLUG_SEARCH}$titleid")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("DesuActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("DesuActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
2
src/ru/mangabook/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/ru/mangabook/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'MangaBook'
|
||||
pkgNameSuffix = 'ru.mangabook'
|
||||
extClass = '.MangaBook'
|
||||
extVersionCode = 5
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/mangabook/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/ru/mangabook/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/ru/mangabook/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/ru/mangabook/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src/ru/mangabook/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/ru/mangabook/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,297 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.mangabook
|
||||
|
||||
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.Companion.toHttpUrlOrNull
|
||||
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 MangaBook : ParsedHttpSource() {
|
||||
// Info
|
||||
override val name = "MangaBook"
|
||||
override val baseUrl = "https://mangabook.org"
|
||||
override val lang = "ru"
|
||||
override val supportsLatest = true
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
private val userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", userAgent)
|
||||
.add("Accept", "image/webp,*/*;q=0.8")
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/filterList?page=$page&ftype[]=0&status[]=0&sortBy=views", headers)
|
||||
override fun popularMangaNextPageSelector() = "a.page-link[rel=next]"
|
||||
override fun popularMangaSelector() = "article.short:not(.shnews) .short-in"
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select(".sh-desc a").first()!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.select("div.sh-title").text().split(" / ").min()
|
||||
}
|
||||
thumbnail_url = element.select(".short-poster.img-box > img").attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
// Search
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
"$baseUrl/dosearch?query=$query&page=$page"
|
||||
} else {
|
||||
val url = "$baseUrl/filterList?page=$page&ftype[]=0&status[]=0".toHttpUrlOrNull()!!.newBuilder()
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
val ord = arrayOf("views", "rate", "name", "created_at")[filter.state]
|
||||
url.addQueryParameter("sortBy", ord)
|
||||
}
|
||||
is CategoryList -> {
|
||||
if (filter.state > 0) {
|
||||
val catQ = getCategoryList()[filter.state].query
|
||||
url.addQueryParameter("cat", catQ)
|
||||
}
|
||||
}
|
||||
is StatusList -> filter.state.forEach { status ->
|
||||
if (status.state) {
|
||||
url.addQueryParameter("status[]", status.id)
|
||||
}
|
||||
}
|
||||
is FormatList -> filter.state.forEach { forma ->
|
||||
if (forma.state) {
|
||||
url.addQueryParameter("ftype[]", forma.id)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select(".flist.row a").first()!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.select("h4 strong").text().split(" / ").min()
|
||||
}
|
||||
thumbnail_url = element.select(".sposter img.img-responsive").attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (!response.request.url.toString().contains("dosearch")) {
|
||||
return popularMangaParse(response)
|
||||
}
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(".manga-list li:not(.vis )").map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("article.full .fmid").first()!!
|
||||
val manga = SManga.create()
|
||||
val titlestr = document.select(".fheader h1").text().split(" / ").sorted()
|
||||
manga.title = titlestr.first()
|
||||
manga.thumbnail_url = infoElement.select("img.img-responsive").first()!!.attr("src")
|
||||
manga.author = infoElement.select(".vis:contains(Автор) > a").text()
|
||||
manga.artist = infoElement.select(".vis:contains(Художник) > a").text()
|
||||
manga.status = if (document.select(".fheader h2").text() == "Чтение заблокировано") {
|
||||
SManga.LICENSED
|
||||
} else {
|
||||
when (infoElement.select(".vis:contains(Статус) span.label").text()) {
|
||||
"Сейчас издаётся" -> SManga.ONGOING
|
||||
"Изданное" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
val rawCategory = infoElement.select(".vis:contains(Жанр (вид)) span.label").text()
|
||||
val category = when {
|
||||
rawCategory == "Веб-Манхва" -> "Манхва"
|
||||
rawCategory.isNotBlank() -> rawCategory
|
||||
else -> "Манхва"
|
||||
}
|
||||
manga.genre = infoElement.select(".vis:contains(Категории) > a").map { it.text() }.plusElement(category).joinToString { it.trim() }
|
||||
val ratingValue = infoElement.select(".rating").text().substringAfter("Рейтинг ").substringBefore("/").toFloat() * 2
|
||||
val ratingVotes = infoElement.select(".rating").text().substringAfter("голосов: ").substringBefore(" ")
|
||||
val ratingStar = when {
|
||||
ratingValue > 9.5 -> "★★★★★"
|
||||
ratingValue > 8.5 -> "★★★★✬"
|
||||
ratingValue > 7.5 -> "★★★★☆"
|
||||
ratingValue > 6.5 -> "★★★✬☆"
|
||||
ratingValue > 5.5 -> "★★★☆☆"
|
||||
ratingValue > 4.5 -> "★★✬☆☆"
|
||||
ratingValue > 3.5 -> "★★☆☆☆"
|
||||
ratingValue > 2.5 -> "★✬☆☆☆"
|
||||
ratingValue > 1.5 -> "★☆☆☆☆"
|
||||
ratingValue > 0.5 -> "✬☆☆☆☆"
|
||||
else -> "☆☆☆☆☆"
|
||||
}
|
||||
val altSelector = document.select(".vis:contains(Другие названия) span")
|
||||
var altName = ""
|
||||
if (altSelector.isNotEmpty()) {
|
||||
altName = "Альтернативные названия:\n" + altSelector.last()!!.text() + "\n\n"
|
||||
}
|
||||
manga.description = titlestr.last() + "\n" + ratingStar + " " + ratingValue + " (голосов: " + ratingVotes + ")\n" + altName + infoElement.select(".fdesc.slice-this").text()
|
||||
return manga
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override fun chapterListSelector(): String = ".chapters li:not(.volume )"
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
val link = element.select("h5 a")
|
||||
name = element.attr("class").substringAfter("volume-") + ". " + link.text()
|
||||
chapter_number = name.substringAfter("Глава №").substringBefore(":").toFloat()
|
||||
setUrlWithoutDomain(link.attr("href") + "/1")
|
||||
date_upload = parseDate(element.select(".date-chapter-title-rtl").text().trim())
|
||||
}
|
||||
private fun parseDate(date: String): Long {
|
||||
return SimpleDateFormat("dd.MM.yyyy", Locale.US).parse(date)?.time ?: 0
|
||||
}
|
||||
|
||||
// Pages
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(".reader-images img.img-responsive:not(.scan-page)").mapIndexed { i, img ->
|
||||
Page(i, "", img.attr("data-src").trim())
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw Exception("imageUrlParse Not Used")
|
||||
|
||||
// Filters
|
||||
private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
|
||||
|
||||
private class FormatList(formas: List<CheckFilter>) : Filter.Group<CheckFilter>("Тип", formas)
|
||||
private class StatusList(statuses: List<CheckFilter>) : Filter.Group<CheckFilter>("Статус", statuses)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
OrderBy(),
|
||||
CategoryList(categoriesName),
|
||||
StatusList(getStatusList()),
|
||||
FormatList(getFormatList()),
|
||||
)
|
||||
|
||||
private class OrderBy : Filter.Select<String>(
|
||||
"Сортировка",
|
||||
arrayOf("По популярности", "По рейтингу", "По алфавиту", "По дате выхода"),
|
||||
)
|
||||
private fun getFormatList() = listOf(
|
||||
CheckFilter("Манга", "1"),
|
||||
CheckFilter("Манхва", "2"),
|
||||
CheckFilter("Веб Манхва", "4"),
|
||||
CheckFilter("Маньхуа", "3"),
|
||||
)
|
||||
|
||||
private fun getStatusList() = listOf(
|
||||
CheckFilter("Сейчас издаётся", "1"),
|
||||
CheckFilter("Анонсировано", "3"),
|
||||
CheckFilter("Изданное", "2"),
|
||||
)
|
||||
|
||||
private class CategoryList(categories: Array<String>) : Filter.Select<String>("Категории", categories)
|
||||
private data class CatUnit(val name: String, val query: String)
|
||||
private val categoriesName = getCategoryList().map {
|
||||
it.name
|
||||
}.toTypedArray()
|
||||
|
||||
private fun getCategoryList() = listOf(
|
||||
CatUnit("Без категории", "not"),
|
||||
CatUnit("16+", "16+"),
|
||||
CatUnit("Арт", "art"),
|
||||
CatUnit("Бара", "bara"),
|
||||
CatUnit("Боевик", "action"),
|
||||
CatUnit("Боевые искусства", "combatskill"),
|
||||
CatUnit("В цвете", "vcvete"),
|
||||
CatUnit("Вампиры", "vampaires"),
|
||||
CatUnit("Веб", "web"),
|
||||
CatUnit("Вестерн", "western"),
|
||||
CatUnit("Гарем", "harem"),
|
||||
CatUnit("Гендерная интрига", "genderintrigue"),
|
||||
CatUnit("Героическое фэнтези", "heroic_fantasy"),
|
||||
CatUnit("Детектив", "detective"),
|
||||
CatUnit("Дзёсэй", "josei"),
|
||||
CatUnit("Додзинси", "doujinshi"),
|
||||
CatUnit("Драма", "drama"),
|
||||
CatUnit("Ёнкома", "yonkoma"),
|
||||
CatUnit("Есси", "18+"),
|
||||
CatUnit("Зомби", "zombie"),
|
||||
CatUnit("Игра", "games"),
|
||||
CatUnit("Инцест", "incest"),
|
||||
CatUnit("Исекай", "isekai"),
|
||||
CatUnit("Искусство", "iskusstvo"),
|
||||
CatUnit("Исторический", "historical"),
|
||||
CatUnit("Киберпанк", "cyberpunk"),
|
||||
CatUnit("Кодомо", "kodomo"),
|
||||
CatUnit("Комедия", "comedy"),
|
||||
CatUnit("Культовое", "iconic"),
|
||||
CatUnit("литРПГ", "litrpg"),
|
||||
CatUnit("Любовь", "love"),
|
||||
CatUnit("Махо-сёдзё", "maho-shojo"),
|
||||
CatUnit("Меха", "robots"),
|
||||
CatUnit("Мистика", "mystery"),
|
||||
CatUnit("Мужская беременность", "male-pregnancy"),
|
||||
CatUnit("Музыка", "music"),
|
||||
CatUnit("Научная фантастика", "sciencefiction"),
|
||||
CatUnit("Новинки", "new"),
|
||||
CatUnit("Омегаверс", "omegavers"),
|
||||
CatUnit("Перерождение", "newlife"),
|
||||
CatUnit("Повседневность", "humdrum"),
|
||||
CatUnit("Постапокалиптика", "postapocalyptic"),
|
||||
CatUnit("Приключения", "adventure"),
|
||||
CatUnit("Психология", "psychology"),
|
||||
CatUnit("Романтика", "romance"),
|
||||
CatUnit("Самураи", "samurai"),
|
||||
CatUnit("Сборник", "compilation"),
|
||||
CatUnit("Сверхъестественное", "supernatural"),
|
||||
CatUnit("Сёдзё", "shojo"),
|
||||
CatUnit("Сёдзё-ай", "maho-shojo"),
|
||||
CatUnit("Сёнэн", "senen"),
|
||||
CatUnit("Сёнэн-ай", "shonen-ai"),
|
||||
CatUnit("Сетакон", "setakon"),
|
||||
CatUnit("Сингл", "singl"),
|
||||
CatUnit("Сказка", "fable"),
|
||||
CatUnit("Сорс", "bdsm"),
|
||||
CatUnit("Спорт", "sport"),
|
||||
CatUnit("Супергерои", "superheroes"),
|
||||
CatUnit("Сэйнэн", "seinen"),
|
||||
CatUnit("Танцы", "dancing"),
|
||||
CatUnit("Трагедия", "tragedy"),
|
||||
CatUnit("Триллер", "thriller"),
|
||||
CatUnit("Ужасы", "horror"),
|
||||
CatUnit("Фантастика", "fantastic"),
|
||||
CatUnit("Фурри", "furri"),
|
||||
CatUnit("Фэнтези", "fantasy"),
|
||||
CatUnit("Школа", "school"),
|
||||
CatUnit("Эротика", "erotica"),
|
||||
CatUnit("Этти", "etty"),
|
||||
CatUnit("Юмор", "humor"),
|
||||
CatUnit("Юри", "yuri"),
|
||||
CatUnit("Яой", "yaoi"),
|
||||
)
|
||||
}
|
||||
2
src/ru/mangaclub/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/ru/mangaclub/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'MangaClub'
|
||||
pkgNameSuffix = 'ru.mangaclub'
|
||||
extClass = '.MangaClub'
|
||||
extVersionCode = 12
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/mangaclub/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/ru/mangaclub/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/ru/mangaclub/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/ru/mangaclub/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src/ru/mangaclub/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/ru/mangaclub/res/web_hi_res_256.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,210 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.mangaclub
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
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.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class MangaClub : ParsedHttpSource() {
|
||||
|
||||
/** Info **/
|
||||
override val name: String = "MangaClub"
|
||||
override val baseUrl: String = "https://mangaclub.ru"
|
||||
override val lang: String = "ru"
|
||||
override val supportsLatest: Boolean = true
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
/** Popular **/
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/f/sort=rating/order=desc/page/$page/", headers)
|
||||
override fun popularMangaNextPageSelector(): String = "div.pagination-list i.icon-right-open"
|
||||
override fun popularMangaSelector(): String = "div.shortstory"
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
thumbnail_url = element.select("div.content-block div.image img").attr("abs:src")
|
||||
element.select("div.content-title h4.title a").apply {
|
||||
title = this.text().replace("\\'", "'").substringBefore("/").trim()
|
||||
setUrlWithoutDomain(this.attr("abs:href"))
|
||||
}
|
||||
}
|
||||
|
||||
/** Latest **/
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/", headers)
|
||||
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
|
||||
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
/** Search **/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
var url = baseUrl
|
||||
if (query.isNotEmpty()) {
|
||||
val formData = FormBody.Builder()
|
||||
.add("do", "search")
|
||||
.add("subaction", "search")
|
||||
.add("search_start", "$page")
|
||||
.add("full_search", "0")
|
||||
.add("result_from", "${((page - 1) * 8) + 1}")
|
||||
.add("story", query).build()
|
||||
val requestHeaders = headers.newBuilder()
|
||||
.add("Content-Type", "application/x-www-form-urlencoded").build()
|
||||
return POST("$url/index.php?do=search", requestHeaders, formData)
|
||||
} else {
|
||||
url += "/f"
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is GenreList -> {
|
||||
val genresIDs = mutableListOf<String>()
|
||||
filter.state.forEach { genre -> if (genre.state) genresIDs += genre.id }
|
||||
if (genresIDs.isNotEmpty()) url += "/n.l.tags=${genresIDs.joinToString(",")}"
|
||||
}
|
||||
is CategoryList -> {
|
||||
val categoriesIDs = mutableListOf<String>()
|
||||
filter.state.forEach { category -> if (category.state) categoriesIDs += category.id }
|
||||
if (categoriesIDs.isNotEmpty()) url += "/o.cat=${categoriesIDs.joinToString(",")}"
|
||||
}
|
||||
is Status -> {
|
||||
val statusID = arrayOf("Не выбрано", "Завершен", "Продолжается", "Заморожено/Заброшено")[filter.state]
|
||||
if (filter.state > 0) url += "/status_translation=$statusID"
|
||||
}
|
||||
is OrderBy -> {
|
||||
val orderState = if (filter.state!!.ascending) "asc" else "desc"
|
||||
val orderID = arrayOf("date", "editdate", "title", "comm_num", "news_read", "rating")[filter.state!!.index]
|
||||
url += "/sort=$orderID/order=$orderState"
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
url += "/page/$page"
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
|
||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
/** Details **/
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
thumbnail_url = document.select("div.image img").attr("abs:src")
|
||||
title = document.select("div.info strong").text().replace("\\'", "'").substringBefore("/").trim()
|
||||
author = document.select("div.info a[href*=author]").joinToString(", ") { it.text().trim() }
|
||||
artist = author
|
||||
status = if (document.select("div.fullstory").text().contains("Данное произведение лицензировано на территории РФ. Главы удалены.")) SManga.LICENSED else when (document.select("div.info a[href*=status_translation]").text().trim()) {
|
||||
"Продолжается" -> SManga.ONGOING
|
||||
"Завершен" -> SManga.COMPLETED
|
||||
"Заморожено/Заброшено" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
description = document.select(".description").first()!!.text()
|
||||
genre = document.select("div.info a[href*=tags]").joinToString(", ") {
|
||||
it.text().replaceFirstChar { it.uppercase() }.trim()
|
||||
}
|
||||
}
|
||||
|
||||
/** Chapters **/
|
||||
private val dateParse = SimpleDateFormat("dd.MM.yyyy", Locale.ROOT)
|
||||
override fun chapterListSelector(): String = "div.chapters div.chapter-item"
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
val chapterLink = element.select("div.chapter-item div.item-left a")
|
||||
name = chapterLink.text().replace(",", ".").trim()
|
||||
chapter_number = name.substringAfter("Глава").trim().toFloat()
|
||||
date_upload = element.select("div.chapter-item div.item-right div.date").text().trim().let { dateParse.parse(it)?.time ?: 0L }
|
||||
setUrlWithoutDomain(chapterLink.attr("abs:href"))
|
||||
}
|
||||
|
||||
/** Pages **/
|
||||
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
|
||||
document.select(".manga-lines-page a").forEach {
|
||||
add(Page(it.attr("data-p").toInt(), "", it.attr("data-i")))
|
||||
}
|
||||
}
|
||||
override fun imageUrlParse(document: Document): String = ""
|
||||
|
||||
/** Filters **/
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Жанры", genres)
|
||||
private class Genre(name: String, val id: String) : Filter.CheckBox(name)
|
||||
private class CategoryList(categories: List<Category>) : Filter.Group<Category>("Категория", categories)
|
||||
private class Category(name: String, val id: String) : Filter.CheckBox(name)
|
||||
private class Status : Filter.Select<String>(
|
||||
"Статус",
|
||||
arrayOf("Не выбрано", "Завершен", "Продолжается", "Заморожено/Заброшено"),
|
||||
)
|
||||
private class OrderBy : Filter.Sort(
|
||||
"Сортировка",
|
||||
arrayOf("По дате добавления", "По дате обновления", "В алфавитном порядке", "По количеству комментариев", "По количеству просмотров", "По рейтингу"),
|
||||
Selection(5, false),
|
||||
)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
GenreList(getGenreList()),
|
||||
Status(),
|
||||
CategoryList(getCategoryList()),
|
||||
OrderBy(),
|
||||
)
|
||||
|
||||
private fun getCategoryList() = listOf(
|
||||
Category("Манга", "1"),
|
||||
Category("Манхва", "2"),
|
||||
Category("Маньхуа", "3"),
|
||||
Category("Веб-манхва", "6"),
|
||||
)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Боевик", "боевик"),
|
||||
Genre("Боевые искусства", "боевые+искусства"),
|
||||
Genre("Вампиры", "вампиры"),
|
||||
Genre("Гарем", "гарем"),
|
||||
Genre("Гендерная интрига", "гендерная+интрига"),
|
||||
Genre("Героическое фэнтези", "героическое+фэнтези"),
|
||||
Genre("Детектив", "детектив"),
|
||||
Genre("Дзёсэй", "дзёсэй"),
|
||||
Genre("Додзинси", "додзинси"),
|
||||
Genre("Драма", "драма"),
|
||||
Genre("Игра", "игра"),
|
||||
Genre("История", "история"),
|
||||
Genre("Киберпанк", "киберпанк"),
|
||||
Genre("Комедия", "комедия"),
|
||||
Genre("Махо-сёдзё", "махо-сёдзё"),
|
||||
Genre("Меха", "меха"),
|
||||
Genre("Мистика", "мистика"),
|
||||
Genre("Музыка", "музыка"),
|
||||
Genre("Научная фантастика", "научная+фантастика"),
|
||||
Genre("Перерождение", "перерождение"),
|
||||
Genre("Повседневность", "повседневность"),
|
||||
Genre("Постапокалиптика", "постапокалиптика"),
|
||||
Genre("Приключения", "приключения"),
|
||||
Genre("Психология", "психология"),
|
||||
Genre("Романтика", "романтика"),
|
||||
Genre("Самурайский боевик", "самурайский+боевик"),
|
||||
Genre("Сборник", "сборник"),
|
||||
Genre("Сверхъестественное", "сверхъестественное"),
|
||||
Genre("Сингл", "сингл"),
|
||||
Genre("Спорт", "спорт"),
|
||||
Genre("Сэйнэн", "сэйнэн"),
|
||||
Genre("Сёдзё", "сёдзё"),
|
||||
Genre("Сёдзё для взрослых", "сёдзе+для+взрослых"),
|
||||
Genre("Сёдзё-ай", "сёдзё-ай"),
|
||||
Genre("Сёнэн", "сёнэн"),
|
||||
Genre("Сёнэн-ай", "сёнэн-ай"),
|
||||
Genre("Трагедия", "трагедия"),
|
||||
Genre("Триллер", "триллер"),
|
||||
Genre("Ужасы", "ужасы"),
|
||||
Genre("Фантастика", "фантастика"),
|
||||
Genre("Фэнтези", "фэнтези"),
|
||||
Genre("Школа", "школа"),
|
||||
Genre("Эротика", "эротика"),
|
||||
Genre("Ёнкома", "ёнкома"),
|
||||
Genre("Этти", "этти"),
|
||||
Genre("Юри", "юри"),
|
||||
Genre("Яой", "яой"),
|
||||
)
|
||||
}
|
||||
2
src/ru/mangahub/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/ru/mangahub/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Mangahub'
|
||||
pkgNameSuffix = 'ru.mangahub'
|
||||
extClass = '.Mangahub'
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/mangahub/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/ru/mangahub/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/ru/mangahub/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/ru/mangahub/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/ru/mangahub/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/ru/mangahub/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
@@ -0,0 +1,149 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.mangahub
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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 kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
open class Mangahub : ParsedHttpSource() {
|
||||
|
||||
override val name = "Mangahub"
|
||||
|
||||
override val baseUrl = "https://mangahub.ru"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
private val userAgentRandomizer = "${Random.nextInt().absoluteValue}"
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Edg/100.0.$userAgentRandomizer")
|
||||
add("Referer", baseUrl)
|
||||
}
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/explore?filter[sort]=rating&filter[dateStart][left_number]=1900&filter[dateStart][right_number]=2099&page=$page", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/explore?filter[sort]=update&filter[dateStart][left_number]=1900&filter[dateStart][right_number]=2099&page=$page", headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.comic-grid-col-xl"
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("div.fast-view-layer-scale").attr("data-background-image")
|
||||
manga.title = element.select("a.fw-medium").text()
|
||||
manga.setUrlWithoutDomain(element.select("a.fw-medium").attr("href"))
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = ".page-link:contains(→)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/search/manga?query=$query&sort=rating_short&page=$page")
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = popularMangaNextPageSelector()
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + ("/chapters/" + manga.url.removePrefix("/title/")), headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.author = document.select("div.detail-attr:contains(Автор) div:gt(0)").text() // TODO: Add "Сценарист" and "Художник"
|
||||
manga.genre = document.select(".tags a").joinToString { it.text() }
|
||||
manga.description = document.select("div.markdown-style").text()
|
||||
manga.status = parseStatus(document.select("div.detail-attr:contains(перевод):eq(0)").toString())
|
||||
manga.thumbnail_url = document.select("img.cover-detail").attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(elements: String): Int = when {
|
||||
elements.contains("Переведена") or elements.contains("Выпуск завершен") -> SManga.COMPLETED
|
||||
else -> SManga.ONGOING
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.py-2.px-3"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("div.align-items-center > a").first()!!
|
||||
val chapter = SChapter.create()
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("div.text-muted").text().let {
|
||||
SimpleDateFormat("dd.MM.yyyy", Locale.US).parse(it)?.time ?: 0L
|
||||
}
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
val basic = Regex("(Глава\\s)((\\d|\\.)+)")
|
||||
when {
|
||||
basic.containsMatchIn(chapter.name) -> {
|
||||
basic.find(chapter.name)?.let {
|
||||
chapter.chapter_number = it.groups[2]?.value!!.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val chapInfo = document.select("reader")
|
||||
.attr("data-store")
|
||||
.replace(""", "\"")
|
||||
.replace("\\/", "/")
|
||||
val chapter = json.parseToJsonElement(chapInfo).jsonObject
|
||||
|
||||
return chapter["scans"]!!.jsonArray.mapIndexed { i, jsonEl ->
|
||||
Page(i, "", "https:" + jsonEl.jsonObject["src"]!!.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeader = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
.add("Referer", baseUrl)
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, imgHeader)
|
||||
}
|
||||
}
|
||||
2
src/ru/mangapoisk/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/ru/mangapoisk/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'MangaPoisk'
|
||||
pkgNameSuffix = 'ru.mangapoisk'
|
||||
extClass = '.MangaPoisk'
|
||||
extVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/mangapoisk/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/ru/mangapoisk/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/ru/mangapoisk/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
src/ru/mangapoisk/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/ru/mangapoisk/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/ru/mangapoisk/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
@@ -0,0 +1,289 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.mangapoisk
|
||||
|
||||
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.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class MangaPoisk : ParsedHttpSource() {
|
||||
override val name = "MangaPoisk"
|
||||
|
||||
override val baseUrl = "https://mangapoisk.me"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Edg/100.0.1185.50")
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/manga?sortBy=popular&page=$page", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/manga?sortBy=-last_chapter_at&page=$page", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
"$baseUrl/search?q=$query&page=$page"
|
||||
} else {
|
||||
val url = "$baseUrl/manga?page=$page".toHttpUrlOrNull()!!.newBuilder()
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
val ord = arrayOf("-year", "popular", "name", "-published_at", "-last_chapter_at")[filter.state!!.index]
|
||||
val ordRev = arrayOf("year", "-popular", "-name", "published_at", "last_chapter_at")[filter.state!!.index]
|
||||
url.addQueryParameter("sortBy", if (filter.state!!.ascending) ordRev else ord)
|
||||
}
|
||||
is StatusList -> filter.state.forEach { status ->
|
||||
if (status.state) {
|
||||
url.addQueryParameter("translated[]", status.id)
|
||||
}
|
||||
}
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
if (genre.state) {
|
||||
url.addQueryParameter("genres[]", genre.id)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String = "article.card"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
thumbnail_url = getImage(element.select("a > img").first()!!)
|
||||
|
||||
setUrlWithoutDomain(element.select("a.card-about").first()!!.attr("href"))
|
||||
|
||||
element.select("a > h2.entry-title").first()!!.let {
|
||||
title = it.text().split("/").first()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val mangas = if (document.location().contains("search?q")) {
|
||||
document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
} else {
|
||||
document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, mangas.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): Nothing? = null
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun popularMangaSelector() = ".manga-card"
|
||||
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
thumbnail_url = getImage(element.select("a > img").first()!!)
|
||||
|
||||
setUrlWithoutDomain(element.select("a").first()!!.attr("href"))
|
||||
|
||||
element.select("a").first()!!.let {
|
||||
title = it.attr("title").split("/").first()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
private fun getImage(first: Element): String? {
|
||||
val image = first.attr("data-src")
|
||||
if (image.isNotEmpty()) {
|
||||
return image
|
||||
}
|
||||
return first.attr("src")
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.card:has(header)").first()!!
|
||||
val manga = SManga.create()
|
||||
manga.title = infoElement.select(".text-base span").first()!!.text()
|
||||
manga.genre = infoElement.select("span:contains(Жанр:) a").joinToString { it.text() }
|
||||
manga.description = infoElement.select(".manga-description").text()
|
||||
manga.status = parseStatus(infoElement.select("span:contains(Статус:)").text())
|
||||
manga.thumbnail_url = infoElement.select("img.w-full").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 fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val document = client.newCall(GET("$baseUrl${manga.url}?tab=chapters", headers)).execute().asJsoup()
|
||||
if (document.select(".text-md:contains(Главы удалены по требованию правообладателя)").isNotEmpty()) {
|
||||
return Observable.error(Exception("Лицензировано - Нет глав"))
|
||||
}
|
||||
val pageItems = client.newCall(chapterListRequest(manga)).execute().asJsoup().select("li.page-item")
|
||||
val pages = mutableListOf(1)
|
||||
if (pageItems.lastIndex > 1) {
|
||||
val lastPage = pageItems[pageItems.lastIndex - 1].text().toInt()
|
||||
for (i in 2.rangeTo(lastPage)) {
|
||||
pages.add(i)
|
||||
}
|
||||
}
|
||||
|
||||
return Observable.just(
|
||||
pages.flatMap { page ->
|
||||
chapterListParse(client.newCall(chapterPageListRequest(manga, page)).execute(), manga)
|
||||
},
|
||||
)
|
||||
}
|
||||
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, manga) }
|
||||
}
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return GET("$baseUrl${manga.url}/chaptersList", headers)
|
||||
}
|
||||
|
||||
private fun chapterPageListRequest(manga: SManga, page: Int): Request {
|
||||
return GET("$baseUrl${manga.url}/chaptersList?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".chapter-item"
|
||||
|
||||
private fun chapterFromElement(element: Element, manga: SManga): SChapter {
|
||||
val title = element.select("span.chapter-title").first()!!.text()
|
||||
val urlElement = element.select("a").first()!!
|
||||
val urlText = urlElement.text()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
|
||||
chapter.name = urlText.trim()
|
||||
chapter.chapter_number = "Глава\\s(\\d+)".toRegex(RegexOption.IGNORE_CASE).find(title)?.groupValues?.get(1)?.toFloat() ?: -1F
|
||||
chapter.date_upload = element.select("span.chapter-date").first()?.text()?.let {
|
||||
try {
|
||||
when {
|
||||
it.contains("минут") -> Date(System.currentTimeMillis() - it.split("\\s".toRegex())[0].toLong() * 60 * 1000).time
|
||||
it.contains("час") -> Date(System.currentTimeMillis() - it.split("\\s".toRegex())[0].toLong() * 60 * 60 * 1000).time
|
||||
it.contains("дня") || it.contains("дней") -> Date(System.currentTimeMillis() - it.split("\\s".toRegex())[0].toLong() * 24 * 60 * 60 * 1000).time
|
||||
else -> SimpleDateFormat("dd MMMM yyyy", Locale("ru")).parse(it)?.time ?: 0L
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Date(System.currentTimeMillis()).time
|
||||
}
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
if (document.toString().contains("text-error-500-400-token")) {
|
||||
throw Exception("Лицензировано - Глава удалена по требованию правообладателя.")
|
||||
}
|
||||
return document.select(".page-image").mapIndexed { index, element ->
|
||||
Page(index, "", getImage(element))
|
||||
}
|
||||
}
|
||||
|
||||
private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
|
||||
|
||||
private class StatusList(statuses: List<CheckFilter>) : Filter.Group<CheckFilter>("Статус", statuses)
|
||||
private class GenreList(genres: List<CheckFilter>) : Filter.Group<CheckFilter>("Жанры", genres)
|
||||
override fun getFilterList() = FilterList(
|
||||
OrderBy(),
|
||||
StatusList(getStatusList()),
|
||||
GenreList(getGenreList()),
|
||||
)
|
||||
|
||||
private class OrderBy : Filter.Sort(
|
||||
"Сортировка",
|
||||
arrayOf("Год", "Популярности", "Алфавиту", "Дате добавления", "Дате обновления"),
|
||||
Selection(1, false),
|
||||
)
|
||||
|
||||
private fun getStatusList() = listOf(
|
||||
CheckFilter("Выпускается", "0"),
|
||||
CheckFilter("Завершена", "1"),
|
||||
)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
CheckFilter("приключения", "1"),
|
||||
CheckFilter("романтика", "2"),
|
||||
CheckFilter("боевик", "3"),
|
||||
CheckFilter("комедия", "4"),
|
||||
CheckFilter("сверхъестественное", "5"),
|
||||
CheckFilter("драма", "6"),
|
||||
CheckFilter("фэнтези", "7"),
|
||||
CheckFilter("сёнэн", "8"),
|
||||
CheckFilter("этти", "7"),
|
||||
CheckFilter("вампиры", "10"),
|
||||
CheckFilter("школа", "11"),
|
||||
CheckFilter("сэйнэн", "12"),
|
||||
CheckFilter("повседневность", "18"),
|
||||
CheckFilter("сёнэн-ай", "19"),
|
||||
CheckFilter("гарем", "29"),
|
||||
CheckFilter("героическое фэнтези", "30"),
|
||||
CheckFilter("боевые искусства", "31"),
|
||||
CheckFilter("психология", "38"),
|
||||
CheckFilter("сёдзё", "57"),
|
||||
CheckFilter("игра", "105"),
|
||||
CheckFilter("триллер", "120"),
|
||||
CheckFilter("детектив", "121"),
|
||||
CheckFilter("трагедия", "122"),
|
||||
CheckFilter("история", "123"),
|
||||
CheckFilter("сёдзё-ай", "147"),
|
||||
CheckFilter("спорт", "160"),
|
||||
CheckFilter("научная фантастика", "171"),
|
||||
CheckFilter("гендерная интрига", "172"),
|
||||
CheckFilter("дзёсэй", "230"),
|
||||
CheckFilter("ужасы", "260"),
|
||||
CheckFilter("постапокалиптика", "310"),
|
||||
CheckFilter("киберпанк", "355"),
|
||||
CheckFilter("меха", "356"),
|
||||
CheckFilter("эротика", "380"),
|
||||
CheckFilter("яой", "612"),
|
||||
CheckFilter("самурайский боевик", "916"),
|
||||
CheckFilter("махо-сёдзё", "1472"),
|
||||
CheckFilter("додзинси", "1785"),
|
||||
CheckFilter("кодомо", "1789"),
|
||||
CheckFilter("юри", "3197"),
|
||||
CheckFilter("арт", "7332"),
|
||||
CheckFilter("омегаверс", "7514"),
|
||||
CheckFilter("бара", "8119"),
|
||||
)
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw Exception("Not Used")
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = throw Exception("Not Used")
|
||||
}
|
||||
2
src/ru/newbie/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
16
src/ru/newbie/build.gradle
Normal file
@@ -0,0 +1,16 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'NewManga(Newbie)'
|
||||
pkgNameSuffix = 'ru.newbie'
|
||||
extClass = '.Newbie'
|
||||
extVersionCode = 19
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':lib-dataimage')
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/newbie/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/ru/newbie/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/ru/newbie/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src/ru/newbie/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
src/ru/newbie/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/ru/newbie/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,678 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.newbie
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.preference.ListPreference
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.BookDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.BranchesDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.LibraryDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.MangaDetDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.PageDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.PageWrapperDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.SearchLibraryDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.SearchWrapperDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.SeriesWrapperDto
|
||||
import eu.kanade.tachiyomi.extension.ru.newbie.dto.SubSearchDto
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.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 kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.Jsoup
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
class Newbie : ConfigurableSource, HttpSource() {
|
||||
override val name = "NewManga(Newbie)"
|
||||
|
||||
override val id: Long = 8033757373676218584
|
||||
|
||||
override val baseUrl = "https://newmanga.org"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private var branches = mutableMapOf<String, List<BranchesDto>>()
|
||||
|
||||
private val userAgentRandomizer = "${Random.nextInt().absoluteValue}"
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Edg/100.0.$userAgentRandomizer")
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
private fun imageContentTypeIntercept(chain: Interceptor.Chain): Response {
|
||||
if (chain.request().url.queryParameter("slice").isNullOrEmpty()) {
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
|
||||
val response = chain.proceed(chain.request())
|
||||
val image = response.body.byteString().toResponseBody("image/*".toMediaType())
|
||||
return response.newBuilder().body(image).build()
|
||||
}
|
||||
|
||||
override val client: OkHttpClient =
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(API_URL.toHttpUrl(), 2)
|
||||
.addInterceptor { imageContentTypeIntercept(it) }
|
||||
.build()
|
||||
|
||||
private val count = 30
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$API_URL/projects/popular?scale=month&size=$count&page=$page", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val page = json.decodeFromString<PageWrapperDto<LibraryDto>>(response.body.string())
|
||||
val mangas = page.items.map {
|
||||
it.toSManga()
|
||||
}
|
||||
return MangasPage(mangas, mangas.isNotEmpty())
|
||||
}
|
||||
|
||||
private fun LibraryDto.toSManga(): SManga {
|
||||
val o = this
|
||||
return SManga.create().apply {
|
||||
// Do not change the title name to ensure work with a multilingual catalog!
|
||||
title = if (isEng.equals("rus")) o.title.ru else o.title.en
|
||||
url = "$id"
|
||||
thumbnail_url = "$IMAGE_URL/${image.name}"
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$API_URL/projects/updates?only_bookmarks=false&size=$count&page=$page", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val page = json.decodeFromString<SearchWrapperDto<SubSearchDto<SearchLibraryDto>>>(response.body.string())
|
||||
val mangas = page.result.hits.map {
|
||||
it.toSearchManga()
|
||||
}
|
||||
return MangasPage(mangas, mangas.isNotEmpty())
|
||||
}
|
||||
|
||||
private fun SearchLibraryDto.toSearchManga(): SManga {
|
||||
return SManga.create().apply {
|
||||
// Do not change the title name to ensure work with a multilingual catalog!
|
||||
title = if (isEng.equals("rus")) document.title_ru else document.title_en
|
||||
url = document.id
|
||||
thumbnail_url = if (document.image_large.isNotEmpty()) {
|
||||
"$IMAGE_URL/${document.image_large}"
|
||||
} else {
|
||||
"$IMAGE_URL/${document.image_small}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US) }
|
||||
|
||||
private fun parseDate(date: String?): Long {
|
||||
date ?: return 0L
|
||||
return try {
|
||||
simpleDateFormat.parse(date)!!.time
|
||||
} catch (_: Exception) {
|
||||
Date().time
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val mutableGenre = mutableListOf<String>()
|
||||
val mutableExGenre = mutableListOf<String>()
|
||||
val mutableTag = mutableListOf<String>()
|
||||
val mutableExTag = mutableListOf<String>()
|
||||
val mutableType = mutableListOf<String>()
|
||||
val mutableStatus = mutableListOf<String>()
|
||||
val mutableTitleStatus = mutableListOf<String>()
|
||||
val mutableAge = mutableListOf<String>()
|
||||
var orderBy = "MATCH"
|
||||
var ascEnd = "DESC"
|
||||
var requireChapters = true
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
if (query.isEmpty()) {
|
||||
orderBy = arrayOf("RATING", "VIEWS", "HEARTS", "COUNT_CHAPTERS", "CREATED_AT", "UPDATED_AT")[filter.state!!.index]
|
||||
ascEnd = if (filter.state!!.ascending) "ASC" else "DESC"
|
||||
}
|
||||
}
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
if (genre.state != Filter.TriState.STATE_IGNORE) {
|
||||
if (genre.isIncluded()) mutableGenre += '"' + genre.name + '"' else mutableExGenre += '"' + genre.name + '"'
|
||||
}
|
||||
}
|
||||
is TagsList -> filter.state.forEach { tag ->
|
||||
if (tag.state != Filter.TriState.STATE_IGNORE) {
|
||||
if (tag.isIncluded()) mutableTag += '"' + tag.name + '"' else mutableExTag += '"' + tag.name + '"'
|
||||
}
|
||||
}
|
||||
is TypeList -> filter.state.forEach { type ->
|
||||
if (type.state) {
|
||||
mutableType += '"' + type.id + '"'
|
||||
}
|
||||
}
|
||||
is StatusList -> filter.state.forEach { status ->
|
||||
if (status.state) {
|
||||
mutableStatus += '"' + status.id + '"'
|
||||
}
|
||||
}
|
||||
is StatusTitleList -> filter.state.forEach { status ->
|
||||
if (status.state) {
|
||||
mutableTitleStatus += '"' + status.id + '"'
|
||||
}
|
||||
}
|
||||
is AgeList -> filter.state.forEach { age ->
|
||||
if (age.state) {
|
||||
mutableAge += '"' + age.id + '"'
|
||||
}
|
||||
}
|
||||
is RequireChapters -> {
|
||||
if (filter.state == 1) {
|
||||
requireChapters = false
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return POST(
|
||||
"https://neo.newmanga.org/catalogue",
|
||||
body = """{"query":"$query","sort":{"kind":"$orderBy","dir":"$ascEnd"},"filter":{"hidden_projects":[],"genres":{"excluded":$mutableExGenre,"included":$mutableGenre},"tags":{"excluded":$mutableExTag,"included":$mutableTag},"type":{"allowed":$mutableType},"translation_status":{"allowed":$mutableStatus},"released_year":{"min":null,"max":null},"require_chapters":$requireChapters,"original_status":{"allowed":$mutableTitleStatus},"adult":{"allowed":$mutableAge}},"pagination":{"page":$page,"size":$count}}""".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
|
||||
headers = headers,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String): Int {
|
||||
return when (status) {
|
||||
"completed" -> SManga.COMPLETED
|
||||
"on_going" -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseType(type: String): String {
|
||||
return when (type) {
|
||||
"manga" -> "Манга"
|
||||
"manhwa" -> "Манхва"
|
||||
"manhya" -> "Маньхуа"
|
||||
"single" -> "Сингл"
|
||||
"comics" -> "Комикс"
|
||||
"russian" -> "Руманга"
|
||||
else -> type
|
||||
}
|
||||
}
|
||||
private fun parseAge(adult: String): String {
|
||||
return when (adult) {
|
||||
"" -> "0+"
|
||||
else -> "$adult+"
|
||||
}
|
||||
}
|
||||
|
||||
private fun MangaDetDto.toSManga(): SManga {
|
||||
val ratingValue = DecimalFormat("#,###.##").format(rating * 2).replace(",", ".").toFloat()
|
||||
val ratingStar = when {
|
||||
ratingValue > 9.5 -> "★★★★★"
|
||||
ratingValue > 8.5 -> "★★★★✬"
|
||||
ratingValue > 7.5 -> "★★★★☆"
|
||||
ratingValue > 6.5 -> "★★★✬☆"
|
||||
ratingValue > 5.5 -> "★★★☆☆"
|
||||
ratingValue > 4.5 -> "★★✬☆☆"
|
||||
ratingValue > 3.5 -> "★★☆☆☆"
|
||||
ratingValue > 2.5 -> "★✬☆☆☆"
|
||||
ratingValue > 1.5 -> "★☆☆☆☆"
|
||||
ratingValue > 0.5 -> "✬☆☆☆☆"
|
||||
else -> "☆☆☆☆☆"
|
||||
}
|
||||
val o = this
|
||||
return SManga.create().apply {
|
||||
// Do not change the title name to ensure work with a multilingual catalog!
|
||||
title = if (isEng.equals("rus")) o.title.ru else o.title.en
|
||||
url = "$id"
|
||||
thumbnail_url = "$IMAGE_URL/${image.name}"
|
||||
author = o.author?.name
|
||||
artist = o.artist?.name
|
||||
val mediaNameLanguage = if (isEng.equals("rus")) o.title.en else o.title.ru
|
||||
description = mediaNameLanguage + "\n" + ratingStar + " " + ratingValue + " [♡" + hearts + "]\n" + Jsoup.parse(o.description).text()
|
||||
genre = parseType(type) + ", " + adult?.let { parseAge(it) } + ", " + genres.joinToString { it.title.ru.capitalize() }
|
||||
status = parseStatus(o.status)
|
||||
}
|
||||
}
|
||||
|
||||
private fun titleDetailsRequest(manga: SManga): Request {
|
||||
return GET(API_URL + "/projects/" + manga.url, headers)
|
||||
}
|
||||
|
||||
// Workaround to allow "Open in browser" use the real URL.
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(titleDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + "/p/" + manga.url, headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val series = json.decodeFromString<MangaDetDto>(response.body.string())
|
||||
branches[series.id.toString()] = series.branches
|
||||
return series.toSManga()
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private fun chapterName(book: BookDto): String {
|
||||
var chapterName = "${book.tom}. Глава ${DecimalFormat("#,###.##").format(book.number).replace(",", ".")}"
|
||||
if (!book.is_available) {
|
||||
chapterName += " \uD83D\uDCB2 "
|
||||
}
|
||||
if (book.name?.isNotBlank() == true) {
|
||||
chapterName += " ${book.name.capitalize()}"
|
||||
}
|
||||
return chapterName
|
||||
}
|
||||
|
||||
private fun mangaBranches(manga: SManga): List<BranchesDto> {
|
||||
val response = client.newCall(titleDetailsRequest(manga)).execute()
|
||||
val series = json.decodeFromString<MangaDetDto>(response.body.string())
|
||||
branches[series.id.toString()] = series.branches
|
||||
return series.branches
|
||||
}
|
||||
|
||||
private fun selector(b: BranchesDto): Boolean = b.is_default
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val branch = branches.getOrElse(manga.url) { mangaBranches(manga) }
|
||||
return when {
|
||||
branch.isEmpty() -> {
|
||||
return Observable.just(listOf())
|
||||
}
|
||||
manga.status == SManga.LICENSED -> {
|
||||
Observable.error(Exception("Лицензировано - Нет глав"))
|
||||
}
|
||||
else -> {
|
||||
val branchId = branch.first { selector(it) }.id
|
||||
client.newCall(chapterListRequest(branchId))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response, manga, branchId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("chapterListParse(response: Response, manga: SManga)")
|
||||
|
||||
private fun chapterListParse(response: Response, manga: SManga, branch: Long): List<SChapter> {
|
||||
var chapters = json.decodeFromString<SeriesWrapperDto<List<BookDto>>>(response.body.string()).items
|
||||
if (!preferences.getBoolean(PAID_PREF, false)) {
|
||||
chapters = chapters.filter { it.is_available }
|
||||
}
|
||||
return chapters.map { chapter ->
|
||||
SChapter.create().apply {
|
||||
chapter_number = chapter.number
|
||||
name = chapterName(chapter)
|
||||
url = "/p/${manga.url}/$branch/r/${chapter.id}"
|
||||
date_upload = parseDate(chapter.created_at)
|
||||
scanlator = chapter.translator
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException("chapterListRequest(branch: Long)")
|
||||
private fun chapterListRequest(branch: Long): Request {
|
||||
return GET(
|
||||
"$API_URL/branches/$branch/chapters?reverse=true&size=1000000",
|
||||
headers,
|
||||
)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(API_URL + "/chapters/${chapter.url.substringAfterLast("/")}/pages", headers)
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return baseUrl + chapter.url
|
||||
}
|
||||
|
||||
private fun pageListParse(response: Response, urlRequest: String): List<Page> {
|
||||
val pages = json.decodeFromString<List<PageDto>>(response.body.string())
|
||||
val result = mutableListOf<Page>()
|
||||
pages.forEach { page ->
|
||||
(1..page.slices!!).map { i ->
|
||||
result.add(Page(result.size, urlRequest + "/${page.id}?slice=$i"))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> = throw Exception("Not used")
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
pageListParse(response, pageListRequest(chapter).url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return Observable.just(page.url)
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request = throw NotImplementedError("Unused")
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw NotImplementedError("Unused")
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val refererHeaders = headersBuilder().build()
|
||||
return GET(page.imageUrl!!, refererHeaders)
|
||||
}
|
||||
|
||||
private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
|
||||
private class SearchFilter(name: String) : Filter.TriState(name)
|
||||
|
||||
private class TypeList(types: List<CheckFilter>) : Filter.Group<CheckFilter>("Типы", types)
|
||||
private class StatusList(statuses: List<CheckFilter>) : Filter.Group<CheckFilter>("Статус перевода", statuses)
|
||||
private class StatusTitleList(titles: List<CheckFilter>) : Filter.Group<CheckFilter>("Статус оригинала", titles)
|
||||
private class GenreList(genres: List<SearchFilter>) : Filter.Group<SearchFilter>("Жанры", genres)
|
||||
private class TagsList(tags: List<SearchFilter>) : Filter.Group<SearchFilter>("Теги", tags)
|
||||
private class AgeList(ages: List<CheckFilter>) : Filter.Group<CheckFilter>("Возрастное ограничение", ages)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
OrderBy(),
|
||||
GenreList(getGenreList()),
|
||||
TagsList(getTagsList()),
|
||||
TypeList(getTypeList()),
|
||||
StatusList(getStatusList()),
|
||||
StatusTitleList(getStatusTitleList()),
|
||||
AgeList(getAgeList()),
|
||||
RequireChapters(),
|
||||
)
|
||||
|
||||
private class OrderBy : Filter.Sort(
|
||||
"Сортировка",
|
||||
arrayOf("По рейтингу", "По просмотрам", "По лайкам", "По кол-ву глав", "По дате создания", "По дате обновления"),
|
||||
Selection(0, false),
|
||||
)
|
||||
|
||||
private class RequireChapters : Filter.Select<String>(
|
||||
"Только проекты с главами",
|
||||
arrayOf("Да", "Все"),
|
||||
)
|
||||
|
||||
private fun getTypeList() = listOf(
|
||||
CheckFilter("Манга", "MANGA"),
|
||||
CheckFilter("Манхва", "MANHWA"),
|
||||
CheckFilter("Маньхуа", "MANHYA"),
|
||||
CheckFilter("Сингл", "SINGLE"),
|
||||
CheckFilter("OEL-манга", "OEL"),
|
||||
CheckFilter("Комикс", "COMICS"),
|
||||
CheckFilter("Руманга", "RUSSIAN"),
|
||||
)
|
||||
|
||||
private fun getStatusList() = listOf(
|
||||
CheckFilter("Выпускается", "ON_GOING"),
|
||||
CheckFilter("Заброшен", "ABANDONED"),
|
||||
CheckFilter("Завершён", "COMPLETED"),
|
||||
)
|
||||
|
||||
private fun getStatusTitleList() = listOf(
|
||||
CheckFilter("Выпускается", "ON_GOING"),
|
||||
CheckFilter("Приостановлен", "SUSPENDED"),
|
||||
CheckFilter("Завершён", "COMPLETED"),
|
||||
CheckFilter("Анонс", "ANNOUNCEMENT"),
|
||||
)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
SearchFilter("cёнэн-ай"),
|
||||
SearchFilter("боевик"),
|
||||
SearchFilter("боевые искусства"),
|
||||
SearchFilter("гарем"),
|
||||
SearchFilter("гендерная интрига"),
|
||||
SearchFilter("героическое фэнтези"),
|
||||
SearchFilter("детектив"),
|
||||
SearchFilter("дзёсэй"),
|
||||
SearchFilter("додзинси"),
|
||||
SearchFilter("драма"),
|
||||
SearchFilter("ёнкома"),
|
||||
SearchFilter("игра"),
|
||||
SearchFilter("драма"),
|
||||
SearchFilter("ёнкома"),
|
||||
SearchFilter("игра"),
|
||||
SearchFilter("исекай"),
|
||||
SearchFilter("история"),
|
||||
SearchFilter("киберпанк"),
|
||||
SearchFilter("кодомо"),
|
||||
SearchFilter("комедия"),
|
||||
SearchFilter("махо-сёдзё"),
|
||||
SearchFilter("меха"),
|
||||
SearchFilter("мистика"),
|
||||
SearchFilter("научная фантастика"),
|
||||
SearchFilter("омегаверс"),
|
||||
SearchFilter("повседневность"),
|
||||
SearchFilter("постапокалиптика"),
|
||||
SearchFilter("приключения"),
|
||||
SearchFilter("психология"),
|
||||
SearchFilter("романтика"),
|
||||
SearchFilter("самурайский боевик"),
|
||||
SearchFilter("сверхъестественное"),
|
||||
SearchFilter("сёдзё"),
|
||||
SearchFilter("сёдзё-ай"),
|
||||
SearchFilter("сёнэн"),
|
||||
SearchFilter("спорт"),
|
||||
SearchFilter("сэйнэн"),
|
||||
SearchFilter("трагедия"),
|
||||
SearchFilter("триллер"),
|
||||
SearchFilter("ужасы"),
|
||||
SearchFilter("фантастика"),
|
||||
SearchFilter("фэнтези"),
|
||||
SearchFilter("школа"),
|
||||
SearchFilter("элементы юмора"),
|
||||
SearchFilter("эротика"),
|
||||
SearchFilter("этти"),
|
||||
SearchFilter("юри"),
|
||||
SearchFilter("яой"),
|
||||
)
|
||||
|
||||
private fun getTagsList() = listOf(
|
||||
SearchFilter("веб"),
|
||||
SearchFilter("в цвете"),
|
||||
SearchFilter("сборник"),
|
||||
SearchFilter("хентай"),
|
||||
SearchFilter("азартные игры"),
|
||||
SearchFilter("алхимия"),
|
||||
SearchFilter("амнезия"),
|
||||
SearchFilter("ангелы"),
|
||||
SearchFilter("антигерой"),
|
||||
SearchFilter("антиутопия"),
|
||||
SearchFilter("апокалипсис"),
|
||||
SearchFilter("аристократия"),
|
||||
SearchFilter("армия"),
|
||||
SearchFilter("артефакты"),
|
||||
SearchFilter("боги"),
|
||||
SearchFilter("бои на мечах"),
|
||||
SearchFilter("борьба за власть"),
|
||||
SearchFilter("брат и сестра"),
|
||||
SearchFilter("будущее"),
|
||||
SearchFilter("вампиры"),
|
||||
SearchFilter("ведьма"),
|
||||
SearchFilter("вестерн"),
|
||||
SearchFilter("видеоигры"),
|
||||
SearchFilter("виртуальная реальность"),
|
||||
SearchFilter("военные"),
|
||||
SearchFilter("война"),
|
||||
SearchFilter("волшебники"),
|
||||
SearchFilter("волшебные существа"),
|
||||
SearchFilter("воспоминания из другого мира"),
|
||||
SearchFilter("врачи / доктора"),
|
||||
SearchFilter("выживание"),
|
||||
SearchFilter("гг женщина"),
|
||||
SearchFilter("гг имба"),
|
||||
SearchFilter("гг мужчина"),
|
||||
SearchFilter("гг не человек"),
|
||||
SearchFilter("геймеры"),
|
||||
SearchFilter("гильдии"),
|
||||
SearchFilter("глупый гг"),
|
||||
SearchFilter("гоблины"),
|
||||
SearchFilter("горничные"),
|
||||
SearchFilter("грузовик-сан"),
|
||||
SearchFilter("гяру"),
|
||||
SearchFilter("демоны"),
|
||||
SearchFilter("драконы"),
|
||||
SearchFilter("дружба"),
|
||||
SearchFilter("ёнкома"),
|
||||
SearchFilter("жестокий мир"),
|
||||
SearchFilter("животные компаньоны"),
|
||||
SearchFilter("завоевание мира"),
|
||||
SearchFilter("зверолюди"),
|
||||
SearchFilter("злые духи"),
|
||||
SearchFilter("зомби"),
|
||||
SearchFilter("игровые элементы"),
|
||||
SearchFilter("империи"),
|
||||
SearchFilter("исекай"),
|
||||
SearchFilter("квесты"),
|
||||
SearchFilter("космос"),
|
||||
SearchFilter("кулинария"),
|
||||
SearchFilter("культивация"),
|
||||
SearchFilter("лгбт"),
|
||||
SearchFilter("легендарное оружие"),
|
||||
SearchFilter("лоли"),
|
||||
SearchFilter("магическая академия"),
|
||||
SearchFilter("магия"),
|
||||
SearchFilter("мафия"),
|
||||
SearchFilter("медицина"),
|
||||
SearchFilter("месть"),
|
||||
SearchFilter("монстродевушки"),
|
||||
SearchFilter("монстры"),
|
||||
SearchFilter("музыка"),
|
||||
SearchFilter("навыки / способности"),
|
||||
SearchFilter("наёмники"),
|
||||
SearchFilter("насилие / жестокость"),
|
||||
SearchFilter("нежить"),
|
||||
SearchFilter("ниндзя"),
|
||||
SearchFilter("обмен телами"),
|
||||
SearchFilter("оборотни"),
|
||||
SearchFilter("обратный гарем"),
|
||||
SearchFilter("огнестрельное оружие"),
|
||||
SearchFilter("офисные работники"),
|
||||
SearchFilter("пародия"),
|
||||
SearchFilter("пираты"),
|
||||
SearchFilter("подземелье"),
|
||||
SearchFilter("политика"),
|
||||
SearchFilter("полиция"),
|
||||
SearchFilter("преступники / криминал"),
|
||||
SearchFilter("призраки / духи"),
|
||||
SearchFilter("прокачка"),
|
||||
SearchFilter("психодел"),
|
||||
SearchFilter("путешествия во времени"),
|
||||
SearchFilter("рабы"),
|
||||
SearchFilter("разумные расы"),
|
||||
SearchFilter("ранги силы"),
|
||||
SearchFilter("реинкарнация"),
|
||||
SearchFilter("роботы"),
|
||||
SearchFilter("рыцари"),
|
||||
SearchFilter("самураи"),
|
||||
SearchFilter("система"),
|
||||
SearchFilter("скрытие личности"),
|
||||
SearchFilter("спасение мира"),
|
||||
SearchFilter("спортивное тело"),
|
||||
SearchFilter("средневековье"),
|
||||
SearchFilter("стимпанк"),
|
||||
SearchFilter("супергерои"),
|
||||
SearchFilter("традиционные игры"),
|
||||
SearchFilter("умный гг"),
|
||||
SearchFilter("управление территорией"),
|
||||
SearchFilter("учитель / ученик"),
|
||||
SearchFilter("философия"),
|
||||
SearchFilter("хикикомори"),
|
||||
SearchFilter("холодное оружие"),
|
||||
SearchFilter("шантаж"),
|
||||
SearchFilter("эльфы"),
|
||||
SearchFilter("якудза"),
|
||||
SearchFilter("япония"),
|
||||
)
|
||||
|
||||
private fun getAgeList() = listOf(
|
||||
CheckFilter("13+", "ADULT_13"),
|
||||
CheckFilter("16+", "ADULT_16"),
|
||||
CheckFilter("18+", "ADULT_18"),
|
||||
)
|
||||
|
||||
private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng")
|
||||
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||
val titleLanguagePref = ListPreference(screen.context).apply {
|
||||
key = LANGUAGE_PREF
|
||||
title = LANGUAGE_PREF_Title
|
||||
entries = arrayOf("Английский", "Русский")
|
||||
entryValues = arrayOf("eng", "rus")
|
||||
summary = "%s"
|
||||
setDefaultValue("eng")
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val titleLanguage = preferences.edit().putString(LANGUAGE_PREF, newValue as String).commit()
|
||||
val warning = "Если язык обложки не изменился очистите базу данных в приложении (Настройки -> Дополнительно -> Очистить базу данных)"
|
||||
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
|
||||
titleLanguage
|
||||
}
|
||||
}
|
||||
val paidChapterShow = androidx.preference.CheckBoxPreference(screen.context).apply {
|
||||
key = PAID_PREF
|
||||
title = PAID_PREF_Title
|
||||
summary = "Показывает не купленные\uD83D\uDCB2 главы(может вызвать ошибки при обновлении/автозагрузке)"
|
||||
setDefaultValue(false)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, checkValue).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(titleLanguagePref)
|
||||
screen.addPreference(paidChapterShow)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val API_URL = "https://api.newmanga.org/v2"
|
||||
private const val IMAGE_URL = "https://storage.newmanga.org"
|
||||
|
||||
private const val LANGUAGE_PREF = "NewMangaTitleLanguage"
|
||||
private const val LANGUAGE_PREF_Title = "Выбор языка на обложке"
|
||||
|
||||
private const val PAID_PREF = "PaidChapter"
|
||||
private const val PAID_PREF_Title = "Показывать платные главы"
|
||||
}
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.newbie.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
// Catalog API
|
||||
@Serializable
|
||||
data class PageWrapperDto<T>(
|
||||
val items: List<T>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LibraryDto(
|
||||
val id: Long,
|
||||
val title: TitleDto,
|
||||
val image: ImgDto,
|
||||
)
|
||||
|
||||
// Manga Details
|
||||
@Serializable
|
||||
data class MangaDetDto(
|
||||
val id: Long,
|
||||
val title: TitleDto,
|
||||
val author: AuthorDto?,
|
||||
val artist: AuthorDto?,
|
||||
val description: String,
|
||||
val image: ImgDto,
|
||||
val genres: List<TagsDto>,
|
||||
val type: String,
|
||||
val status: String,
|
||||
val rating: Float,
|
||||
val hearts: Long,
|
||||
val adult: String?,
|
||||
val branches: List<BranchesDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TitleDto(
|
||||
val en: String,
|
||||
val ru: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthorDto(
|
||||
val name: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ImgDto(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TagsDto(
|
||||
val title: TitleDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BranchesDto(
|
||||
val id: Long,
|
||||
val is_default: Boolean,
|
||||
)
|
||||
|
||||
// Chapters
|
||||
@Serializable
|
||||
data class SeriesWrapperDto<T>(
|
||||
val items: T,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BookDto(
|
||||
val id: Long,
|
||||
val tom: Int?,
|
||||
val name: String?,
|
||||
val number: Float,
|
||||
val created_at: String,
|
||||
val translator: String?,
|
||||
val is_available: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PageDto(
|
||||
val id: Int,
|
||||
val slices: Int?,
|
||||
)
|
||||
|
||||
// Search NEO in POST Request
|
||||
@Serializable
|
||||
data class SearchWrapperDto<T>(
|
||||
val result: T,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SubSearchDto<T>(
|
||||
val hits: List<T>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchLibraryDto(
|
||||
val document: DocElementsDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DocElementsDto(
|
||||
val id: String,
|
||||
val title_en: String,
|
||||
val title_ru: String,
|
||||
val image_large: String,
|
||||
val image_small: String,
|
||||
)
|
||||
2
src/ru/nudemoon/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/ru/nudemoon/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Nude-Moon'
|
||||
pkgNameSuffix = 'ru.nudemoon'
|
||||
extClass = '.Nudemoon'
|
||||
extVersionCode = 16
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/nudemoon/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/ru/nudemoon/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/ru/nudemoon/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/ru/nudemoon/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src/ru/nudemoon/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/ru/nudemoon/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,331 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.nudemoon
|
||||
|
||||
import android.webkit.CookieManager
|
||||
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.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.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.net.URLEncoder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Nudemoon : ParsedHttpSource() {
|
||||
|
||||
override val name = "Nude-Moon"
|
||||
|
||||
override val baseUrl = "https://x.nude-moon.fun"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val dateParseRu = SimpleDateFormat("d MMMM yyyy", Locale("ru"))
|
||||
private val dateParseSlash = SimpleDateFormat("d/MM/yyyy", Locale("ru"))
|
||||
|
||||
private val cookieManager by lazy { CookieManager.getInstance() }
|
||||
|
||||
init {
|
||||
cookieManager.setCookie(baseUrl, "nm_mobile=1; Domain=" + baseUrl.split("//")[1])
|
||||
}
|
||||
|
||||
private val cookiesHeader by lazy {
|
||||
val cookies = mutableMapOf<String, String>()
|
||||
cookies["NMfYa"] = "1"
|
||||
cookies["nm_mobile"] = "1"
|
||||
buildCookies(cookies)
|
||||
}
|
||||
|
||||
private fun buildCookies(cookies: Map<String, String>) =
|
||||
cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||
}
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val newReq = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.addHeader("Cookie", cookiesHeader)
|
||||
.addHeader("Referer", baseUrl)
|
||||
.build()
|
||||
|
||||
chain.proceed(newReq)
|
||||
}.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/all_manga?views&rowstart=${30 * (page - 1)}", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/all_manga?date&rowstart=${30 * (page - 1)}", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Search by query on this site works really badly, i don't even sure of the need to implement it
|
||||
val url = if (query.isNotEmpty()) {
|
||||
"$baseUrl/search?stext=${URLEncoder.encode(query, "CP1251")}&rowstart=${30 * (page - 1)}"
|
||||
} else {
|
||||
var genres = ""
|
||||
var order = ""
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
if (filter is GenreList) {
|
||||
filter.state.forEach { f ->
|
||||
if (f.state) {
|
||||
genres += f.id + '+'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (genres.isNotEmpty()) {
|
||||
for (filter in filters) {
|
||||
if (filter is OrderBy) {
|
||||
// The site has no ascending order
|
||||
order = arrayOf("&date", "&views", "&like")[filter.state!!.index]
|
||||
}
|
||||
}
|
||||
"$baseUrl/tags/${genres.dropLast(1)}$order&rowstart=${30 * (page - 1)}"
|
||||
} else {
|
||||
for (filter in filters) {
|
||||
if (filter is OrderBy) {
|
||||
// The site has no ascending order
|
||||
order = arrayOf(
|
||||
"all_manga?date",
|
||||
"all_manga?views",
|
||||
"all_manga?like",
|
||||
)[filter.state!!.index]
|
||||
}
|
||||
}
|
||||
"$baseUrl/$order&rowstart=${30 * (page - 1)}"
|
||||
}
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "table.news_pic2"
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
manga.thumbnail_url = element.select("img.news_pic2").attr("abs:src")
|
||||
element.select("a:has(h2)").let {
|
||||
manga.title = it.text().substringBefore(" / ").substringBefore(" №")
|
||||
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() = "a.small:contains(>)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
val infoElement = document.select("table.news_pic2").first()!!
|
||||
manga.title = document.select("h1").first()!!.text().substringBefore(" / ").substringBefore(" №")
|
||||
manga.author = infoElement.select("a[href*=mangaka]").text()
|
||||
manga.genre = infoElement.select("div.tag-links a").joinToString { it.text() }
|
||||
manga.description = document.select(".description").text()
|
||||
manga.thumbnail_url = document.selectFirst("meta[property=og:image]")!!.attr("abs:content")
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = popularMangaSelector()
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> = mutableListOf<SChapter>().apply {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val allPageElement = document.select("td.button a:contains(Все главы)")
|
||||
|
||||
if (allPageElement.isEmpty()) {
|
||||
add(
|
||||
SChapter.create().apply {
|
||||
val chapterName = document.select("table td.bg_style1 h1").text()
|
||||
val chapterUrl = response.request.url.toString()
|
||||
setUrlWithoutDomain(chapterUrl)
|
||||
name = "$chapterName Сингл"
|
||||
scanlator = document.select("table.news_pic2 a[href*=perevod]").text()
|
||||
date_upload = document.select("table.news_pic2 span.small2:contains(/)").text().let {
|
||||
try {
|
||||
dateParseSlash.parse(it)?.time ?: 0L
|
||||
} catch (e: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
chapter_number = 0F
|
||||
},
|
||||
)
|
||||
} else {
|
||||
var pageListDocument: Document
|
||||
val pageListLink = allPageElement.attr("href")
|
||||
client.newCall(
|
||||
GET(baseUrl + pageListLink, headers),
|
||||
).execute().run {
|
||||
if (!isSuccessful) {
|
||||
close()
|
||||
throw Exception("HTTP error $code")
|
||||
}
|
||||
pageListDocument = this.asJsoup()
|
||||
}
|
||||
pageListDocument.select(chapterListSelector())
|
||||
.forEach {
|
||||
add(chapterFromElement(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
val nameAndUrl = element.select("tr[valign=top] a:has(h2)")
|
||||
name = nameAndUrl.select("h2").text()
|
||||
setUrlWithoutDomain(nameAndUrl.attr("abs:href"))
|
||||
if (url.contains(baseUrl)) {
|
||||
url = url.replace(baseUrl, "")
|
||||
}
|
||||
val informBlock = element.select("tr[valign=top] td[align=left]")
|
||||
scanlator = informBlock.select("a[href*=perevod]").text()
|
||||
date_upload = informBlock.select("span.small2")
|
||||
.text().replace("Май", "Мая").let { textDate ->
|
||||
try {
|
||||
dateParseRu.parse(textDate)?.time ?: 0L
|
||||
} catch (e: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
chapter_number = name.substringAfter("№").substringBefore(" ").toFloatOrNull() ?: -1f
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> = mutableListOf<Page>().apply {
|
||||
response.asJsoup().select("div.gallery-item img.textbox").mapIndexed { index, img ->
|
||||
add(Page(index, imageUrl = img.attr("abs:data-src")))
|
||||
}
|
||||
if (size == 0 && cookieManager.getCookie(baseUrl).contains("fusion_user").not()) {
|
||||
throw Exception("Страницы не найдены. Возможно необходима авторизация в WebView")
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw Exception("Not Used")
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = throw Exception("Not Used")
|
||||
|
||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.CheckBox(name.replaceFirstChar { it.uppercaseChar() })
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
|
||||
private class OrderBy : Filter.Sort(
|
||||
"Сортировка",
|
||||
arrayOf("Дата", "Просмотры", "Лайки"),
|
||||
Selection(1, false),
|
||||
)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
OrderBy(),
|
||||
GenreList(getGenreList()),
|
||||
)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("анал"),
|
||||
Genre("без цензуры"),
|
||||
Genre("беременные"),
|
||||
Genre("близняшки"),
|
||||
Genre("большие груди"),
|
||||
Genre("в бассейне"),
|
||||
Genre("в больнице"),
|
||||
Genre("в ванной"),
|
||||
Genre("в общественном месте"),
|
||||
Genre("в первый раз"),
|
||||
Genre("в транспорте"),
|
||||
Genre("в туалете"),
|
||||
Genre("гарем"),
|
||||
Genre("гипноз"),
|
||||
Genre("горничные"),
|
||||
Genre("горячий источник"),
|
||||
Genre("групповой секс"),
|
||||
Genre("драма"),
|
||||
Genre("запредельное"),
|
||||
Genre("золотой дождь"),
|
||||
Genre("зрелые женщины"),
|
||||
Genre("идолы"),
|
||||
Genre("извращение"),
|
||||
Genre("измена"),
|
||||
Genre("имеют парня"),
|
||||
Genre("клизма"),
|
||||
Genre("колготки"),
|
||||
Genre("комиксы"),
|
||||
Genre("комиксы 3D"),
|
||||
Genre("косплей"),
|
||||
Genre("мастурбация"),
|
||||
Genre("мерзкий мужик"),
|
||||
Genre("много спермы"),
|
||||
Genre("молоко"),
|
||||
Genre("монстры"),
|
||||
Genre("на камеру"),
|
||||
Genre("на природе"),
|
||||
Genre("обычный секс"),
|
||||
Genre("огромный член"),
|
||||
Genre("пляж"),
|
||||
Genre("подглядывание"),
|
||||
Genre("принуждение"),
|
||||
Genre("продажность"),
|
||||
Genre("пьяные"),
|
||||
Genre("рабыни"),
|
||||
Genre("романтика"),
|
||||
Genre("с ушками"),
|
||||
Genre("секс игрушки"),
|
||||
Genre("спящие"),
|
||||
Genre("страпон"),
|
||||
Genre("студенты"),
|
||||
Genre("суккуб"),
|
||||
Genre("тентакли"),
|
||||
Genre("толстушки"),
|
||||
Genre("трапы"),
|
||||
Genre("ужасы"),
|
||||
Genre("униформа"),
|
||||
Genre("учитель и ученик"),
|
||||
Genre("фемдом"),
|
||||
Genre("фетиш"),
|
||||
Genre("фурри"),
|
||||
Genre("футанари"),
|
||||
Genre("футфетиш"),
|
||||
Genre("фэнтези"),
|
||||
Genre("цветная"),
|
||||
Genre("чикан"),
|
||||
Genre("чулки"),
|
||||
Genre("шимейл"),
|
||||
Genre("эксгибиционизм"),
|
||||
Genre("юмор"),
|
||||
Genre("юри"),
|
||||
Genre("ahegao"),
|
||||
Genre("BDSM"),
|
||||
Genre("ganguro"),
|
||||
Genre("gender bender"),
|
||||
Genre("megane"),
|
||||
Genre("mind break"),
|
||||
Genre("monstergirl"),
|
||||
Genre("netorare"),
|
||||
Genre("nipple penetration"),
|
||||
Genre("titsfuck"),
|
||||
Genre("x-ray"),
|
||||
)
|
||||
}
|
||||
27
src/ru/remanga/AndroidManifest.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ru.remanga.RemangaActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- RemangaActivity sites can be added here. -->
|
||||
<data
|
||||
android:host="remanga.org"
|
||||
android:pathPattern="/manga/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="xn--80aaig9ahr.xn--c1avg"
|
||||
android:pathPattern="/manga/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
16
src/ru/remanga/build.gradle
Normal file
@@ -0,0 +1,16 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Remanga'
|
||||
pkgNameSuffix = 'ru.remanga'
|
||||
extClass = '.Remanga'
|
||||
extVersionCode = 82
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':lib-dataimage')
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/remanga/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/ru/remanga/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/ru/remanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/ru/remanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/ru/remanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/ru/remanga/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
@@ -0,0 +1,40 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.remanga
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
/**
|
||||
* Springboard that accepts https://remanga.org/manga/xxx intents and redirects them to
|
||||
* the main tachiyomi process. The idea is to not install the intent filter unless
|
||||
* you have this extension installed, but still let the main tachiyomi app control
|
||||
* things.
|
||||
*/
|
||||
class RemangaActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
val titleid = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${Remanga.PREFIX_SLUG_SEARCH}$titleid")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("RemangaActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("RemangaActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package eu.kanade.tachiyomi.extension.ru.remanga.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TagsDto(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BranchesDto(
|
||||
val id: Long,
|
||||
val count_chapters: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ImgDto(
|
||||
val high: String? = null,
|
||||
val mid: String? = null,
|
||||
val low: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LibraryDto(
|
||||
val id: Long,
|
||||
val en_name: String,
|
||||
val rus_name: String,
|
||||
val dir: String,
|
||||
val img: ImgDto,
|
||||
val bookmark_type: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MyLibraryDto(
|
||||
val title: LibraryDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StatusDto(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaDetDto(
|
||||
val id: Long,
|
||||
val en_name: String,
|
||||
val rus_name: String,
|
||||
val another_name: String,
|
||||
val dir: String,
|
||||
val description: String?,
|
||||
val issue_year: Int?,
|
||||
val img: ImgDto,
|
||||
val type: TagsDto,
|
||||
val genres: List<TagsDto>,
|
||||
val categories: List<TagsDto>,
|
||||
val branches: List<BranchesDto>,
|
||||
val status: StatusDto,
|
||||
val avg_rating: String,
|
||||
val count_rating: Int,
|
||||
val age_limit: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PropsDto(
|
||||
val total_pages: Int? = 0,
|
||||
val page: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PageWrapperDto<T>(
|
||||
val content: List<T>,
|
||||
val props: PropsDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SeriesWrapperDto<T>(
|
||||
val content: T,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PublisherDto(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BookDto(
|
||||
val id: Long,
|
||||
val tome: Int,
|
||||
val chapter: String,
|
||||
val name: String,
|
||||
val upload_date: String,
|
||||
val is_paid: Boolean,
|
||||
val is_bought: Boolean?,
|
||||
val publishers: List<PublisherDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ExWrapperDto<T>(
|
||||
val data: T,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ExBookDto(
|
||||
val id: Long,
|
||||
val tome: Int,
|
||||
val chapter: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ExLibraryDto(
|
||||
val id: Long,
|
||||
val dir: String,
|
||||
val name: String = "Без названия",
|
||||
val img: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PagesDto(
|
||||
val id: Int,
|
||||
val height: Int,
|
||||
val link: String,
|
||||
val page: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PageDto(
|
||||
val pages: List<PagesDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChunksPageDto(
|
||||
val pages: List<List<PagesDto>>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserDto(
|
||||
val id: Long,
|
||||
)
|
||||
24
src/ru/unicomics/AndroidManifest.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ru.unicomics.UniComicsActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- UniComicsaActivity sites can be added here. -->
|
||||
<data
|
||||
android:host="unicomics.ru"
|
||||
android:pathPattern="/comics/series/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
12
src/ru/unicomics/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'UniComics'
|
||||
pkgNameSuffix = 'ru.unicomics'
|
||||
extClass = '.UniComics'
|
||||
extVersionCode = 6
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ru/unicomics/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/ru/unicomics/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/ru/unicomics/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/ru/unicomics/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |