diff --git a/src/es/doujinyang/build.gradle b/src/es/doujinyang/build.gradle new file mode 100644 index 000000000..5c7b5bce2 --- /dev/null +++ b/src/es/doujinyang/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Doujin-Yang' + pkgNameSuffix = 'es.doujinyang' + extClass = '.DoujinYang' + extVersionCode = 1 + libVersion = '1.2' +} + +dependencies { + compileOnly 'com.google.code.gson:gson:2.8.5' + compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/es/doujinyang/src/eu/kanade/tachiyomi/extension/es/doujinyang/DoujinYang.kt b/src/es/doujinyang/src/eu/kanade/tachiyomi/extension/es/doujinyang/DoujinYang.kt new file mode 100644 index 000000000..7c30917c7 --- /dev/null +++ b/src/es/doujinyang/src/eu/kanade/tachiyomi/extension/es/doujinyang/DoujinYang.kt @@ -0,0 +1,333 @@ +package eu.kanade.tachiyomi.extension.es.doujinyang + +import android.net.Uri +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +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.Request +import okhttp3.RequestBody +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.Locale + +open class DoujinYang : ParsedHttpSource() { + + override val name = "Doujin-Yang" + override val baseUrl = "https://doujin-yang.es" + override val lang = "es" + override val supportsLatest = true + + override fun popularMangaSelector() = "article[id=item]" + override fun latestUpdatesSelector() = popularMangaSelector() + override fun searchMangaSelector() = "article[id=item]" + override fun chapterListSelector() = throw Exception("Not Used") + + override fun popularMangaNextPageSelector() = "a[href*=directorio]:containsOwn(Última)" + override fun latestUpdatesNextPageSelector() = "nav#paginacion a:contains(Última)" + override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() + + + override fun popularMangaRequest(page: Int) = + GET("$baseUrl/directorio/?orden=visitas&p=$page", headers) + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/reciente/doujin?p=$page", headers) + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val uri = if (query.isNotBlank()) { + Uri.parse(baseUrl).buildUpon() + .appendQueryParameter("s", query) + } else { + val uri = Uri.parse("$baseUrl/directorio").buildUpon() + //Append uri filters + filters.forEach { + if (it is UriFilter) + it.addToUri(uri) + } + uri.appendQueryParameter("p", page.toString()) + } + return GET(uri.toString(), headers) + } + + //override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, headers) + //override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers) + + override fun chapterListRequest(manga: SManga): Request { + return GET(baseUrl + manga.url, headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage { + val document = response.asJsoup() + val mangas = document.select(latestUpdatesSelector()) + .distinctBy { it.select("a").first().attr("abs:href") } + .map { latestUpdatesFromElement(it) } + val hasNextPage = latestUpdatesNextPageSelector().let { selector -> + document.select(selector).first() + } != null + return MangasPage(mangas, hasNextPage) + } + + override fun popularMangaFromElement(element: Element) = mangaFromElement(element) + override fun latestUpdatesFromElement(element: Element): SManga { + val manga = SManga.create() + manga.setUrlWithoutDomain(element.select("a").first().attr("abs:href")) + manga.title = element.select("a").first().text().trim() + return manga + } + + override fun searchMangaFromElement(element: Element) = mangaFromElement(element) + + private fun mangaFromElement(element: Element): SManga { + val manga = SManga.create() + manga.setUrlWithoutDomain(element.select("a").first().attr("abs:href")) + manga.title = element.select("h2").text().trim() + //manga.thumbnail_url = "https:" + element.select("img").attr("src") + manga.thumbnail_url = element.select("img").attr("abs:src") + return manga + } + + + override fun chapterListParse(response: Response): List { + return response.asJsoup().select("div#c_list a").map { element -> + SChapter.create().apply { + name = element.select("h3").text() + setUrlWithoutDomain(element.attr("abs:href")) + } + } + } + + private fun parseDate(date: String): Long { + return SimpleDateFormat("yyyy-MM-dd kk:mm:ss", Locale.US).parse(date)?.time ?: 0 + } + + override fun chapterFromElement(element: Element) = throw Exception("Not used") + + override fun mangaDetailsParse(document: Document): SManga { + val manga = SManga.create() + manga.thumbnail_url = document.select("img[src*=cover]").attr("abs:src") + manga.description = document.select("div[id=sinopsis]").last().ownText() + manga.author = document.select("div[id=info-i]").text().let { + if (it.contains("Autor", true)) { + it.substringAfter("Autor:").substringBefore("Fecha:").trim() + } else "N/A" + } + manga.artist = manga.author + val glist = document.select("div[id=categ] a[href*=genero]").map { it.text() } + manga.genre = glist.joinToString(", ") + manga.status = when (document.select("span[id=desarrollo]")?.first()?.text()) { + "En desarrollo" -> SManga.ONGOING + //"Completed" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + return manga + } + + override fun pageListRequest(chapter: SChapter): Request { + return POST( + baseUrl + chapter.url, + headersBuilder().add("Content-Type", "application/x-www-form-urlencoded").build(), + RequestBody.create(null, "info") + ) + } + + override fun pageListParse(response: Response): List { + return response.body()!!.string().substringAfter(",[").substringBefore("]") + .replace(Regex("""[\\"]"""), "").split(",").let { list -> + val path = "https:" + list[0] + list.drop(1).mapIndexed { i, img -> Page(i, "", path + img) } + } + } + + override fun pageListParse(document: Document) = throw Exception("Not Used") + override fun imageUrlParse(document: Document) = throw Exception("Not Used") + + override fun getFilterList() = FilterList( + Filter.Header("NOTA: ¡La búsqueda de títulos no funciona!"), // "Title search not working" + Filter.Separator(), + GenreFilter(), + LetterFilter(), + StatusFilter(), + SortFilter() + ) + + class GenreFilter : UriPartFilter( + "Género", "genero", arrayOf( + Pair("all", "All"), + Pair("1", "Ahegao"), + Pair("379", "Alien"), + Pair("2", "Anal"), + Pair("490", "Android18"), + Pair("717", "Angel"), + Pair("633", "Asphyxiation"), + Pair("237", "Bandages"), + Pair("77", "Bbw"), + Pair("143", "Bdsm"), + Pair("23", "Blackmail"), + Pair("113", "Blindfold"), + Pair("24", "Blowjob"), + Pair("166", "Blowjobface"), + Pair("25", "Body Writing"), + Pair("314", "Bodymodification"), + Pair("806", "Bodystocking"), + Pair("366", "Bodysuit"), + Pair("419", "Bodyswap"), + Pair("325", "Bodywriting"), + Pair("5", "Bondage"), + Pair("51", "Bukkake"), + Pair("410", "Catgirl"), + Pair("61", "Chastitybelt"), + Pair("78", "Cheating"), + Pair("293", "Cheerleader"), + Pair("62", "Collar"), + Pair("120", "Compilation"), + Pair("74", "Condom"), + Pair("63", "Corruption"), + Pair("191", "Corset"), + Pair("234", "Cosplaying"), + Pair("389", "Cowgirl"), + Pair("256", "Crossdressing"), + Pair("179", "Crotchtattoo"), + Pair("689", "Crown"), + Pair("733", "Cumflation"), + Pair("385", "Cumswap"), + Pair("251", "Cunnilingus"), + Pair("75", "Darkskin"), + Pair("180", "Daughter"), + Pair("52", "Deepthroat"), + Pair("28", "Defloration"), + Pair("198", "Demon"), + Pair("145", "Demongirl"), + Pair("64", "Drugs"), + Pair("95", "Drunk"), + Pair("462", "Femalesonly"), + Pair("82", "Femdom"), + Pair("139", "Ffmthreesome"), + Pair("823", "Fftthreesome"), + Pair("55", "Full Color"), + Pair("181", "Fullbodytattoo"), + Pair("203", "Fullcensorship"), + Pair("111", "Fullcolor"), + Pair("114", "Gag"), + Pair("3", "Glasses"), + Pair("515", "Gloryhole"), + Pair("116", "Humanpet"), + Pair("32", "Humiliation"), + Pair("147", "Latex"), + Pair("12", "Maid"), + Pair("4", "Milf"), + Pair("245", "Military"), + Pair("414", "Milking"), + Pair("34", "Mind Control"), + Pair("68", "Mindbreak"), + Pair("124", "Mindcontrol"), + Pair("645", "Nun"), + Pair("312", "Nurse"), + Pair("272", "Robot"), + Pair("7", "Romance"), + Pair("761", "Sundress"), + Pair("412", "Tailplug"), + Pair("253", "Tutor"), + Pair("259", "Twins"), + Pair("207", "Twintails"), + Pair("840", "Valkyrie"), + Pair("530", "Vampire"), + Pair("16", "Yuri"), + Pair("273", "Zombie") + ) + ) + + class LetterFilter : UriPartFilter( + "Letra", "letra", arrayOf( + Pair("all", "All"), + Pair("a", "A"), + Pair("b", "B"), + Pair("c", "C"), + Pair("d", "D"), + Pair("e", "E"), + Pair("f", "F"), + Pair("g", "G"), + Pair("h", "H"), + Pair("i", "I"), + Pair("j", "J"), + Pair("k", "K"), + Pair("l", "L"), + Pair("m", "M"), + Pair("n", "N"), + Pair("o", "O"), + Pair("p", "P"), + Pair("q", "Q"), + Pair("r", "R"), + Pair("s", "S"), + Pair("t", "T"), + Pair("u", "U"), + Pair("v", "V"), + Pair("w", "W"), + Pair("x", "X"), + Pair("y", "Y"), + Pair("z", "Z") + ) + ) + + class StatusFilter : UriPartFilter( + "Estado", "estado", arrayOf( + Pair("all", "All"), Pair("1", "En desarrollo"), Pair("0", "Finalizado") + ) + ) + + class SortFilter : UriPartFilterreq( + "Sort", "orden", arrayOf( + Pair("visitas", "Visitas"), + Pair("desc", "Descendente"), + Pair("asc", "Ascendente"), + Pair("lanzamiento", "Lanzamiento"), + Pair("nombre", "Nombre") + ) + ) + + /** + * Class that creates a select filter. Each entry in the dropdown has a name and a display name. + * If an entry is selected it is appended as a query parameter onto the end of the URI. + * If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI. + */ + //vals: + open class UriPartFilter( + displayName: String, + private val uriParam: String, + private val vals: Array>, + private val firstIsUnspecified: Boolean = true, + defaultValue: Int = 0 + ) : + Filter.Select(displayName, vals.map { it.second }.toTypedArray(), defaultValue), + UriFilter { + override fun addToUri(uri: Uri.Builder) { + if (state != 0 || !firstIsUnspecified) + uri.appendQueryParameter(uriParam, vals[state].first) + } + } + + open class UriPartFilterreq( + displayName: String, + private val uriParam: String, + private val vals: Array> + ) : + Filter.Select(displayName, vals.map { it.second }.toTypedArray()), UriFilter { + override fun addToUri(uri: Uri.Builder) { + uri.appendQueryParameter(uriParam, vals[state].first) + } + } + + /** + * Represents a filter that is able to modify a URI. + */ + private interface UriFilter { + fun addToUri(uri: Uri.Builder) + } +} +