diff --git a/src/ru/mangabook/AndroidManifest.xml b/src/ru/mangabook/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/ru/mangabook/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/ru/mangabook/build.gradle b/src/ru/mangabook/build.gradle new file mode 100644 index 000000000..a1470a3a9 --- /dev/null +++ b/src/ru/mangabook/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'MangaBook' + pkgNameSuffix = 'ru.mangabook' + extClass = '.MangaBook' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ru/mangabook/res/mipmap-hdpi/ic_launcher.png b/src/ru/mangabook/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..9e3bffe85 Binary files /dev/null and b/src/ru/mangabook/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ru/mangabook/res/mipmap-mdpi/ic_launcher.png b/src/ru/mangabook/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..5a2af815a Binary files /dev/null and b/src/ru/mangabook/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ru/mangabook/res/mipmap-xhdpi/ic_launcher.png b/src/ru/mangabook/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..fe0a8b015 Binary files /dev/null and b/src/ru/mangabook/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ru/mangabook/res/mipmap-xxhdpi/ic_launcher.png b/src/ru/mangabook/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..5bfb27579 Binary files /dev/null and b/src/ru/mangabook/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ru/mangabook/res/mipmap-xxxhdpi/ic_launcher.png b/src/ru/mangabook/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..e18139346 Binary files /dev/null and b/src/ru/mangabook/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ru/mangabook/res/web_hi_res_512.png b/src/ru/mangabook/res/web_hi_res_512.png new file mode 100644 index 000000000..5e175656b Binary files /dev/null and b/src/ru/mangabook/res/web_hi_res_512.png differ diff --git a/src/ru/mangabook/src/eu/kanade/tachiyomi/extension/ru/mangabook/MangaBook.kt b/src/ru/mangabook/src/eu/kanade/tachiyomi/extension/ru/mangabook/MangaBook.kt new file mode 100644 index 000000000..58bcd57f8 --- /dev/null +++ b/src/ru/mangabook/src/eu/kanade/tachiyomi/extension/ru/mangabook/MangaBook.kt @@ -0,0 +1,264 @@ +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=rate", headers) + override fun popularMangaNextPageSelector() = "a.page-link[rel=next]" + override fun popularMangaSelector() = "article.short .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(" / ").last() + } + 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" + } else { + val url = "$baseUrl/filterList?page=$page&ftype[]=0&status[]=0&sortBy=rate".toHttpUrlOrNull()!!.newBuilder() + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is OrderBy -> { + val ord = arrayOf("rate", "name", "views", "created_at")[filter.state] + url.addQueryParameter("sortBy", "$ord") + } + is CategoryList -> filter.state.forEach { category -> + if (category.state) { + url.addQueryParameter("cat", category.id) + } + } + 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) + } + } + } + } + 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").text().split(" / ").last() + } + 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() + manga.title = document.select(".fheader h1").text().split(" / ").last() + 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() } + manga.description = 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 = 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 { + return document.select(".reader-images img.img-responsive").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) : Filter.Group("Тип", formas) + private class StatusList(statuses: List) : Filter.Group("Статус", statuses) + private class CategoryList(categories: List) : Filter.Group("Категории", categories) + override fun getFilterList() = FilterList( + OrderBy(), + CategoryList(getCategoryList()), + StatusList(getStatusList()), + FormatList(getFormatList()) + ) + + private class OrderBy : Filter.Select( + "Сортировка", + arrayOf("По рейтингу", "По алфавиту", "По популярности", "По дате выхода") + ) + private fun getFormatList() = listOf( + CheckFilter("Манга", "1"), + CheckFilter("Манхва", "2"), + CheckFilter("Веб Манхва", "4"), + CheckFilter("Маньхуа", "3") + ) + + private fun getStatusList() = listOf( + CheckFilter("Сейчас издаётся", "1"), + CheckFilter("Анонсировано", "3"), + CheckFilter("Изданное", "2") + ) + + private fun getCategoryList() = listOf( + CheckFilter("16+", "16+"), + CheckFilter("Арт", "art"), + CheckFilter("Бара", "bara"), + CheckFilter("Боевик", "action"), + CheckFilter("Боевые искусства", "combatskill"), + CheckFilter("В цвете", "vcvete"), + CheckFilter("Вампиры", "vampaires"), + CheckFilter("Веб", "web"), + CheckFilter("Вестерн", "western"), + CheckFilter("Гарем", "harem"), + CheckFilter("Гендерная интрига", "genderintrigue"), + CheckFilter("Героическое фэнтези", "heroic_fantasy"), + CheckFilter("Детектив", "detective"), + CheckFilter("Дзёсэй", "josei"), + CheckFilter("Додзинси", "doujinshi"), + CheckFilter("Драма", "drama"), + CheckFilter("Ёнкома", "yonkoma"), + CheckFilter("Есси", "18+"), + CheckFilter("Зомби", "zombie"), + CheckFilter("Игра", "games"), + CheckFilter("Инцест", "incest"), + CheckFilter("Исекай", "isekai"), + CheckFilter("Искусство", "iskusstvo"), + CheckFilter("Исторический", "historical"), + CheckFilter("Киберпанк", "cyberpunk"), + CheckFilter("Кодомо", "kodomo"), + CheckFilter("Комедия", "comedy"), + CheckFilter("Культовое", "iconic"), + CheckFilter("литРПГ", "litrpg"), + CheckFilter("Любовь", "love"), + CheckFilter("Махо-сёдзё", "maho-shojo"), + CheckFilter("Меха", "robots"), + CheckFilter("Мистика", "mystery"), + CheckFilter("Мужская беременность", "male-pregnancy"), + CheckFilter("Музыка", "music"), + CheckFilter("Научная фантастика", "sciencefiction"), + CheckFilter("Новинки", "new"), + CheckFilter("Омегаверс", "omegavers"), + CheckFilter("Перерождение", "newlife"), + CheckFilter("Повседневность", "humdrum"), + CheckFilter("Постапокалиптика", "postapocalyptic"), + CheckFilter("Приключения", "adventure"), + CheckFilter("Психология", "psychology"), + CheckFilter("Романтика", "romance"), + CheckFilter("Самураи", "samurai"), + CheckFilter("Сборник", "compilation"), + CheckFilter("Сверхъестественное", "supernatural"), + CheckFilter("Сёдзё", "shojo"), + CheckFilter("Сёдзё-ай", "maho-shojo"), + CheckFilter("Сёнэн", "senen"), + CheckFilter("Сёнэн-ай", "shonen-ai"), + CheckFilter("Сетакон", "setakon"), + CheckFilter("Сингл", "singl"), + CheckFilter("Сказка", "fable"), + CheckFilter("Сорс", "bdsm"), + CheckFilter("Спорт", "sport"), + CheckFilter("Супергерои", "superheroes"), + CheckFilter("Сэйнэн", "seinen"), + CheckFilter("Танцы", "dancing"), + CheckFilter("Трагедия", "tragedy"), + CheckFilter("Триллер", "thriller"), + CheckFilter("Ужасы", "horror"), + CheckFilter("Фантастика", "fantastic"), + CheckFilter("Фурри", "furri"), + CheckFilter("Фэнтези", "fantasy"), + CheckFilter("Школа", "school"), + CheckFilter("Эротика", "erotica"), + CheckFilter("Этти", "etty"), + CheckFilter("Юмор", "humor"), + CheckFilter("Юри", "yuri"), + CheckFilter("Яой", "yaoi") + ) +}