diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 03c8efdc2..2253b24f6 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 000000000..ca0419468
--- /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 000000000..c98063f88
--- /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 000000000..521502ac0
--- /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 000000000..5f88b4b9e
--- /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 000000000..d6e2a3471
--- /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