diff --git a/src/es/latanime/AndroidManifest.xml b/src/es/latanime/AndroidManifest.xml new file mode 100644 index 000000000..acb4de356 --- /dev/null +++ b/src/es/latanime/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/es/latanime/build.gradle b/src/es/latanime/build.gradle new file mode 100644 index 000000000..2307dc802 --- /dev/null +++ b/src/es/latanime/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Latanime' + pkgNameSuffix = 'es.latanime' + extClass = '.Latanime' + extVersionCode = 1 + libVersion = '13' +} + +dependencies { + implementation(project(':lib-fembed-extractor')) + implementation(project(':lib-okru-extractor')) + implementation(project(':lib-dood-extractor')) + implementation(project(':lib-streamsb-extractor')) + implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1" +} + +apply from: "$rootDir/common.gradle" diff --git a/src/es/latanime/res/mipmap-hdpi/ic_launcher.png b/src/es/latanime/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..70b9017a6 Binary files /dev/null and b/src/es/latanime/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/es/latanime/res/mipmap-mdpi/ic_launcher.png b/src/es/latanime/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..ba8b1e174 Binary files /dev/null and b/src/es/latanime/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/es/latanime/res/mipmap-xhdpi/ic_launcher.png b/src/es/latanime/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..519981d7a Binary files /dev/null and b/src/es/latanime/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/es/latanime/res/mipmap-xxhdpi/ic_launcher.png b/src/es/latanime/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..a9406e886 Binary files /dev/null and b/src/es/latanime/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/es/latanime/res/mipmap-xxxhdpi/ic_launcher.png b/src/es/latanime/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..8cd29bc51 Binary files /dev/null and b/src/es/latanime/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/es/latanime/res/web_hi_res_512.png b/src/es/latanime/res/web_hi_res_512.png new file mode 100644 index 000000000..c67e40f79 Binary files /dev/null and b/src/es/latanime/res/web_hi_res_512.png differ diff --git a/src/es/latanime/src/eu/kanade/tachiyomi/animeextension/es/latanime/Latanime.kt b/src/es/latanime/src/eu/kanade/tachiyomi/animeextension/es/latanime/Latanime.kt new file mode 100644 index 000000000..5dcfaa820 --- /dev/null +++ b/src/es/latanime/src/eu/kanade/tachiyomi/animeextension/es/latanime/Latanime.kt @@ -0,0 +1,392 @@ +package eu.kanade.tachiyomi.animeextension.es.latanime + +import android.app.Application +import android.content.SharedPreferences +import android.util.Base64 +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.animeextension.es.latanime.extractors.Mp4uploadExtractor +import eu.kanade.tachiyomi.animeextension.es.latanime.extractors.UploadExtractor +import eu.kanade.tachiyomi.animeextension.es.latanime.extractors.YourUploadExtractor +import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +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.lib.doodextractor.DoodExtractor +import eu.kanade.tachiyomi.lib.fembedextractor.FembedExtractor +import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor +import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.lang.Exception + +class Latanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + + override val name = "Latanime" + + override val baseUrl = "https://latanime.org" + + override val lang = "es" + + override val supportsLatest = false + + override val client: OkHttpClient = network.cloudflareClient + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // ============================== Popular =============================== + + override fun popularAnimeSelector(): String = "div.row > div" + + override fun popularAnimeRequest(page: Int): Request { + return GET("$baseUrl/emision?p=$page") + } + + override fun popularAnimeFromElement(element: Element): SAnime { + val anime = SAnime.create() + + anime.setUrlWithoutDomain(element.selectFirst("a").attr("href").toHttpUrl().encodedPath) + anime.title = element.selectFirst("div.seriedetails > h3").text() + anime.thumbnail_url = element.selectFirst("img").attr("src") + + return anime + } + + override fun popularAnimeNextPageSelector(): String = "ul.pagination > li.active ~ li:has(a)" + + // =============================== Latest =============================== + + override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used") + + override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used") + + override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") + + override fun latestUpdatesSelector(): String = throw Exception("Not used") + + // =============================== Search =============================== + + override fun searchAnimeSelector(): String = "div.row > div:has(a)" + + override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) + + override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector() + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + val filterList = if (filters.isEmpty()) getFilterList() else filters + + val yearFilter = filterList.find { it is YearFilter } as YearFilter + val genreFilter = filterList.find { it is GenreFilter } as GenreFilter + val letterFilter = filterList.find { it is LetterFilter } as LetterFilter + + return when { + query.isNotBlank() -> GET("$baseUrl/buscar?q=$query&p=$page") + else -> { + GET("$baseUrl/animes?fecha=${yearFilter.toUriPart()}&genero=${genreFilter.toUriPart()}&letra=${letterFilter.toUriPart()}") + } + } + } + + // ============================== Filters =============================== + + override fun getFilterList(): AnimeFilterList = AnimeFilterList( + AnimeFilter.Header("La busqueda por texto ignora el filtro"), + YearFilter(), + GenreFilter(), + LetterFilter() + ) + + private class YearFilter : UriPartFilter( + "Año", + arrayOf( + Pair("Seleccionar", "false"), + Pair("2023", "2023"), + Pair("2022", "2022"), + Pair("2021", "2021"), + Pair("2020", "2020"), + Pair("2019", "2019"), + Pair("2018", "2018"), + Pair("2017", "2017"), + Pair("2016", "2016"), + Pair("2015", "2015"), + Pair("2014", "2014"), + Pair("2013", "2013"), + Pair("2012", "2012"), + Pair("2011", "2011"), + Pair("2010", "2010"), + Pair("2009", "2009"), + Pair("2008", "2008"), + Pair("2007", "2007"), + Pair("2006", "2006"), + Pair("2005", "2005"), + Pair("2004", "2004"), + Pair("2003", "2003"), + Pair("2002", "2002"), + Pair("2001", "2001"), + Pair("2000", "2000"), + Pair("1999", "1999"), + Pair("1998", "1998"), + Pair("1997", "1997"), + Pair("1996", "1996"), + Pair("1995", "1995"), + Pair("1994", "1994"), + Pair("1993", "1993"), + Pair("1992", "1992"), + Pair("1991", "1991"), + Pair("1990", "1990"), + Pair("1989", "1989"), + Pair("1988", "1988"), + Pair("1987", "1987"), + Pair("1986", "1986"), + Pair("1985", "1985"), + Pair("1984", "1984"), + Pair("1983", "1983"), + Pair("1982", "1982") + ) + ) + + private class GenreFilter : UriPartFilter( + "Genéros", + arrayOf( + Pair("Seleccionar", "false"), + Pair("Acción", "accion"), + Pair("Aventura", "aventura"), + Pair("Carreras", "carreras"), + Pair("Ciencia Ficción", "ciencia-ficcion"), + Pair("Comedia", "comedia"), + Pair("Cyberpunk", "cyberpunk"), + Pair("Deportes", "deportes"), + Pair("Drama", "drama"), + Pair("Ecchi", "ecchi"), + Pair("Escolares", "escolares"), + Pair("Fantasía", "fantasia"), + Pair("Gore", "gore"), + Pair("Harem", "harem"), + Pair("Horror", "horror"), + Pair("Josei", "josei"), + Pair("Lucha", "lucha"), + Pair("Magia", "magia"), + Pair("Mecha", "mecha"), + Pair("Militar", "militar"), + Pair("Misterio", "misterio"), + Pair("Música", "musica"), + Pair("Parodias", "parodias"), + Pair("Psicológico", "psicologico"), + Pair("Recuerdos de la vida", "recuerdos-de-la-vida"), + Pair("Seinen", "seinen"), + Pair("Shojo", "shojo"), + Pair("Shonen", "shonen"), + Pair("Sobrenatural", "sobrenatural"), + Pair("Vampiros", "vampiros"), + Pair("Yaoi", "yaoi"), + Pair("Yuri", "yuri"), + Pair("Latino", "latino"), + Pair("Espacial", "espacial"), + Pair("Histórico", "historico"), + Pair("Samurai", "samurai"), + Pair("Artes Marciales", "artes-marciales"), + Pair("Demonios", "demonios"), + Pair("Romance", "romance"), + Pair("Dementia", "dementia"), + Pair("Policía", "policia"), + Pair("Castellano", "castellano"), + Pair("Historia paralela", "historia-paralela"), + Pair("Aenime", "aenime"), + Pair("Donghua", "donghua"), + Pair("Blu-ray", "blu-ray"), + Pair("Monogatari", "monogatari") + ) + ) + + private class LetterFilter : UriPartFilter( + "Letra", + arrayOf( + Pair("Seleccionar", "false"), + Pair("0-9", "09"), + 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") + ) + ) + + private open class UriPartFilter(displayName: String, val vals: Array>) : + AnimeFilter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } + + // =========================== Anime Details ============================ + + override fun animeDetailsParse(document: Document): SAnime { + val anime = SAnime.create() + anime.title = document.select("div.row > div > h2").text() + anime.genre = document.select("div.row > div > a:has(div.btn)").eachText().joinToString(separator = ", ") + anime.description = document.selectFirst("div.row > div > p.my-2").text() + return anime + } + + // ============================== Episodes ============================== + + override fun episodeListParse(response: Response): List { + val document = response.asJsoup() + return document.select(episodeListSelector()).map { episodeFromElement(it) }.reversed() + } + + override fun episodeListSelector() = "div.row > div > div.row > div > a" + + override fun episodeFromElement(element: Element): SEpisode { + val episode = SEpisode.create() + val title = element.text() + episode.episode_number = title.substringAfter("Capitulo ").toFloatOrNull() ?: 0F + episode.name = title.replace("- ", "") + episode.setUrlWithoutDomain(element.attr("href").toHttpUrl().encodedPath) + return episode + } + + // ============================ Video Links ============================= + + override fun videoListParse(response: Response): List