diff --git a/src/pt/hentaiyabu/AndroidManifest.xml b/src/pt/hentaiyabu/AndroidManifest.xml new file mode 100644 index 000000000..648a5d4ff --- /dev/null +++ b/src/pt/hentaiyabu/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/src/pt/hentaiyabu/build.gradle b/src/pt/hentaiyabu/build.gradle new file mode 100644 index 000000000..4cd8d1083 --- /dev/null +++ b/src/pt/hentaiyabu/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'HentaiYabu' + pkgNameSuffix = 'pt.hentaiyabu' + extClass = '.HentaiYabu' + extVersionCode = 1 + libVersion = '12' + containsNsfw = true +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/hentaiyabu/res/mipmap-hdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..3bba6f76d Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/hentaiyabu/res/mipmap-mdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..74d6e1971 Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/hentaiyabu/res/mipmap-xhdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..c511591b4 Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/hentaiyabu/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..88515b652 Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/hentaiyabu/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/hentaiyabu/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..d27590920 Binary files /dev/null and b/src/pt/hentaiyabu/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYConstants.kt b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYConstants.kt new file mode 100644 index 000000000..5b3a8844f --- /dev/null +++ b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYConstants.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu + +object HYConstants { + const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7" + const val USER_AGENT = "Mozilla/5.0 (Linux; Android 10; SM-A307GT Build/QP1A.190711.020;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/103.0.5060.71 Mobile Safari/537.36" + const val PREFERRED_QUALITY = "preferred_quality" + const val PREFIX_SEARCH_SLUG = "slug:" + val QUALITY_LIST = arrayOf("SD", "HD") + val PLAYER_REGEX = Regex("""label: "(\w+)",.*file: "(.*?)"""") +} diff --git a/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYFilters.kt b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYFilters.kt new file mode 100644 index 000000000..65fdbc7b3 --- /dev/null +++ b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYFilters.kt @@ -0,0 +1,275 @@ +package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu + +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +object HYFilters { + + open class QueryPartFilter( + displayName: String, + val vals: Array> + ) : AnimeFilter.Select( + displayName, + vals.map { it.first }.toTypedArray() + ) { + fun toQueryPart() = vals[state].second + } + + open class TriStateFilterList(name: String, values: List) : AnimeFilter.Group(name, values) + private class TriStateVal(name: String) : AnimeFilter.TriState(name) + + private inline fun AnimeFilterList.getFirst(): R { + return this.filterIsInstance().first() + } + + private inline fun AnimeFilterList.asQueryPart(): String { + return this.getFirst().let { + (it as QueryPartFilter).toQueryPart() + } + } + + class InitialLetterFilter : QueryPartFilter("Primeira letra", HYFiltersData.initialLetter) + + class EpisodeFilter : AnimeFilter.Text("Episódios") + class EpisodeFilterMode : QueryPartFilter("Modo de filtro", HYFiltersData.episodeFilterMode) + class SortFilter : AnimeFilter.Sort( + "Ordenar", + HYFiltersData.orders.map { it.first }.toTypedArray(), + Selection(0, true) + ) + + class GenresFilter : TriStateFilterList( + "Gêneros", + HYFiltersData.genres.map { TriStateVal(it) } + ) + + val filterList = AnimeFilterList( + InitialLetterFilter(), + SortFilter(), + AnimeFilter.Separator(), + EpisodeFilter(), + EpisodeFilterMode(), + AnimeFilter.Separator(), + GenresFilter(), + ) + + data class FilterSearchParams( + val initialLetter: String = "", + val episodesFilterMode: String = ">=", + var numEpisodes: Int = 0, + var orderAscending: Boolean = true, + var sortBy: String = "", + val blackListedGenres: ArrayList = ArrayList(), + val includedGenres: ArrayList = ArrayList(), + var animeName: String = "" + ) + + internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams { + val searchParams = FilterSearchParams( + filters.asQueryPart(), + filters.asQueryPart(), + ) + + searchParams.numEpisodes = try { + filters.getFirst().state.toInt() + } catch (e: NumberFormatException) { 0 } + + filters.getFirst().state?.let { + val order = HYFiltersData.orders[it.index].second + searchParams.orderAscending = it.ascending + searchParams.sortBy = order + } + + filters.getFirst() + .state.forEach { genre -> + if (genre.isIncluded()) { + searchParams.includedGenres.add(genre.name) + } else if (genre.isExcluded()) { + searchParams.blackListedGenres.add(genre.name) + } + } + + return searchParams + } + + private fun mustRemove(anime: SearchResultDto, params: FilterSearchParams): Boolean { + val epFilterMode = params.episodesFilterMode + return when { + params.animeName != "" && params.animeName.lowercase() !in anime.title.lowercase() -> true + anime.title == "null" -> true + params.initialLetter != "" && !anime.title.startsWith(params.initialLetter) -> true + params.blackListedGenres.size > 0 && params.blackListedGenres.any { + it.lowercase() in anime.genre.lowercase() + } -> true + params.includedGenres.size > 0 && params.includedGenres.any { + it.lowercase() !in anime.genre.lowercase() + } -> true + params.numEpisodes > 0 -> { + when (epFilterMode) { + "==" -> params.numEpisodes != anime.videos + ">=" -> params.numEpisodes >= anime.videos + "<=" -> params.numEpisodes <= anime.videos + else -> false + } + } + else -> false + } + } + + fun MutableList.applyFilterParams(params: FilterSearchParams) { + this.removeAll { anime -> mustRemove(anime, params) } + when (params.sortBy) { + "A-Z" -> { + if (!params.orderAscending) + this.reverse() + } + "num" -> { + if (params.orderAscending) + this.sortBy { it.videos } + else + this.sortByDescending { it.videos } + } + } + } + + private object HYFiltersData { + + val orders = arrayOf( + Pair("Alfabeticamente", "A-Z"), + Pair("Por número de eps", "num") + ) + + val initialLetter = arrayOf(Pair("Qualquer uma", "")) + ('A'..'Z').map { + Pair(it.toString(), it.toString()) + }.toTypedArray() + + val episodeFilterMode = arrayOf( + Pair("Maior ou igual", ">="), + Pair("Menor ou igual", "<="), + Pair("Igual", "=="), + ) + + val genres = arrayOf( + "Ahegao", + "Anal", + "Artes Marciais", + "Ashikoki", + "Aventura", + "Ação", + "BDSM", + "Bara", + "Boquete", + "Boys Love", + "Brinquedos", + "Brinquedos Sexuais", + "Bukkake", + "Bunda Grande", + "Chikan", + "Científica", + "Comédia", + "Cosplay", + "Creampie", + "Dark Skin", + "Demônio", + "Drama", + "Dupla Penetração", + "Ecchi", + "Elfos", + "Empregada", + "Enfermeira", + "Eroge", + "Erótico", + "Escolar", + "Esporte", + "Estupro", + "Facial", + "Fantasia", + "Femdom", + "Ficção", + "Ficção Científica", + "Futanari", + "Gang Bang", + "Garotas De Escritório", + "Gender Bender", + "Gerakuro", + "Gokkun", + "Golden Shower", + "Gore", + "Gozando Dentro", + "Grupo", + "Grávida", + "Guerra", + "Gyaru", + "Harém", + "Hipnose", + "Histórico", + "Horror", + "Incesto", + "Jogos Eróticos", + "Josei", + "Kemono", + "Kemonomimi", + "Lactação", + "Lolicon", + "Magia", + "Maid", + "Masturbação", + "Mecha", + "Menage", + "Metrô", + "Milf", + "Mind Break", + "Mind Control", + "Mistério", + "Moe", + "Monstros", + "Médico", + "Nakadashi", + "Nerd", + "Netorare", + "Ninjas", + "Óculos", + "Oral", + "Orgia", + "Paizuri", + "Paródia", + "Peitões", + "Pelos Pubianos", + "Pettanko", + "Policial", + "Preservativo", + "Professor", + "Psicológico", + "Punição", + "Raio-X", + "Romance", + "Ronin", + "Sci-Fi", + "Seinen", + "Sexo Público", + "Shotacon", + "Shoujo Ai", + "Shounen", + "Shounen Ai", + "Slice Of Life", + "Sobrenatural", + "Submissão", + "Succubus", + "Super Poder", + "Swimsuit", + "Tentáculos", + "Terror", + "Tetas", + "Thriller", + "Traição", + "Trem", + "Vampiros", + "Vanilla", + "Vida Escolar", + "Virgem", + "Voyeur", + "Yaoi", + "Yuri", + "Zoofilia", + ) + } +} diff --git a/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYUrlActivity.kt b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYUrlActivity.kt new file mode 100644 index 000000000..b78eb49d5 --- /dev/null +++ b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HYUrlActivity.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +/** + * Springboard that accepts https://hentaiyabu.com/hentai/intents + * and redirects them to the main Aniyomi process. + */ +class HYUrlActivity : Activity() { + + private val TAG = "HYUrlActivity" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val slug = pathSegments[1] + val searchQuery = HYConstants.PREFIX_SEARCH_SLUG + slug + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.ANIMESEARCH" + putExtra("query", searchQuery) + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e(TAG, e.toString()) + } + } else { + Log.e(TAG, "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +} diff --git a/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HentaiYabu.kt b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HentaiYabu.kt new file mode 100644 index 000000000..4a0e320e2 --- /dev/null +++ b/src/pt/hentaiyabu/src/eu/kanade/tachiyomi/animeextension/pt/hentaiyabu/HentaiYabu.kt @@ -0,0 +1,291 @@ +package eu.kanade.tachiyomi.animeextension.pt.hentaiyabu + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.pt.hentaiyabu.HYFilters.applyFilterParams +import eu.kanade.tachiyomi.animeextension.pt.hentaiyabu.extractors.PlayerOneExtractor +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.AnimesPage +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SEpisode +import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.lang.Exception + +class HentaiYabu : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "HentaiYabu" + + override val baseUrl = "https://hentaiyabu.com" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + private val json = Json { + ignoreUnknownKeys = true + } + + private var searchJson: List? = null + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("Accept-Language", HYConstants.ACCEPT_LANGUAGE) + .add("Referer", baseUrl) + .add("User-Agent", HYConstants.USER_AGENT) + + // ============================== Popular =============================== + override fun popularAnimeSelector(): String = "div.main-index > div.index-size > div.episodes-container > div.anime-episode" + override fun popularAnimeRequest(page: Int): Request = GET(baseUrl) + + override fun popularAnimeFromElement(element: Element): SAnime { + val anime: SAnime = SAnime.create() + val img = element.selectFirst("img") + val elementA = element.selectFirst("a") + anime.setUrlWithoutDomain(elementA.attr("href")) + anime.title = element.selectFirst("h3").text() + anime.thumbnail_url = img.attr("src") + return anime + } + + override fun popularAnimeNextPageSelector() = throw Exception("not used") + + override fun popularAnimeParse(response: Response): AnimesPage { + val document = response.asJsoup() + val animes = document.select(popularAnimeSelector()).map { element -> + popularAnimeFromElement(element) + } + return AnimesPage(animes, false) + } + + // ============================== Episodes ============================== + override fun episodeListSelector(): String = "div.left-single div.anime-episode" + + override fun episodeListParse(response: Response): List { + val url = response.request.url.toString() + val doc = if (url.contains("/video/")) { + getRealDoc(response.asJsoup()) + } else { + response.asJsoup() + } + return doc.select(episodeListSelector()).map { + episodeFromElement(it) + }.reversed() + } + override fun episodeFromElement(element: Element): SEpisode { + val episode = SEpisode.create() + val elementA = element.selectFirst("a") + episode.setUrlWithoutDomain(elementA.attr("href")) + val name = element.selectFirst("h3").text() + val epName = name.substringAfterLast("– ") + episode.name = epName + episode.episode_number = try { + epName.substringAfter(" ").substringBefore(" ").toFloat() + } catch (e: NumberFormatException) { 0F } + return episode + } + + // ============================ Video Links ============================= + override fun videoListParse(response: Response): List