From 8cbbcc6ae0acc2bfb7ee8d1110adb69273ccb16f Mon Sep 17 00:00:00 2001 From: jack Date: Sat, 4 Nov 2023 00:01:30 +0700 Subject: [PATCH] added Moenime --- .github/workflows/build.yml | 2 + Moenime/build.gradle.kts | 37 +++ Moenime/src/main/AndroidManifest.xml | 2 + .../src/main/kotlin/com/hexated/Extractors.kt | 14 ++ .../src/main/kotlin/com/hexated/Moenime.kt | 223 ++++++++++++++++++ .../main/kotlin/com/hexated/MoenimePlugin.kt | 16 ++ 6 files changed, 294 insertions(+) create mode 100644 Moenime/build.gradle.kts create mode 100644 Moenime/src/main/AndroidManifest.xml create mode 100644 Moenime/src/main/kotlin/com/hexated/Extractors.kt create mode 100644 Moenime/src/main/kotlin/com/hexated/Moenime.kt create mode 100644 Moenime/src/main/kotlin/com/hexated/MoenimePlugin.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03c8efdc..2253b24f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,6 +58,7 @@ jobs: PRIMEWIRE_KEY: ${{ secrets.PRIMEWIRE_KEY }} ZSHOW_API: ${{ secrets.ZSHOW_API }} SFMOVIES_API: ${{ secrets.SFMOVIES_API }} + MOENIME_API: ${{ secrets.MOENIME_API }} run: | cd $GITHUB_WORKSPACE/src echo SORA_API=$SORA_API >> local.properties @@ -76,6 +77,7 @@ jobs: echo PRIMEWIRE_KEY=$PRIMEWIRE_KEY >> local.properties echo ZSHOW_API=$ZSHOW_API >> local.properties echo SFMOVIES_API=$SFMOVIES_API >> local.properties + echo MOENIME_API=$MOENIME_API >> local.properties - name: Build Plugins run: | diff --git a/Moenime/build.gradle.kts b/Moenime/build.gradle.kts new file mode 100644 index 00000000..ca041946 --- /dev/null +++ b/Moenime/build.gradle.kts @@ -0,0 +1,37 @@ +import org.jetbrains.kotlin.konan.properties.Properties + +// use an integer for version numbers +version = 1 + +android { + defaultConfig { + val properties = Properties() + properties.load(project.rootProject.file("local.properties").inputStream()) + + buildConfigField("String", "MOENIME_API", "\"${properties.getProperty("MOENIME_API")}\"") + } +} + +cloudstream { + language = "id" + // All of these properties are optional, you can safely remove them + + // description = "Lorem Ipsum" + authors = listOf("Hexated") + + /** + * Status int as the following: + * 0: Down + * 1: Ok + * 2: Slow + * 3: Beta only + * */ + status = 0 // will be 3 if unspecified + tvTypes = listOf( + "AnimeMovie", + "Anime", + "OVA", + ) + + iconUrl = "https://cdn.discordapp.com/attachments/1170001679085744209/1170001727332810802/fast-forward.png?ex=65577405&is=6544ff05&hm=bdc8c8a9325e31ead9d528fd44a142e2254f29961679eb5196981cf9c06d2171&" +} \ No newline at end of file diff --git a/Moenime/src/main/AndroidManifest.xml b/Moenime/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c98063f8 --- /dev/null +++ b/Moenime/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Moenime/src/main/kotlin/com/hexated/Extractors.kt b/Moenime/src/main/kotlin/com/hexated/Extractors.kt new file mode 100644 index 00000000..521502ac --- /dev/null +++ b/Moenime/src/main/kotlin/com/hexated/Extractors.kt @@ -0,0 +1,14 @@ +package com.hexated + +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.extractors.StreamSB + +class Nyomo : StreamSB() { + override var name: String = "Nyomo" + override var mainUrl = "https://nyomo.my.id" +} + +class Streamhide : Filesim() { + override var name: String = "Streamhide" + override var mainUrl: String = "https://streamhide.to" +} \ No newline at end of file diff --git a/Moenime/src/main/kotlin/com/hexated/Moenime.kt b/Moenime/src/main/kotlin/com/hexated/Moenime.kt new file mode 100644 index 00000000..5f88b4b9 --- /dev/null +++ b/Moenime/src/main/kotlin/com/hexated/Moenime.kt @@ -0,0 +1,223 @@ +package com.hexated + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.Jsoup +import org.jsoup.nodes.Element + +class Moenime : MainAPI() { + private var apiUrl = BuildConfig.MOENIME_API + override val instantLinkLoading = true + override var name = "Moenime" + override val hasMainPage = true + override var lang = "id" + private var headers: Map = mapOf() + private var cookies: Map = mapOf() + override val supportedTypes = setOf( + TvType.Anime, + TvType.AnimeMovie, + TvType.OVA + ) + + override val mainPage = mainPageOf( + "$apiUrl/anime/ongoing?order_by=updated&page=" to "Sedang Tayang", + "$apiUrl/anime/finished?order_by=updated&page=" to "Selesai Tayang", + "$apiUrl/properties/season/summer-2022?order_by=most_viewed&page=" to "Dilihat Terbanyak Musim Ini", + "$apiUrl/anime/movie?order_by=updated&page=" to "Film Layar Lebar", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + + val home = document.select("div.col-lg-4.col-md-6.col-sm-6").mapNotNull { + it.toSearchResult() + } + + return newHomePageResponse(request.name, home) + } + + private fun getProperAnimeLink(uri: String): String { + return if (uri.contains("/episode")) { + Regex("(.*)/episode/.+").find(uri)?.groupValues?.get(1).toString() + "/" + } else { + uri + } + } + + private fun Element.toSearchResult(): AnimeSearchResponse? { + val href = getProperAnimeLink(fixUrl(this.selectFirst("a")!!.attr("href"))) + val title = this.selectFirst("h5 a")?.text() ?: return null + val posterUrl = fixUrl(this.select("div.product__item__pic.set-bg").attr("data-setbg")) + val episode = this.select("div.ep span").text().let { + Regex("Ep\\s(\\d+)\\s/").find(it)?.groupValues?.getOrNull(1)?.toIntOrNull() + } + + return newAnimeSearchResponse(title, href, TvType.Anime) { + this.posterUrl = posterUrl + addSub(episode) + } + + } + + override suspend fun search(query: String): List { + val link = "$apiUrl/anime?search=$query&order_by=latest" + val document = app.get(link).document + + return document.select("div#animeList div.product__item").mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse { + val document = app.get(url).document + + val title = document.selectFirst(".anime__details__title > h3")!!.text().trim() + val poster = document.selectFirst(".anime__details__pic")?.attr("data-setbg") + val tags = document.select("div.anime__details__widget > div > div:nth-child(2) > ul > li:nth-child(1)") + .text().trim().replace("Genre: ", "").split(", ") + + val year = Regex("\\D").replace( + document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(5)") + .text().trim().replace("Musim: ", ""), "" + ).toIntOrNull() + val status = getStatus( + document.select("div.anime__details__widget > div > div:nth-child(1) > ul > li:nth-child(3)") + .text().trim().replace("Status: ", "") + ) + val description = document.select(".anime__details__text > p").text().trim() + + val episodes = mutableListOf() + + for (i in 1..6) { + val doc = app.get("$url?page=$i").document + val eps = Jsoup.parse(doc.select("#episodeLists").attr("data-content")).select("a.btn.btn-sm.btn-danger") + .mapNotNull { + val name = it.text().trim() + val episode = Regex("(\\d+[.,]?\\d*)").find(name)?.groupValues?.getOrNull(0) + ?.toIntOrNull() + val link = it.attr("href") + Episode(link, episode = episode) + } + if(eps.isEmpty()) break else episodes.addAll(eps) + } + + val type = getType(document.selectFirst("div.col-lg-6.col-md-6 ul li:contains(Tipe:) a")?.text()?.lowercase() ?: "tv", episodes.size) + val recommendations = document.select("div#randomList > a").mapNotNull { + val epHref = it.attr("href") + val epTitle = it.select("h5.sidebar-title-h5.px-2.py-2").text() + val epPoster = it.select(".product__sidebar__view__item.set-bg").attr("data-setbg") + newAnimeSearchResponse(epTitle, epHref, TvType.Anime) { + this.posterUrl = epPoster + addDubStatus(dubExist = false, subExist = true) + } + } + + val tracker = APIHolder.getTracker(listOf(title),TrackerType.getTypes(type),year,true) + + return newAnimeLoadResponse(title, url, type) { + engName = title + posterUrl = tracker?.image ?: poster + backgroundPosterUrl = tracker?.cover + this.year = year + addEpisodes(DubStatus.Subbed, episodes) + showStatus = status + plot = description + this.tags = tags + this.recommendations = recommendations + addMalId(tracker?.malId) + addAniListId(tracker?.aniId?.toIntOrNull()) + } + + } + + private suspend fun invokeLocalSource( + url: String, + server: String, + ref: String, + callback: (ExtractorLink) -> Unit + ) { + val name = if(server.contains("drive")) "Main Server" else "Backup Server" + val document = app.get( + url, + referer = ref, + headers = headers, + cookies = cookies + ).document + document.select("video#player > source").map { + val link = fixUrl(it.attr("src")) + val quality = it.attr("size").toIntOrNull() + callback.invoke( + ExtractorLink( + name, + name, + link, + referer = "", + quality = quality ?: Qualities.Unknown.value, + ) + ) + } + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val req = app.get(data) + val res = req.document + val token = res.select("meta[name=csrf-token]").attr("content") + headers = mapOf( + "X-Requested-With" to "XMLHttpRequest", + "X-CSRF-TOKEN" to token + ) + cookies = req.cookies.toMutableMap() + .plus(mapOf(sessions to value)) + res.select("select#changeServer option").apmap { source -> + val server = source.attr("value") + val link = "$data?dfgRr1OagZvvxbzHNpyCy0FqJQ18mCnb=XNvyMgJO6J&twEvZlbZbYRWBdKKwxkOnwYF0VWoGGVg=$server" + if (server.contains(Regex("(?i)$server1|$server2"))) { + invokeLocalSource(link, server, data, callback) + } else { + app.get( + link, + referer = data, + headers = headers, + cookies = cookies + ).document.select("div.iframe-container iframe").attr("src").let { videoUrl -> + loadExtractor(fixUrl(videoUrl), "$apiUrl/", subtitleCallback, callback) + } + } + } + + return true + } + + companion object { + private var sessions = base64Decode("a3VyYW1hbmltZV9zZXNzaW9ucw==") + private var value = base64Decode("VXV3ZENIR1B3NVVBYlBTdDRpYXpUZFNpUHpCZXBhd2pEMmJoWjFDYWRkUzYyUUUwMlRLZVZtYWpKNnFKeWJ3SA==") + private val server1 = base64Decode("a3VyYW1hZHJpdmU=") + private val server2 = base64Decode("YXJjaGl2ZQ==") + + fun getType(t: String, s: Int): TvType { + return if (t.contains("OVA", true) || t.contains("Special")) TvType.OVA + else if (t.contains("Movie", true) && s == 1) TvType.AnimeMovie + else TvType.Anime + } + + fun getStatus(t: String): ShowStatus { + return when (t) { + "Selesai Tayang" -> ShowStatus.Completed + "Sedang Tayang" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + } +} \ No newline at end of file diff --git a/Moenime/src/main/kotlin/com/hexated/MoenimePlugin.kt b/Moenime/src/main/kotlin/com/hexated/MoenimePlugin.kt new file mode 100644 index 00000000..d6e2a347 --- /dev/null +++ b/Moenime/src/main/kotlin/com/hexated/MoenimePlugin.kt @@ -0,0 +1,16 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class MoenimePlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Moenime()) + registerExtractorAPI(Nyomo()) + registerExtractorAPI(Streamhide()) + } +} \ No newline at end of file