Initial commit

This commit is contained in:
FourTOne5
2024-01-09 04:12:39 +06:00
commit 600c345dfe
8593 changed files with 150590 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -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),
)
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

12
src/ru/comx/build.gradle Normal file
View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View 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) }
}
}

View 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
View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

View 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"
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -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"),
)
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -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("Яой", "яой"),
)
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -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("&quot;", "\"")
.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)
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -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")
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -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()
}

View File

@@ -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,
)

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -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"),
)
}

View 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>

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}
}

View File

@@ -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,
)

View 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>

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Some files were not shown because too many files have changed in this diff Show More