diff --git a/src/id/doujindesu/AndroidManifest.xml b/src/id/doujindesu/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/id/doujindesu/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/id/doujindesu/build.gradle b/src/id/doujindesu/build.gradle new file mode 100644 index 000000000..bb23b6e7b --- /dev/null +++ b/src/id/doujindesu/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'DoujinDesu' + pkgNameSuffix = 'id.doujindesu' + extClass = '.DoujinDesu' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/id/doujindesu/res/mipmap-hdpi/ic_launcher.png b/src/id/doujindesu/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..d38defae2 Binary files /dev/null and b/src/id/doujindesu/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/id/doujindesu/res/mipmap-mdpi/ic_launcher.png b/src/id/doujindesu/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..1824ab09d Binary files /dev/null and b/src/id/doujindesu/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/id/doujindesu/res/mipmap-xhdpi/ic_launcher.png b/src/id/doujindesu/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e96305136 Binary files /dev/null and b/src/id/doujindesu/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/id/doujindesu/res/mipmap-xxhdpi/ic_launcher.png b/src/id/doujindesu/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..309727cb0 Binary files /dev/null and b/src/id/doujindesu/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/id/doujindesu/res/mipmap-xxxhdpi/ic_launcher.png b/src/id/doujindesu/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..3caa40ef0 Binary files /dev/null and b/src/id/doujindesu/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/id/doujindesu/res/web_hi_res_192.png b/src/id/doujindesu/res/web_hi_res_192.png new file mode 100644 index 000000000..56a060ec7 Binary files /dev/null and b/src/id/doujindesu/res/web_hi_res_192.png differ diff --git a/src/id/doujindesu/src/eu/kanade/tachiyomi/extension/id/doujindesu/DoujinDesu.kt b/src/id/doujindesu/src/eu/kanade/tachiyomi/extension/id/doujindesu/DoujinDesu.kt new file mode 100644 index 000000000..8ff86d3c2 --- /dev/null +++ b/src/id/doujindesu/src/eu/kanade/tachiyomi/extension/id/doujindesu/DoujinDesu.kt @@ -0,0 +1,321 @@ +package eu.kanade.tachiyomi.extension.id.doujindesu + +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.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.Locale + +class DoujinDesu : ParsedHttpSource() { + // Information : DoujinDesu use EastManga WordPress Theme + override val name = "Doujindesu" + override val baseUrl = "https://doujindesu.id" + override val lang = "id" + override val supportsLatest = true + override val client: OkHttpClient = network.cloudflareClient + + // Private stuff + + private val DATE_FORMAT by lazy { + SimpleDateFormat("MMMM d, yyyy", Locale("id")) + } + + private fun parseStatus(status: String) = when { + status.toLowerCase(Locale.US).contains("finished") -> SManga.ONGOING + status.toLowerCase(Locale.US).contains("publishing") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + private class Category(title: String, val key: String) : Filter.TriState(title) { + override fun toString(): String { + return name + } + } + + private class Genre(title: String, val key: String) : Filter.TriState(title) { + override fun toString(): String { + return name + } + } + + private class Order(title: String, val key: String) : Filter.TriState(title) { + override fun toString(): String { + return name + } + } + + private class Status(title: String, val key: String) : Filter.TriState(title) { + override fun toString(): String { + return name + } + } + + private val orderBy = arrayOf( + Order("All", ""), + Order("A-Z", "title"), + Order("Z-A", "titlereverse"), + Order("Latest Update", "update"), + Order("Latest Added", "latest"), + Order("Popular", "popular") + ) + + private val statusList = arrayOf( + Status("All", ""), + Status("Publishing", "Publishing"), + Status("Finished", "Finished") + ) + + private val genreList = arrayOf( + Genre("All", ""), + Genre("Age Regression", "age-regression"), + Genre("Ahegao", "ahegao"), + Genre("All The Way Through", "all-the-way-through"), + Genre("Amputee", "amputee"), + Genre("Anal", "anal"), + Genre("Anorexia", "anorexia"), + Genre("Apron", "apron"), + Genre("Artist CG", "artist-cg"), + Genre("Aunt", "aunt"), + Genre("Bald", "bald"), + Genre("Bestiality", "bestiality"), + Genre("Big As", "big-as"), + Genre("Big Ass", "big-ass"), + Genre("Big Breast", "big-breast"), + Genre("Big Penis", "big-penis"), + Genre("Bike Shorts", "bike-shorts"), + Genre("Bikini", "bikini"), + Genre("Birth", "birth"), + Genre("Bisexual", "bisexual"), + Genre("Blackmail", "blackmail"), + Genre("Blindfold", "blindfold"), + Genre("Bloomers", "bloomers"), + Genre("Blowjob", "blowjob"), + Genre("Body Swap", "body-swap"), + Genre("Bodysuit", "bodysuit"), + Genre("Bondage", "bondage"), + Genre("Business Suit", "business-suit"), + Genre("Cheating", "cheating"), + Genre("Collar", "collar"), + Genre("Condom", "condom"), + Genre("Cousin", "cousin"), + Genre("Crossdressing", "crossdressing"), + Genre("Cunnilingus", "cunnilingus"), + Genre("Dark Skin", "dark-skin"), + Genre("Daughter", "daughter"), + Genre("Defloartion", "defloartion"), + Genre("Defloration", "defloration"), + Genre("Demon", "demon"), + Genre("Demon Girl", "demon-girl"), + Genre("Dick Growth", "dick-growth"), + Genre("DILF", "dilf"), + Genre("Double Penetration", "double-penetration"), + Genre("Drugs", "drugs"), + Genre("Drunk", "drunk"), + Genre("Elf", "elf"), + Genre("Emotionless Sex", "emotionless-sex"), + Genre("Exhibitionism", "exhibitionism"), + Genre("Eyepatch", "eyepatch"), + Genre("Fantasy", "fantasy"), + Genre("Females Only", "females-only"), + Genre("Femdom", "femdom"), + Genre("Filming", "filming"), + Genre("Fingering", "fingering"), + Genre("Footjob", "footjob"), + Genre("Full Color", "full-color"), + Genre("Furry", "furry"), + Genre("Futanari", "futanari"), + Genre("Garter Belt", "garter-belt"), + Genre("Gender Bender", "gender-bender"), + Genre("Ghost", "ghost"), + Genre("Glasses", "glasses"), + Genre("Gore", "gore"), + Genre("Group", "group"), + Genre("Guro", "guro"), + Genre("Gyaru", "gyaru"), + Genre("Hairy", "hairy"), + Genre("Handjob", "handjob"), + Genre("Harem", "harem"), + Genre("Horns", "horns"), + Genre("Huge Breast", "huge-breast"), + Genre("Humiliation", "humiliation"), + Genre("Impregnation", "impregnation"), + Genre("Incest", "incest"), + Genre("Inflation", "inflation"), + Genre("Insect", "insect"), + Genre("Inseki", "inseki"), + Genre("Inverted Nipples", "inverted-nipples"), + Genre("Invisible", "invisible"), + Genre("Kemomimi", "kemomimi"), + Genre("Kimono", "kimono"), + Genre("Lactation", "lactation"), + Genre("Leotard", "leotard"), + Genre("Lingerie", "lingerie"), + Genre("Loli", "loli"), + Genre("Lolipai", "lolipai"), + Genre("Maid", "maid"), + Genre("Males Only", "males-only"), + Genre("Masturbation", "masturbation"), + Genre("Miko", "miko"), + Genre("MILF", "milf"), + Genre("Mind Break", "mind-break"), + Genre("Mind Control", "mind-control"), + Genre("Minigirl", "minigirl"), + Genre("Miniguy", "miniguy"), + Genre("Monster", "monster"), + Genre("Monster Girl", "monster-girl"), + Genre("Mother", "mother"), + Genre("Multi-work Series", "multi-work-series"), + Genre("Muscle", "muscle") + ) + + private val categoryNames = arrayOf( + Category("All", ""), + Category("Manga", "Manga"), + Category("Manhua", "Manhua"), + Category("Doujinshi", "Doujinshi"), + Category("Manhwa", "Manhwa") + ) + + private class CategoryNames(categories: Array) : Filter.Select("Category", categories, 0) + private class OrderBy(orders: Array) : Filter.Select("Order", orders, 0) + private class GenreList(genres: Array) : Filter.Select("Genre", genres, 0) + private class StatusList(statuses: Array) : Filter.Select("Status", statuses, 0) + + private fun basicInformationFromElement(element: Element): SManga { + val manga = SManga.create() + + manga.title = element.select("div > div > a").attr("alt") + manga.setUrlWithoutDomain(element.select("div > div > a").attr("href")) + manga.thumbnail_url = element.select("div > div > a > div > img").attr("src") + + return manga + } + + private fun getNumberFromString(epsStr: String): String { + return epsStr.filter { it.isDigit() } + } + + private fun reconstructDate(dateStr: String): Long { + return runCatching { DATE_FORMAT.parse(dateStr)?.time } + .getOrNull() ?: 0L + } + + private fun getImage(element: Element): Page { + return Page(getNumberFromString(element.attr("img-id")).toInt(), "", element.attr("src")) + } + + // Popular + + override fun popularMangaFromElement(element: Element): SManga = basicInformationFromElement(element) + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/komik-list/page/$page/?&order=popular") + } + + // Latest + + override fun latestUpdatesFromElement(element: Element): SManga = basicInformationFromElement(element) + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/komik-list/page/$page/?order=update") + } + + // Element Selectors + + override fun latestUpdatesSelector(): String = "#main > div.relat > article" + override fun popularMangaSelector(): String = "#main > div.relat > article" + override fun searchMangaSelector(): String = "#main > div.relat > article" + + override fun popularMangaNextPageSelector(): String = "#nextpagination" + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + // Search & FIlter + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + var url = "$baseUrl/komik-list/page/$page/".toHttpUrlOrNull()?.newBuilder()!!.addQueryParameter("title", query) + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is CategoryNames -> { + val category = filter.values[filter.state] + url.addQueryParameter("type", category.key) + } + is OrderBy -> { + val order = filter.values[filter.state] + url.addQueryParameter("order", order.key) + } + is GenreList -> { + val genre = filter.values[filter.state] + url.addQueryParameter("genre", genre.key) + } + is StatusList -> { + val status = filter.values[filter.state] + url.addQueryParameter("status", status.key) + } + } + } + return GET(url.toString(), headers) + } + + override fun searchMangaFromElement(element: Element): SManga = basicInformationFromElement(element) + + override fun getFilterList() = FilterList( + CategoryNames(categoryNames), + OrderBy(orderBy), + GenreList(genreList), + StatusList(statusList) + ) + + // Detail Parse + + override fun mangaDetailsParse(document: Document): SManga { + val manga = SManga.create() + manga.description = when { + document.select("div.infox > div.entry-content.entry-content-single > p").isEmpty() -> "No description specified" + else -> document.select("div.infox > div.entry-content.entry-content-single > p").first().text() + } + manga.author = document.select("div.infox > div.spe > span:nth-child(5)").text() + manga.genre = document.select("div.genre-info > a[itemprop=genre]").joinToString { it.text() } + manga.status = parseStatus(document.select("div.infox > div.spe > span:nth-child(1)").text()) + manga.thumbnail_url = document.select("div.thumb > img").attr("src") + manga.artist = document.select("div.infox > div.spe > span:nth-child(6)").text() + + return manga + } + + // Chapter Stuff + + override fun chapterFromElement(element: Element): SChapter { + val chapter = SChapter.create() + val number = getNumberFromString(element.select("div.epsright > span > a > chapter").text()) + chapter.chapter_number = when { + (number.isNotEmpty()) -> number.toFloat() + else -> 1F + } + chapter.date_upload = reconstructDate(element.select("div.epsleft > span.date").text()) + chapter.name = element.select("div.epsleft > span.lchx > a").text() + chapter.setUrlWithoutDomain(element.select("div.epsleft > span.lchx > a").attr("href")) + + return chapter + } + + override fun chapterListSelector(): String = "#chapter_list li" + + // More parser stuff + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used") + + override fun pageListParse(document: Document): List { + return document.select("div.reader-area > img").mapIndexed { i, element -> + Page(i, "", element.attr("src")) + } + } +}