diff --git a/src/id/nimegami/AndroidManifest.xml b/src/id/nimegami/AndroidManifest.xml new file mode 100644 index 000000000..dc72cdf93 --- /dev/null +++ b/src/id/nimegami/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/id/nimegami/build.gradle b/src/id/nimegami/build.gradle new file mode 100644 index 000000000..a81bb2375 --- /dev/null +++ b/src/id/nimegami/build.gradle @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) +} + +ext { + extName = 'NimeGami' + pkgNameSuffix = 'id.nimegami' + extClass = '.NimeGami' + extVersionCode = 1 +} + +dependencies { + implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1") + implementation(project(":lib-synchrony")) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/id/nimegami/res/mipmap-hdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..591180bcf Binary files /dev/null and b/src/id/nimegami/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/id/nimegami/res/mipmap-mdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..80325dbe3 Binary files /dev/null and b/src/id/nimegami/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/id/nimegami/res/mipmap-xhdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..00bfa0a6e Binary files /dev/null and b/src/id/nimegami/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/id/nimegami/res/mipmap-xxhdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..960f27134 Binary files /dev/null and b/src/id/nimegami/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/id/nimegami/res/mipmap-xxxhdpi/ic_launcher.png b/src/id/nimegami/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..019e15738 Binary files /dev/null and b/src/id/nimegami/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/id/nimegami/src/eu/kanade/tachiyomi/animeextension/id/nimegami/NimeGami.kt b/src/id/nimegami/src/eu/kanade/tachiyomi/animeextension/id/nimegami/NimeGami.kt new file mode 100644 index 000000000..1621e0265 --- /dev/null +++ b/src/id/nimegami/src/eu/kanade/tachiyomi/animeextension/id/nimegami/NimeGami.kt @@ -0,0 +1,270 @@ +package eu.kanade.tachiyomi.animeextension.id.nimegami + +import android.util.Base64 +import dev.datlag.jsunpacker.JsUnpacker +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.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import uy.kohesive.injekt.injectLazy +import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator as Synchrony + +class NimeGami : ParsedAnimeHttpSource() { + + override val name = "NimeGami" + + override val baseUrl = "https://nimegami.id" + + override val lang = "id" + + override val supportsLatest = true + + private val json: Json by injectLazy() + + // ============================== Popular =============================== + override fun popularAnimeRequest(page: Int) = GET(baseUrl) + + override fun popularAnimeSelector() = "div.wrapper-2-a > article > a" + + override fun popularAnimeFromElement(element: Element) = SAnime.create().apply { + setUrlWithoutDomain(element.attr("href")) + thumbnail_url = element.selectFirst("img")!!.attr("data-lazy-src") + title = element.selectFirst("div.title-post2")!!.text() + } + + override fun popularAnimeNextPageSelector() = null + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page") + + override fun latestUpdatesSelector() = "div.post article" + + override fun latestUpdatesFromElement(element: Element) = SAnime.create().apply { + element.selectFirst("h2 > a")!!.let { + setUrlWithoutDomain(it.attr("href")) + title = it.text() + } + thumbnail_url = element.selectFirst("img")!!.attr("srcset").substringBefore(" ") + } + + override fun latestUpdatesNextPageSelector() = "ul.pagination > li > a:contains(Next)" + + // =============================== Search =============================== + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/$id")) + .asObservableSuccess() + .map(::searchAnimeByIdParse) + } else { + super.fetchSearchAnime(page, query, filters) + } + } + + private fun searchAnimeByIdParse(response: Response): AnimesPage { + val details = animeDetailsParse(response.asJsoup()) + return AnimesPage(listOf(details), false) + } + + // TODO: Add support for search filters + override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = + GET("$baseUrl/page/$page/?s=$query&post_type=post") + + override fun searchAnimeSelector() = "div.archive > div > article" + + override fun searchAnimeFromElement(element: Element) = latestUpdatesFromElement(element) + + override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector() + + // =========================== Anime Details ============================ + override fun animeDetailsParse(document: Document) = SAnime.create().apply { + setUrlWithoutDomain(document.location()) + thumbnail_url = document.selectFirst("div.coverthumbnail img")!!.attr("src") + val infosDiv = document.selectFirst("div.info2 > table > tbody")!! + title = infosDiv.getInfo("Judul:") + ?: document.selectFirst("h2[itemprop=name]")!!.text() + genre = infosDiv.getInfo("Kategori") + artist = infosDiv.getInfo("Studio") + status = with(document.selectFirst("h1.title")?.text().orEmpty()) { + when { + contains("(On-Going)") -> SAnime.ONGOING + contains("(End)") || contains("(Movie)") -> SAnime.COMPLETED + else -> SAnime.UNKNOWN + } + } + + description = buildString { + document.select("div#Sinopsis p").eachText().forEach { + append("$it\n") + } + + val nonNeeded = listOf("Judul:", "Kategori", "Studio") + infosDiv.select("tr") + .eachText() + .filterNot(nonNeeded::contains) + .forEach { append("\n$it") } + } + } + + private fun Element.getInfo(info: String) = + selectFirst("tr:has(td.tablex:contains($info))")?.text()?.substringAfter(": ") + + // ============================== Episodes ============================== + override fun episodeListParse(response: Response) = super.episodeListParse(response).reversed() + + override fun episodeListSelector() = "div.list_eps_stream > li.select-eps" + + override fun episodeFromElement(element: Element) = SEpisode.create().apply { + val num = element.attr("id").substringAfterLast("_") + episode_number = num.toFloatOrNull() ?: 1F + name = "Episode $num" + url = element.attr("data") + } + + // ============================ Video Links ============================= + override fun fetchVideoList(episode: SEpisode): Observable> { + val qualities = json.decodeFromString>(episode.url.b64Decode()) + val episodeIndex = episode.episode_number.toInt() - 1 + var usedBunga = false // to prevent repeating the same request to bunga.nimegami + return qualities.flatMap { + val quality = it.format + it.url.mapNotNull { url -> + if (url.contains("bunga.nimegami")) { + if (usedBunga) { + return@mapNotNull null + } else usedBunga = true + } + runCatching { + extractVideos(url, quality, episodeIndex) + }.getOrElse { emptyList() } + }.flatten() + }.let { Observable.just(it) } + } + + private fun extractVideos(url: String, quality: String, episodeIndex: Int): List