diff --git a/src/en/eggporncomics/build.gradle b/src/en/eggporncomics/build.gradle new file mode 100644 index 000000000..7cd623f86 --- /dev/null +++ b/src/en/eggporncomics/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Eggporncomics' + pkgNameSuffix = 'en.eggporncomics' + extClass = '.Eggporncomics' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/eggporncomics/res/mipmap-hdpi/ic_launcher.png b/src/en/eggporncomics/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..a04710a0b Binary files /dev/null and b/src/en/eggporncomics/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/eggporncomics/res/mipmap-mdpi/ic_launcher.png b/src/en/eggporncomics/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..4aac20b94 Binary files /dev/null and b/src/en/eggporncomics/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/eggporncomics/res/mipmap-xhdpi/ic_launcher.png b/src/en/eggporncomics/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..5a230fc85 Binary files /dev/null and b/src/en/eggporncomics/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/eggporncomics/res/mipmap-xxhdpi/ic_launcher.png b/src/en/eggporncomics/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..e2c17c03a Binary files /dev/null and b/src/en/eggporncomics/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/eggporncomics/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/eggporncomics/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..e64583d28 Binary files /dev/null and b/src/en/eggporncomics/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/eggporncomics/res/web_hi_res_512.png b/src/en/eggporncomics/res/web_hi_res_512.png new file mode 100644 index 000000000..08d663d37 Binary files /dev/null and b/src/en/eggporncomics/res/web_hi_res_512.png differ diff --git a/src/en/eggporncomics/src/eu/kanade/tachiyomi/extension/en/eggporncomics/Eggporncomics.kt b/src/en/eggporncomics/src/eu/kanade/tachiyomi/extension/en/eggporncomics/Eggporncomics.kt new file mode 100644 index 000000000..fa39b7b97 --- /dev/null +++ b/src/en/eggporncomics/src/eu/kanade/tachiyomi/extension/en/eggporncomics/Eggporncomics.kt @@ -0,0 +1,262 @@ +package eu.kanade.tachiyomi.extension.en.eggporncomics + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservable +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 java.util.Calendar +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable + +class Eggporncomics : ParsedHttpSource() { + + override val name = "Eggporncomics" + + override val baseUrl = "https://eggporncomics.com" + + override val lang = "en" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + // Popular + + // couldn't find a page with popular comics, defaulting to the "anime" category + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/category/1/anime?page=$page", headers) + } + + override fun popularMangaSelector() = "div.preview:has(div.name)" + + override fun popularMangaFromElement(element: Element): SManga { + return SManga.create().apply { + element.select("a:has(img)").let { + setUrlWithoutDomain(it.attr("href")) + title = it.text() + thumbnail_url = it.select("img").attr("abs:src") + } + } + } + + override fun popularMangaNextPageSelector() = "ul.ne-pe li.next:not(.disabled)" + + // Latest + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/latest-comics?page=$page", headers) + } + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + + // Search + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return client.newCall(searchMangaRequest(page, query, filters)) + .asObservable().doOnNext { response -> + if (!response.isSuccessful) { + // when combining a category filter and comics filter, if there are no results the source + // issues a 404, override that so as not to confuse users + if (response.request().url().toString().contains("category-tag") && response.code() == 404) { + Observable.just(MangasPage(emptyList(), false)) + } else { + response.close() + throw Exception("HTTP error ${response.code()}") + } + } + } + .map { response -> + searchMangaParse(response) + } + } + + private val queryRegex = Regex("""[\s']""") + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return if (query.isNotBlank()) { + GET("$baseUrl/search/${query.replace(queryRegex, "-")}?page=$page", headers) + } else { + val url = HttpUrl.parse(baseUrl)!!.newBuilder() + val filterList = if (filters.isEmpty()) getFilterList() else filters + val category = filterList.find { it is CategoryFilter } as UriPartFilter + val comics = filterList.find { it is ComicsFilter } as UriPartFilter + + when { + category.isNotNull() && comics.isNotNull() -> { + url.addPathSegments("category-tag/${category.toUriPart()}/${comics.toUriPart()}") + } + category.isNotNull() -> { + url.addPathSegments("category/${category.toUriPart()}") + } + comics.isNotNull() -> { + url.addPathSegments("comics-tag/${comics.toUriPart()}") + } + } + + url.addQueryParameter("page", page.toString()) + + GET(url.toString(), headers) + } + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + // Details + + private val descriptionPrefixRegex = Regex(""":.*""") + + override fun mangaDetailsParse(document: Document): SManga { + return SManga.create().apply { + thumbnail_url = document.select(pageListSelector).first().toFullSizeImage() + description = document.select("div.links ul").joinToString("\n") { element -> element.select("a") + .joinToString(prefix = element.select("span").text().replace(descriptionPrefixRegex, ": ")) { it.text() } } + } + } + + // Chapters + + override fun chapterListParse(response: Response): List { + return listOf(SChapter.create().apply { + setUrlWithoutDomain(response.request().url().toString()) + name = "Chapter" + date_upload = response.asJsoup().select("div.info > div.meta li:contains(days ago)").firstOrNull() + ?.let { Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -(it.text().substringBefore(" ").toIntOrNull() ?: 0)) }.timeInMillis } + ?: 0 + }) + } + + override fun chapterListSelector() = throw UnsupportedOperationException("Not used") + + override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException("Not used") + + // Pages + + private fun Element.toFullSizeImage() = this.attr("abs:src").replace("thumb300_", "") + + private val pageListSelector = "div.grid div.image img" + + override fun pageListParse(document: Document): List { + return document.select(pageListSelector).mapIndexed { i, img -> + Page(i, "", img.toFullSizeImage()) + } + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") + + // Filters + + override fun getFilterList() = FilterList( + Filter.Header("Leave query blank to use filters"), + Filter.Separator(), + CategoryFilter("Category", getCategoryList), + ComicsFilter("Comics", getComicsList) + ) + + private class CategoryFilter(name: String, vals: Array>) : UriPartFilter(name, vals) + private class ComicsFilter(name: String, vals: Array>) : UriPartFilter(name, vals) + + private val getCategoryList: Array> = arrayOf( + Pair("Any", null), + Pair("3d comics", "7/3d-comics"), + Pair("8muses", "18/8muses"), + Pair("Anime", "1/anime"), + Pair("Cartoon", "2/cartoon"), + Pair("Dickgirls & Shemale", "6/dickgirls-shemale"), + Pair("Furry", "4/furry"), + Pair("Games comics", "3/games-comics"), + Pair("Hentai manga", "10/hentai-manga"), + Pair("Interracial", "14/interracial"), + Pair("Milf", "11/milf"), + Pair("Mindcontrol", "15/mindcontrol"), + Pair("Porn Comix", "16/porn-comix"), + Pair("Western", "12/western"), + Pair("Yaoi/Gay", "8/yaoigay"), + Pair("Yuri and Lesbian", "9/yuri-and-lesbian") + ) + + private val getComicsList: Array> = arrayOf( + Pair("Any", null), + Pair("3d", "85/3d"), + Pair("Adventure Time", "2950/adventure-time"), + Pair("Anal", "13/anal"), + Pair("Ben 10", "641/ben10"), + Pair("Big boobs", "3025/big-boobs"), + Pair("Big breasts", "6/big-breasts"), + Pair("Big cock", "312/big-cock"), + Pair("Bigass", "604/big-ass-porn-comics-new"), + Pair("Black cock", "2990/black-cock"), + Pair("Blowjob", "7/blowjob"), + Pair("Bondage", "24/bondage"), + Pair("Breast expansion hentai", "102/breast-expansion-new"), + Pair("Cumshot", "427/cumshot"), + Pair("Dark skin", "29/dark-skin"), + Pair("Dofantasy", "1096/dofantasy"), + Pair("Double penetration", "87/double-penetration"), + Pair("Doujin moe", "3028/doujin-moe"), + Pair("Erotic", "602/erotic"), + Pair("Fairy tail porn", "3036/fairy-tail"), + Pair("Fakku", "1712/Fakku-Comics-new"), + Pair("Fakku comics", "1712/fakku-comics-new"), + Pair("Family Guy porn", "774/family-guy"), + Pair("Fansadox", "1129/fansadox-collection"), + Pair("Feminization", "385/feminization"), + Pair("Forced", "315/forced"), + Pair("Full color", "349/full-color"), + Pair("Furry", "19/furry"), + Pair("Futanari", "2994/futanari"), + Pair("Group", "58/group"), + Pair("Hardcore", "304/hardcore"), + Pair("Harry Potter porn", "338/harry-potter"), + Pair("Hentai", "321/hentai"), + Pair("Incest", "3007/incest"), + Pair("Incest - Family Therapy Top", "3007/family-therapy-top"), + Pair("Incognitymous", "545/incognitymous"), + Pair("Interracical", "608/interracical"), + Pair("Jab Comix", "1695/JAB-Comics-NEW-2"), + Pair("Kaos comics", "467/kaos"), + Pair("Kim Possible porn", "788/kim-possible"), + Pair("Lesbian", "313/lesbian"), + Pair("Locofuria", "343/locofuria"), + Pair("Milf", "48/milf"), + Pair("Milftoon", "1678/milftoon-comics"), + Pair("Muscle", "2/muscle"), + Pair("Nakadashi", "10/nakadashi"), + Pair("PalComix", "373/palcomix"), + Pair("Pokemon hentai", "657/pokemon"), + Pair("Shadbase", "1717/shadbase-comics"), + Pair("Shemale", "126/shemale"), + Pair("Slut", "301/slut"), + Pair("Sparrow hentai", "3035/sparrow-hentai"), + Pair("Star Wars hentai", "1344/star-wars"), + Pair("Stockings", "51/stockings"), + Pair("Superheroine Central", "615/superheroine-central"), + Pair("The Cummoner", "3034/the-cummoner"), + Pair("The Rock Cocks", "3031/the-rock-cocks"), + Pair("ZZZ Comics", "1718/zzz-comics") + ) + + private open class UriPartFilter(displayName: String, val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + fun isNotNull(): Boolean = toUriPart() != null + } +}