diff --git a/src/en/mangahen/build.gradle b/src/en/mangahen/build.gradle new file mode 100644 index 000000000..3246b2c00 --- /dev/null +++ b/src/en/mangahen/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'MangaHen' + extClass = '.MangaHen' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/mangahen/res/mipmap-hdpi/ic_launcher.png b/src/en/mangahen/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..17f27fa2b Binary files /dev/null and b/src/en/mangahen/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/mangahen/res/mipmap-mdpi/ic_launcher.png b/src/en/mangahen/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..0b1a61d1e Binary files /dev/null and b/src/en/mangahen/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/mangahen/res/mipmap-xhdpi/ic_launcher.png b/src/en/mangahen/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..a15def8d7 Binary files /dev/null and b/src/en/mangahen/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/mangahen/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mangahen/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..8e4b9d5b5 Binary files /dev/null and b/src/en/mangahen/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/mangahen/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/mangahen/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1becc0835 Binary files /dev/null and b/src/en/mangahen/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/mangahen/src/eu/kanade/tachiyomi/extension/en/mangahen/MangaHen.kt b/src/en/mangahen/src/eu/kanade/tachiyomi/extension/en/mangahen/MangaHen.kt new file mode 100644 index 000000000..5307e1e35 --- /dev/null +++ b/src/en/mangahen/src/eu/kanade/tachiyomi/extension/en/mangahen/MangaHen.kt @@ -0,0 +1,174 @@ +package eu.kanade.tachiyomi.extension.en.mangahen + +import eu.kanade.tachiyomi.network.GET +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 eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Element +import rx.Observable + +class MangaHen : HttpSource() { + + override val name = "MangaHen" + + override val baseUrl = "https://manga-hen.com" + + private val advSearchURL = "$baseUrl/advanced-search" + + override val lang = "en" + + override val supportsLatest = true + + override val client = network.cloudflareClient + + private var tagsList: List = listOf() + + // Popular + override fun popularMangaRequest(page: Int): Request { + return GET("$advSearchURL/?search=1&type=0&sort=1&page=$page", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val doc = response.asJsoup() + + val mangas = doc.select("a[href^=/manga/]").map(::popularMangaFromElement) + + val hasNextPage = doc.select("a[href*=page]").any { it.text().isBlank() } + + return MangasPage(mangas, hasNextPage) + } + + private fun popularMangaFromElement(element: Element): SManga { + return SManga.create().apply { + title = element.selectFirst("h2")!!.ownText() + setUrlWithoutDomain(element.absUrl("href")) + thumbnail_url = element.selectFirst("img")!!.absUrl("src") + } + } + + // Latest + override fun latestUpdatesRequest(page: Int): Request { + return GET("$advSearchURL/?search=1&type=0&sort=2&page=$page", headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response) + + // Search + + private fun tagSearch(tag: String, tagsList: List): String? { + val index = (tagsList.indexOf(tag) + 1).toString() + return if (index != "-1") index else null + } + + private fun tagsList(): List { + if (tagsList.isEmpty()) { + val request = GET(advSearchURL, headers) + + val response = client.newCall(request).execute() + + tagsList = response.asJsoup().select("li[onclick=updateTag(this)]").map { it.ownText().lowercase() } + } + return tagsList + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val includeTags = mutableListOf() + val excludeTags = mutableListOf() + + val tagsList = tagsList() + val url = advSearchURL.toHttpUrl().newBuilder().apply { + filters.forEach { + when (it) { + is SortFilter -> addQueryParameter("sort", it.getValue()) + + is TypeFilter -> addQueryParameter("type", it.getValue()) + + is TextFilter -> { + if (it.state.isNotEmpty()) { + it.state.split(",").filter(String::isNotBlank).map { tag -> + val trimmed = tag.trim().lowercase() + if (trimmed.startsWith('-')) { + tagSearch(trimmed.removePrefix("-"), tagsList)?.let { tagInfo -> + excludeTags.add(tagInfo) + } + } else { + tagSearch(trimmed, tagsList)?.let { tagInfo -> + includeTags.add(tagInfo) + } + } + } + } + } + else -> {} + } + } + + addQueryParameter("name", query) + + addQueryParameter("search", "1") + if (includeTags.isNotEmpty()) addQueryParameter("include_tags", includeTags.joinToString()) + if (excludeTags.isNotEmpty()) addQueryParameter("exclude_tags", excludeTags.joinToString()) + if (page > 1) addQueryParameter("page", page.toString()) + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) + + // Details + + override fun mangaDetailsParse(response: Response): SManga { + val document = response.asJsoup() + return SManga.create().apply { + val authors = document.select("a[href*=/circles/]").eachText().joinToString() + val artists = document.select("a[href*=/authors/]").eachText().joinToString() + initialized = true + title = document.select("h1.font-semibold").text() + author = authors.ifEmpty { artists } + artist = artists + genre = document.select("a[href*=/tags/]").eachText().joinToString() + description = buildString { + append("Categories: ", document.select("a[href*=/categories/]").text(), "\n") + append("Parodies: ", document.select("a[href*=/parodies/]").text(), "\n") + append("Circles: ", document.select("a[href*=/circles/]").text(), "\n\n") + append(document.select("tr:contains(page)").text(), "\n") + append(document.select("tr:contains(view)").text(), "\n") + } + thumbnail_url = document.selectFirst("img[src*=thumbnail].w-96")?.absUrl("src") + } + } + + // Chapters + + override fun fetchChapterList(manga: SManga): Observable> { + return Observable.just( + listOf( + SChapter.create().apply { + name = "Chapter" + setUrlWithoutDomain(manga.url) + }, + ), + ) + } + + // Pages + + override fun pageListParse(response: Response): List { + val images = response.asJsoup().select("img[src*=images]:not(img[src*=thumbnail]).w-full") + return images.mapIndexed { index, image -> + Page(index, imageUrl = image.absUrl("src").replace(Regex("-t(?=\\.)"), "")) + } + } + + override fun getFilterList() = getFilters() + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + override fun chapterListParse(response: Response): List = throw UnsupportedOperationException() +} diff --git a/src/en/mangahen/src/eu/kanade/tachiyomi/extension/en/mangahen/MangaHenFilters.kt b/src/en/mangahen/src/eu/kanade/tachiyomi/extension/en/mangahen/MangaHenFilters.kt new file mode 100644 index 000000000..62418adc9 --- /dev/null +++ b/src/en/mangahen/src/eu/kanade/tachiyomi/extension/en/mangahen/MangaHenFilters.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.extension.en.mangahen + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList + +fun getFilters(): FilterList { + return FilterList( + SortFilter("Sort by", getSortsList), + TypeFilter("Types", getTypesList), + Filter.Separator(), + Filter.Header("Separate tags with commas (,)"), + Filter.Header("Prepend with dash (-) to exclude"), + TextFilter("Tags"), + ) +} + +internal open class TextFilter(name: String) : Filter.Text(name) + +internal open class SortFilter(name: String, vals: List>) : SelectFilter(name, vals) + +internal open class TypeFilter(name: String, vals: List>) : SelectFilter(name, vals) + +internal open class SelectFilter(name: String, private val vals: List>, state: Int = 0) : + Filter.Select(name, vals.map { it.first }.toTypedArray(), state) { + fun getValue() = vals[state].second +} + +private val getTypesList: List> = listOf( + Pair("All", "0"), + Pair("Manga", "1"), + Pair("Doujinshi", "2"), +) + +private val getSortsList: List> = listOf( + Pair("Newest", "2"), + Pair("Popular", "1"), + Pair("Relevance", "0"), + Pair("Best Rated", "3"), + Pair("Most Viewed", "4"), +)