diff --git a/multisrc/overrides/manga18/default/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/manga18/default/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..3b50748b7 Binary files /dev/null and b/multisrc/overrides/manga18/default/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/default/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/manga18/default/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..59144677c Binary files /dev/null and b/multisrc/overrides/manga18/default/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/default/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/manga18/default/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..8fc523d67 Binary files /dev/null and b/multisrc/overrides/manga18/default/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/default/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/manga18/default/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d51a0ef31 Binary files /dev/null and b/multisrc/overrides/manga18/default/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/default/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/manga18/default/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b64b91cf9 Binary files /dev/null and b/multisrc/overrides/manga18/default/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/hanman18/src/HANMAN18.kt b/multisrc/overrides/manga18/hanman18/src/HANMAN18.kt new file mode 100644 index 000000000..f1487737d --- /dev/null +++ b/multisrc/overrides/manga18/hanman18/src/HANMAN18.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.extension.zh.hanman18 + +import eu.kanade.tachiyomi.multisrc.manga18.Manga18 + +class HANMAN18 : Manga18("HANMAN18", "https://hanman18.com", "zh") { + // tag filter doesn't work on site + override val getAvailableTags = false +} diff --git a/multisrc/overrides/manga18/hentai3zcc/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..7825c08b7 Binary files /dev/null and b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/hentai3zcc/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..ddcbe1ecc Binary files /dev/null and b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..2635b6e49 Binary files /dev/null and b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..3edefcaf4 Binary files /dev/null and b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..cf5633a64 Binary files /dev/null and b/multisrc/overrides/manga18/hentai3zcc/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/manga18/hentai3zcc/src/Hentai3zCC.kt b/multisrc/overrides/manga18/hentai3zcc/src/Hentai3zCC.kt new file mode 100644 index 000000000..eb213bf49 --- /dev/null +++ b/multisrc/overrides/manga18/hentai3zcc/src/Hentai3zCC.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.extension.en.hentai3zcc + +import eu.kanade.tachiyomi.multisrc.manga18.Manga18 +import eu.kanade.tachiyomi.source.model.SManga +import org.jsoup.nodes.Element + +class Hentai3zCC : Manga18("Hentai3z.CC", "https://hentai3z.cc", "en") { + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) + title = element.selectFirst("div.mg_info > div.mg_name a")!!.text() + thumbnail_url = element.selectFirst("img")?.absUrl("src") + ?.replace("cover_thumb_2.webp", "cover_250x350.jpg") + ?.replace("admin.manga18.us", "bk.18porncomic.com") + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18.kt new file mode 100644 index 000000000..a290fa7aa --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18.kt @@ -0,0 +1,214 @@ +package eu.kanade.tachiyomi.multisrc.manga18 + +import android.util.Base64 +import android.util.Log +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.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.lang.Exception +import java.lang.UnsupportedOperationException +import java.text.SimpleDateFormat +import java.util.Locale + +abstract class Manga18( + override val name: String, + override val baseUrl: String, + override val lang: String, +) : ParsedHttpSource() { + + override val supportsLatest = true + + override val client = network.cloudflareClient + + override fun headersBuilder() = super.headersBuilder() + .set("Referer", "$baseUrl/") + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/list-manga/$page?order_by=views", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + + getTags(document) + + val entries = document.select(popularMangaSelector()).map(::popularMangaFromElement) + val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null + + return MangasPage(entries, hasNextPage) + } + + override fun popularMangaSelector() = "div.story_item" + override fun popularMangaNextPageSelector() = ".pagination a[rel=next]" + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) + title = element.selectFirst("div.mg_info > div.mg_name a")!!.text() + thumbnail_url = element.selectFirst("img")?.absUrl("src") + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/list-manga/$page", headers) + } + + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + override fun latestUpdatesSelector() = popularMangaSelector() + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + val tag = filters.filterIsInstance().firstOrNull() + if (query.isNotEmpty() || tag?.selected.isNullOrEmpty()) { + addPathSegment("list-manga") + addPathSegment(page.toString()) + addQueryParameter("search", query.trim()) + } else { + addPathSegment("manga-list") + addPathSegment(tag!!.selected!!) + addPathSegment(page.toString()) + filters.filterIsInstance().firstOrNull()?.selected?.let { + addQueryParameter("order_by", it) + } + } + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + override fun searchMangaSelector() = popularMangaSelector() + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + protected open val getAvailableTags = true + protected open val tagsSelector = "div.grid_cate li > a" + + private var tags = listOf>() + private var getTagsAttempts = 0 + + protected open fun getTags(document: Document) { + if (getAvailableTags && tags.isEmpty() && getTagsAttempts < 3) { + try { + tags = document.select(tagsSelector).map { + Pair( + it.text().trim(), + it.attr("href") + .removeSuffix("/") + .substringAfterLast("/"), + ) + }.let { + listOf(Pair("", "")) + it + } + } catch (e: Exception) { + Log.d(name, e.stackTraceToString()) + } + } + } + + override fun getFilterList(): FilterList { + if (!getAvailableTags) return FilterList() + + return if (tags.isEmpty()) { + FilterList( + Filter.Header("Press 'reset' to attempt to load genres"), + ) + } else { + FilterList( + Filter.Header("Ignored with text search"), + Filter.Separator(), + SortFilter(), + TagFilter(tags), + ) + } + } + + protected open val infoElementSelector = "div.detail_listInfo" + protected open val titleSelector = "div.detail_name > h1" + protected open val descriptionSelector = "div.detail_reviewContent" + protected open val statusSelector = "div.item:contains(Status) div.info_value" + protected open val altNameSelector = "div.item:contains(Other name) div.info_value" + protected open val genreSelector = "div.info_value > a[href*=/manga-list/]" + protected open val authorSelector = "div.info_label:contains(author) + div.info_value, div.info_label:contains(autor) + div.info_value" + protected open val artistSelector = "div.info_label:contains(artist) + div.info_value" + protected open val thumbnailSelector = "div.detail_avatar > img" + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val info = document.selectFirst(infoElementSelector)!! + + title = document.select(titleSelector).text() + description = buildString { + document.select(descriptionSelector) + .eachText().onEach { + append(it.trim()) + append("\n\n") + } + + info.selectFirst(altNameSelector) + ?.text() + ?.takeIf { it != "Updating" && it.isNotEmpty() } + ?.let { + append("Alternative Names:\n") + append(it.trim()) + } + } + status = when (info.select(statusSelector).text()) { + "On Going" -> SManga.ONGOING + "Completed" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + author = info.selectFirst(authorSelector)?.text()?.takeIf { it != "Updating" } + artist = info.selectFirst(artistSelector)?.text()?.takeIf { it != "Updating" } + genre = info.select(genreSelector).eachText().joinToString() + thumbnail_url = document.selectFirst(thumbnailSelector)?.absUrl("src") + } + + override fun chapterListSelector() = "div.chapter_box .item" + + protected open val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH) + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + element.selectFirst("a")!!.run { + setUrlWithoutDomain(absUrl("href")) + name = text() + } + date_upload = try { + dateFormat.parse(element.selectFirst("p")!!.text())!!.time + } catch (_: Exception) { + 0L + } + } + + override fun pageListParse(document: Document): List { + val script = document.selectFirst("script:containsData(slides_p_path)") + ?: throw Exception("Unable to find script with image data") + + val encodedImages = script.data() + .substringAfter('[') + .substringBefore(",]") + .replace("\"", "") + .split(",") + + return encodedImages.mapIndexed { idx, encoded -> + val url = Base64.decode(encoded, Base64.DEFAULT).toString(Charsets.UTF_8) + val imageUrl = when { + url.startsWith("/") -> "$baseUrl$url" + else -> url + } + Page(idx, imageUrl = imageUrl) + } + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18Filters.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18Filters.kt new file mode 100644 index 000000000..9c664e77a --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18Filters.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.multisrc.manga18 + +import eu.kanade.tachiyomi.source.model.Filter + +abstract class SelectFilter( + name: String, + private val options: List>, +) : Filter.Select( + name, + options.map { it.first }.toTypedArray(), +) { + val selected get() = options[state].second.takeUnless { it.isEmpty() } +} + +class TagFilter(tags: List>) : SelectFilter("Tags", tags) + +class SortFilter : SelectFilter("Sort", sortValues) + +private val sortValues = listOf( + Pair("Latest", ""), + Pair("Views", "views"), + Pair("A-Z", "name"), +) diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18Generator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18Generator.kt new file mode 100644 index 000000000..76712cff2 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/manga18/Manga18Generator.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.multisrc.manga18 + +import generator.ThemeSourceData.SingleLang +import generator.ThemeSourceGenerator + +class Manga18Generator : ThemeSourceGenerator { + + override val themePkg = "manga18" + + override val themeClass = "Manga18" + + override val baseVersionCode = 1 + + override val sources = listOf( + SingleLang("18 Porn Comic", "https://18porncomic.com", "en", isNsfw = true, className = "EighteenPornComic"), + SingleLang("Comic1000", "https://comic1000.com", "en", isNsfw = true), + SingleLang("HANMAN18", "https://hanman18.com", "zh", isNsfw = true), + SingleLang("Hentai3z.CC", "https://hentai3z.cc", "en", isNsfw = true, className = "Hentai3zCC"), + SingleLang("Manga18.Club", "https://manga18.club", "en", isNsfw = true, className = "Manga18Club"), + SingleLang("TuManhwas.Club", "https://tumanhwas.club", "es", isNsfw = true, className = "TuManhwasClub"), + ) + + companion object { + @JvmStatic + fun main(args: Array) { + Manga18Generator().createAll() + } + } +}