diff --git a/src/ru/henchan/build.gradle b/src/ru/henchan/build.gradle index e68c9f10e..8d9e5bd44 100644 --- a/src/ru/henchan/build.gradle +++ b/src/ru/henchan/build.gradle @@ -5,7 +5,7 @@ ext { appName = 'Tachiyomi: Henchan' pkgNameSuffix = 'ru.henchan' extClass = '.Henchan' - extVersionCode = 6 + extVersionCode = 7 libVersion = '1.2' } diff --git a/src/ru/henchan/src/eu/kanade/tachiyomi/extension/ru/henchan/Henchan.kt b/src/ru/henchan/src/eu/kanade/tachiyomi/extension/ru/henchan/Henchan.kt index ccfd1cb0e..fdd91fb14 100644 --- a/src/ru/henchan/src/eu/kanade/tachiyomi/extension/ru/henchan/Henchan.kt +++ b/src/ru/henchan/src/eu/kanade/tachiyomi/extension/ru/henchan/Henchan.kt @@ -8,6 +8,9 @@ import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.* + class Henchan : ParsedHttpSource() { @@ -28,7 +31,56 @@ class Henchan : ParsedHttpSource() { GET("$baseUrl/manga/new?offset=${20 * (page - 1)}", headers) override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val url = "$baseUrl/?do=search&subaction=search&story=$query" + var pageNum = 1 + when { + page < 1 -> pageNum = 1 + page >= 1 -> pageNum = page + } + val url = if (query.isNotEmpty()) { + "$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum" + } else { + var genres = "" + var order = "" + for (filter in if (filters.isEmpty()) getFilterList() else filters) { + when(filter){ + is GenreList -> { + filter.state.forEach { f -> + if (!f.isIgnored()) { + genres += (if (f.isExcluded()) "-" else "") + f.id + '+' + } + } + } + } + } + + if (genres.isNotEmpty()) { + for (filter in filters) { + when (filter) { + is OrderBy -> { + order = if (filter.state!!.ascending) { + arrayOf("&n=dateasc", "&n=favasc", "&n=abcdesc")[filter.state!!.index] + } else { + arrayOf("", "&n=favdesc", "&n=abcasc")[filter.state!!.index] + } + } + } + } + "$baseUrl/tags/${genres.dropLast(1)}&sort=manga$order?offset=${20 * (pageNum - 1)}" + }else{ + for (filter in filters) { + when (filter) { + is OrderBy -> { + order = if (filter.state!!.ascending) { + arrayOf("manga/new&n=dateasc", "manga/new&n=favasc", "manga/new&n=abcdesc")[filter.state!!.index] + } else { + arrayOf("manga/new", "mostfavorites&sort=manga", "manga/new&n=abcasc")[filter.state!!.index] + } + } + } + } + "$baseUrl/$order?offset=${20 * (pageNum - 1)}" + } + } return GET(url, headers) } @@ -36,36 +88,21 @@ class Henchan : ParsedHttpSource() { override fun latestUpdatesSelector() = popularMangaSelector() - override fun searchMangaSelector() = popularMangaSelector() + override fun searchMangaSelector() = ".content_row:not(:has(div.item:containsOwn(Тип)))" - override fun searchMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - - val mangas = mutableListOf() - document.select(searchMangaSelector()).forEach { element -> - val manga = searchMangaFromElement(element) - if (manga.url != "") { - mangas.add(manga) - } - } - - return MangasPage(mangas, false) - } - - private fun String.getHQThumbnail(): String = this.replace("manganew_thumbs", "showfull_retina/manga").replace("img.", "imgcover.") + private fun String.getHQThumbnail(): String = this + .replace("manganew_thumbs", "showfull_retina/manga") + .replace("img.", "imgcover.") + .replace("_henchan.me", "_hentaichan.ru") override fun popularMangaFromElement(element: Element): SManga { val manga = SManga.create() manga.thumbnail_url = element.select("img").first().attr("src").getHQThumbnail() - element.select("h2 > a").first().let { - val url = it.attr("href") - if (url.contains("/manga/")) { - manga.setUrlWithoutDomain(url) - manga.title = it.text() - } else { - manga.url = "" - } - } + + val urlElem = element.select("h2 > a").first() + manga.setUrlWithoutDomain(urlElem.attr("href")) + manga.title = urlElem.text() + return manga } @@ -79,8 +116,7 @@ class Henchan : ParsedHttpSource() { override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() - override fun searchMangaNextPageSelector() = throw Exception("Not Used") - + override fun searchMangaNextPageSelector() = "#nextlink, ${popularMangaNextPageSelector()}" override fun mangaDetailsParse(document: Document): SManga { @@ -110,8 +146,12 @@ class Henchan : ParsedHttpSource() { val chap = SChapter.create() chap.setUrlWithoutDomain(responseUrl.removePrefix(baseUrl)) chap.name = document.select("a.title_top_a").text() - chap.chapter_number = 0.0F - chap.date_upload = 0L + chap.chapter_number = 1F + + val date = document.select("div.row4_right b")?.text()?.let { + SimpleDateFormat("dd MMMM yyyy", Locale("ru")).parse(it).time + } ?: 0 + chap.date_upload = date return listOf(chap) } @@ -119,15 +159,15 @@ class Henchan : ParsedHttpSource() { val chap = SChapter.create() chap.setUrlWithoutDomain(document.select("#left > div > a").attr("href")) chap.name = document.select("#right > div:nth-child(4)").text().split(" похожий на ")[1] - chap.chapter_number = 0.0F + chap.chapter_number = 1F chap.date_upload = 0L return listOf(chap) } val result = mutableListOf() - result.addAll(document.select(chapterListSelector()).mapIndexed { index, element -> - chapterFromElement(index, element) + result.addAll(document.select(chapterListSelector()).map { + chapterFromElement(it) }) var url = document.select("div#pagination_related a:contains(Вперед)").attr("href") @@ -137,9 +177,8 @@ class Henchan : ParsedHttpSource() { headers = headers ) val nextPage = client.newCall(get).execute().asJsoup() - result.addAll(nextPage.select(chapterListSelector()).mapIndexed { - index, element -> - chapterFromElement(index, element) + result.addAll(nextPage.select(chapterListSelector()).map { + chapterFromElement(it) }) url = nextPage.select("div#pagination_related a:contains(Вперед)").attr("href") @@ -148,17 +187,16 @@ class Henchan : ParsedHttpSource() { return result.reversed() } - private fun chapterFromElement(index: Int, element: Element): SChapter { + override fun chapterFromElement(element: Element): SChapter { val chapter = SChapter.create() chapter.setUrlWithoutDomain(element.select("h2 a").attr("href")) - chapter.name = element.select("h2 a").attr("title") - chapter.chapter_number = index.toFloat() + val chapterName = element.select("h2 a").attr("title") + chapter.name = chapterName + chapter.chapter_number = "(глава\\s|часть\\s)(\\d+)".toRegex(RegexOption.IGNORE_CASE).find(chapterName)?.groupValues?.get(2)?.toFloat() ?: 0F chapter.date_upload = 0L return chapter } - override fun chapterFromElement(element: Element): SChapter = throw Exception("Not Used") - override fun pageListRequest(chapter: SChapter): Request { return GET(exhentaiBaseUrl + chapter.url.replace("/manga/", "/online/") + "?development_access=true", headers) } @@ -169,7 +207,7 @@ class Henchan : ParsedHttpSource() { val resPages = mutableListOf() val imgs = imgString.split(",") imgs.forEachIndexed { index, s -> - resPages.add(Page(index, imageUrl = s.trim('"', '\'', '[', ' '))) + resPages.add(Page(index, imageUrl = s.trim('"', '\'', ' '))) } return resPages } @@ -177,4 +215,168 @@ class Henchan : ParsedHttpSource() { override fun imageUrlParse(document: Document) = throw Exception("Not Used") override fun pageListParse(document: Document): List = throw Exception("Not Used") + + private class Genre(val id: String, name: String = id.replace('_', ' ').capitalize()) : Filter.TriState(name) + private class GenreList(genres: List) : Filter.Group("Тэги", genres) + private class OrderBy : Filter.Sort("Сортировка", + arrayOf("Дата", "Популярность", "Алфавит"), + Filter.Sort.Selection(1, false)) + + override fun getFilterList() = FilterList( + OrderBy(), + GenreList(getGenreList()) + ) + + private fun getGenreList() = listOf( + Genre("3D"), + Genre("action"), + Genre("ahegao"), + Genre("bdsm"), + Genre("foot_fetish"), + Genre("footfuck"), + Genre("gender_bender"), + Genre("live"), + Genre("lolcon"), + Genre("megane"), + Genre("mind_break"), + Genre("monstergirl"), + Genre("netorare"), + Genre("netori"), + Genre("nipple_penetration"), + Genre("paizuri_(titsfuck)"), + Genre("rpg"), + Genre("scat"), + Genre("shemale"), + Genre("shooter"), + Genre("simulation"), + Genre("tomboy"), + 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("измена"), + 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("сетакон"), + 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("яой") + ) } diff --git a/src/ru/nudemoon/build.gradle b/src/ru/nudemoon/build.gradle new file mode 100644 index 000000000..25c871cbb --- /dev/null +++ b/src/ru/nudemoon/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Nude-Moon' + pkgNameSuffix = 'ru.nudemoon' + extClass = '.Nudemoon' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ru/nudemoon/res/mipmap-hdpi/ic_launcher.png b/src/ru/nudemoon/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..75abbb1cb Binary files /dev/null and b/src/ru/nudemoon/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ru/nudemoon/res/mipmap-mdpi/ic_launcher.png b/src/ru/nudemoon/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..f26c1dcb6 Binary files /dev/null and b/src/ru/nudemoon/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ru/nudemoon/res/mipmap-xhdpi/ic_launcher.png b/src/ru/nudemoon/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..a5ee737e8 Binary files /dev/null and b/src/ru/nudemoon/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ru/nudemoon/res/mipmap-xxhdpi/ic_launcher.png b/src/ru/nudemoon/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..de6a2f282 Binary files /dev/null and b/src/ru/nudemoon/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ru/nudemoon/res/mipmap-xxxhdpi/ic_launcher.png b/src/ru/nudemoon/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..850e07e4f Binary files /dev/null and b/src/ru/nudemoon/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ru/nudemoon/res/web_hi_res_512.png b/src/ru/nudemoon/res/web_hi_res_512.png new file mode 100644 index 000000000..d5756edd1 Binary files /dev/null and b/src/ru/nudemoon/res/web_hi_res_512.png differ diff --git a/src/ru/nudemoon/src/eu/kanade/tachiyomi/extension/ru/nudemoon/Nudemoon.kt b/src/ru/nudemoon/src/eu/kanade/tachiyomi/extension/ru/nudemoon/Nudemoon.kt new file mode 100644 index 000000000..225655381 --- /dev/null +++ b/src/ru/nudemoon/src/eu/kanade/tachiyomi/extension/ru/nudemoon/Nudemoon.kt @@ -0,0 +1,282 @@ +package eu.kanade.tachiyomi.extension.ru.nudemoon + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +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 org.jsoup.nodes.TextNode +import java.net.URLEncoder +import java.text.SimpleDateFormat +import java.util.* + + +class Nudemoon : ParsedHttpSource() { + + override val name = "Nude-Moon" + + override val baseUrl = "http://nude-moon.me" + + override val lang = "ru" + + override val supportsLatest = true + + 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) { + when(filter){ + is GenreList -> { + filter.state.forEach { f -> + if (f.state) { + genres += f.id + '+' + } + } + } + } + } + + if (genres.isNotEmpty()) { + for (filter in filters) { + when (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) { + when (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[cellspacing=\"2\"].news_pic2" + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun searchMangaSelector() = popularMangaSelector() + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + + val thumbnailElem = element.select("img.news_pic2").first() + val parentElem = thumbnailElem.parent() + + manga.thumbnail_url = baseUrl + thumbnailElem.attr("src") + manga.title = parentElem.attr("title") + manga.setUrlWithoutDomain(parentElem.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() + manga.author = document.select("a[title^=Показать всю мангу от]").first().text() + manga.genre = document.select("div.tbl2[align] > a").joinToString { it.text() } + manga.description = document.select(".description").text() + return manga + } + + override fun chapterListRequest(manga: SManga): Request { + + val chapterUrl = if(manga.title.contains("\\s#\\d+".toRegex())) + "/vse_glavy/" + manga.title.split("\\s#\\d+".toRegex())[0].replace("\\W".toRegex(), "_") + else + manga.url + + return GET(baseUrl + chapterUrl, headers) + } + + override fun chapterListSelector() = popularMangaSelector() + + override fun chapterListParse(response: Response): List { + val responseUrl = response.request().url().toString() + val document = response.asJsoup() + + if(!responseUrl.contains("/vse_glavy/")){ + return listOf(chapterFromElement(document)) + } + + //Order chapters by its number 'cause on the site they are in random order + return document.select(chapterListSelector()).sortedByDescending { + val regex = "#(\\d+)".toRegex() + val chapterName = it.select("img.news_pic2").first().parent().attr("title") + regex.find(chapterName)?.groupValues?.get(1)?.toInt() ?: 0 + }.map { chapterFromElement(it) } + } + + override fun chapterFromElement(element: Element): SChapter { + val chapter = SChapter.create() + + val infoElem = element.select("img.news_pic2").first().parent() + val chapterName = infoElem.attr("title") + var chapterUrl = infoElem.attr("href") + if(!chapterUrl.contains("-online")) { + chapterUrl = chapterUrl.replace("/\\d+".toRegex(), "$0-online") + } else { + chapter.chapter_number = 1F + } + + chapter.setUrlWithoutDomain(if(!chapterUrl.startsWith("/")) "/$chapterUrl" else chapterUrl) + chapter.name = chapterName + chapter.date_upload = (element.select("font:containsOwn(Дата:)")?.first()?.nextSibling() as? TextNode)?.text()?.let { + SimpleDateFormat("dd MMMM yyyy", Locale("ru")).parse(it).time + } ?: 0 + + return chapter + } + + override fun pageListRequest(chapter: SChapter): Request { + return GET(baseUrl + chapter.url, headers) + } + + override fun pageListParse(response: Response): List { + val document = response.asJsoup() + val resPages = mutableListOf() + val imgScript = document.select("script:containsData(var images)").first().html() + + Regex("images\\[(\\d+)].src\\s=\\s'.(.*)'").findAll(imgScript).forEach { + resPages.add(Page(it.groupValues[1].toInt(), imageUrl = baseUrl + it.groupValues[2])) + } + + return resPages + } + + override fun imageUrlParse(document: Document) = throw Exception("Not Used") + + override fun pageListParse(document: Document): List = throw Exception("Not Used") + + private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.CheckBox(name.capitalize()) + private class GenreList(genres: List) : Filter.Group("Тэги", genres) + private class OrderBy : Filter.Sort("Сортировка", + arrayOf("Дата", "Просмотры", "Лайки"), + Filter.Sort.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") + ) +}