diff --git a/.gitignore b/.gitignore index 38a2c2605..42a86b55b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,8 @@ build/ repo/ apk/ gen -generated-src/ \ No newline at end of file +generated-src/ +.vscode/ +.project +.settings +.classpath \ No newline at end of file diff --git a/src/id/neonime/AndroidManifest.xml b/src/id/neonime/AndroidManifest.xml new file mode 100644 index 000000000..acb4de356 --- /dev/null +++ b/src/id/neonime/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/id/neonime/build.gradle b/src/id/neonime/build.gradle new file mode 100644 index 000000000..e4ae4f56d --- /dev/null +++ b/src/id/neonime/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'NeoNime' + pkgNameSuffix = 'id.neonime' + extClass = '.NeoNime' + extVersionCode = 1 + libVersion = '12' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/id/neonime/res/mipmap-hdpi/ic_launcher.png b/src/id/neonime/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..17985d5e2 Binary files /dev/null and b/src/id/neonime/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/id/neonime/res/mipmap-mdpi/ic_launcher.png b/src/id/neonime/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8aee77e68 Binary files /dev/null and b/src/id/neonime/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/id/neonime/res/mipmap-xhdpi/ic_launcher.png b/src/id/neonime/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..546b74e9a Binary files /dev/null and b/src/id/neonime/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/id/neonime/res/mipmap-xxhdpi/ic_launcher.png b/src/id/neonime/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..85b5361dc Binary files /dev/null and b/src/id/neonime/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/id/neonime/res/mipmap-xxxhdpi/ic_launcher.png b/src/id/neonime/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..09253537d Binary files /dev/null and b/src/id/neonime/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/id/neonime/res/web_hi_res_128.png b/src/id/neonime/res/web_hi_res_128.png new file mode 100644 index 000000000..170b02474 Binary files /dev/null and b/src/id/neonime/res/web_hi_res_128.png differ diff --git a/src/id/neonime/src/eu/kanade/tachiyomi/animeextension/id/neonime/NeoNime.kt b/src/id/neonime/src/eu/kanade/tachiyomi/animeextension/id/neonime/NeoNime.kt new file mode 100644 index 000000000..2e1989118 --- /dev/null +++ b/src/id/neonime/src/eu/kanade/tachiyomi/animeextension/id/neonime/NeoNime.kt @@ -0,0 +1,257 @@ +package eu.kanade.tachiyomi.animeextension.id.neonime + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +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 okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.select.Elements +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.text.SimpleDateFormat +import java.util.Locale + +class NeoNime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { + override val baseUrl: String = "https://neonime.cc" + override val lang: String = "id" + override val name: String = "NeoNime" + override val supportsLatest: Boolean = true + override val client: OkHttpClient = network.cloudflareClient + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + // Private Fun + private fun reconstructDate(Str: String): Long { + val pattern = SimpleDateFormat("dd-MM-yyyy", Locale.US) + return pattern.parse(Str)!!.time + } + private fun parseStatus(statusString: String): Int { + return when { + statusString.toLowerCase(Locale.US).contains("ongoing") -> SAnime.ONGOING + statusString.toLowerCase(Locale.US).contains("completed") -> SAnime.COMPLETED + else -> SAnime.UNKNOWN + } + } + + private fun getAnimeFromAnimeElement(element: Element): SAnime { + val anime = SAnime.create() + anime.setUrlWithoutDomain(element.select("a").first().attr("href")) + anime.thumbnail_url = element.select("a > div.image > img").first().attr("data-src") + anime.title = element.select("div.fixyear > div > h2").text() + return anime + } + + private fun getAnimeFromEpisodeElement(element: Element): SAnime { + val animepage = client.newCall(GET(element.select("td.bb > a").first().attr("href"))).execute().asJsoup() + val anime = SAnime.create() + anime.setUrlWithoutDomain(animepage.select("#fixar > div.imagen > a").first().attr("href")) + anime.thumbnail_url = animepage.select("#fixar > div.imagen > a > img").first().attr("data-src") + anime.title = animepage.select("#fixar > div.imagen > a > img").first().attr("alt") + return anime + } + + private fun getAnimeFromSearchElement(element: Element): SAnime { + val url = element.select("a").first().attr("href") + val animepage = client.newCall(GET(url)).execute().asJsoup() + val anime = SAnime.create() + anime.setUrlWithoutDomain(url) + anime.title = animepage.select("#info > div:nth-child(2) > span").text() + anime.thumbnail_url = animepage.select("div.imagen > img").first().attr("data-src") + anime.status = parseStatus(animepage.select("#info > div:nth-child(13) > span").text()) + anime.genre = animepage.select("#info > div:nth-child(3) > span > a").joinToString(", ") { it.text() } + // this site didnt provide artist and author + anime.artist = "Unknown" + anime.author = "Unknown" + + anime.description = animepage.select("#info > div.contenidotv").text() + return anime + } + + private fun getNumberFromEpsString(epsStr: String): String { + return epsStr.filter { it.isDigit() } + } + + // Popular + override fun popularAnimeFromElement(element: Element): SAnime = getAnimeFromAnimeElement(element) + + override fun popularAnimeNextPageSelector(): String = "#contenedor > div > div.box > div.box_item > div.peliculas > div.item_1.items > div.respo_pag > div.pag_b > a" + + override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/tvshows/page/$page") + + override fun popularAnimeSelector(): String = "div.items > div.item" + + // Latest + + override fun latestUpdatesNextPageSelector(): String = "#contenedor > div > div.box > div.box_item > div.peliculas > div.item_1.items > div.respo_pag > div.pag_b > a" + + override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/episode/page/$page") + + override fun latestUpdatesSelector(): String = "table > tbody > tr" + + override fun latestUpdatesFromElement(element: Element): SAnime = getAnimeFromEpisodeElement(element) + + // Search + + override fun searchAnimeFromElement(element: Element): SAnime = getAnimeFromSearchElement(element) + + override fun searchAnimeNextPageSelector(): String = "#contenedor > div > div.box > div.box_item > div.peliculas > div.item_1.items > div.respo_pag > div.pag_b > a" + + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { + // filter is not avilable in neonime site + return GET("$baseUrl/list-anime/") + } + + override fun searchAnimeSelector() = throw Exception("Not Used") + + private fun generateSelector(query: String): String = "div.letter-section > ul > li > a:contains($query)" + + override fun searchAnimeParse(response: Response) = throw Exception("Not Used") + + private fun searchQueryParse(response: Response, query: String): AnimesPage { + val document = response.asJsoup() + + val animes = filterNoBatch(document.select(generateSelector(query))).map { element -> + searchAnimeFromElement(element) + } + + return AnimesPage(animes, false) + } + + private fun filterNoBatch(eles: Elements): Elements { + val retElements = Elements() + + for (ele in eles) { + if (ele.attr("href").contains("/tvshows")) { + retElements.add(ele) + } + } + + return retElements + } + + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + val returnedSearch = searchAnimeRequest(page, query, filters) + return client.newCall(returnedSearch) + .asObservableSuccess() + .map { response -> + searchQueryParse(response, query) + } + } + + // Anime Details + + override fun animeDetailsParse(document: Document): SAnime { + val anime = SAnime.create() + anime.title = document.select("#info > div:nth-child(2) > span").text() + anime.thumbnail_url = document.select("div.imagen > img").first().attr("data-src") + anime.status = parseStatus(document.select("#info > div:nth-child(13) > span").text()) + anime.genre = document.select("#info > div:nth-child(3) > span > a").joinToString(", ") { it.text() } + // this site didnt provide artist and author + anime.artist = "Unknown" + anime.author = "Unknown" + + anime.description = document.select("#info > div.contenidotv").text() + return anime + } + + // Episode List + + override fun episodeListSelector(): String = "ul.episodios > li" + + override fun episodeFromElement(element: Element): SEpisode { + val episode = SEpisode.create() + val epsNum = getNumberFromEpsString(element.select("div.episodiotitle > a").text()) + episode.setUrlWithoutDomain(element.select("div.episodiotitle > a").attr("href")) + episode.episode_number = when { + (epsNum.isNotEmpty()) -> epsNum.toFloat() + else -> 1F + } + episode.name = element.select("div.episodiotitle > a").text() + episode.date_upload = reconstructDate(element.select("div.episodiotitle > span.date").text()) + + return episode + } + + // Video + override fun videoListSelector() = "div > ul >ul > li >a:nth-child(6)" + + override fun videoUrlParse(document: Document) = throw Exception("not used") + + override fun List